Commit 73cdfb2e by luoqi

merge: fix/migrate-concurrently-txn → test(解 migrate 卡死:多语句迁移不能写 CONCURRENTLY)

parents 765f70c1 8dba340f
Pipeline #3465 failed in 0 seconds
......@@ -20,9 +20,13 @@
-- 故用 CONCURRENTLY(不锁写)。
--
-- ⚠️ 关于「Prisma migrate 有事务包裹所以跑不了 CONCURRENTLY」——
-- 那是 20260701060607 里的说法,**在当前 Prisma 6.19 已不成立**:2026-07-27 本地实测
-- `prisma migrate deploy` 直接把本文件跑通、索引建出、plan 生效。所以本条**可以**随
-- migrate deploy 走,不需要 `migrate resolve` 那套。
-- 本条实测能随 migrate deploy 跑通,但**别把它总结成「Prisma 新版已经没事务包裹了」**
-- (本文件原来就是这么写的,错的归因,2026-07-28 已订正)。
-- 真正的分界线是**本文件里的语句条数**:Prisma 把整份 migration.sql 用一次 simple query
-- 发给 Postgres,而 Postgres 只对「一个查询串里多条语句」隐式开事务块。
-- 本文件只有 1 条语句 → 没有隐式事务 → CONCURRENTLY 跑得通;
-- 多条语句的迁移里写 CONCURRENTLY 必挂 25001,还会把 _prisma_migrations 堵死(P3018)。
-- 2026-07-28 的 20260728020000 就是这么炸的,那份文件头有完整复盘。
--
-- 但**推荐仍手动先建**,理由与事务无关,是停机时间:
-- deploy 时 pac-service 通过 depends_on 等 pac-migrate 退出才启动,而 33/64GB 建索引要几分钟
......
......@@ -22,8 +22,9 @@
--
-- 【CONCURRENTLY 与上线方式】同 20260727070000(sync_log_id 那条):
-- 普通 CREATE INDEX 会 ACCESS EXCLUSIVE 锁全表数分钟,摄入与 push 全挂,故用 CONCURRENTLY。
-- 当前 Prisma 6.19 可以直接 `migrate deploy` 跑通(20260701060607 里"有事务包裹跑不了"的
-- 说法已不成立,2026-07-27 实测)。但**推荐部署前手动先建**:deploy 时 pac-service 要等
-- 本条可以直接 `migrate deploy` 跑通 —— 但原因是**本文件只有 1 条语句**(Postgres 只对
-- 多语句查询串隐式开事务块),不是「Prisma 新版没有事务包裹了」。原注释那句错误归因
-- 2026-07-28 已订正,详见 20260727070000 与 20260728020000 的文件头。但**推荐部署前手动先建**:deploy 时 pac-service 要等
-- pac-migrate 退出才启动,几分钟索引构建 = 白等的停机。手动建好后本条 IF NOT EXISTS 空跑:
-- docker exec pac-postgres-1 psql -U pac -d pac -c \
-- 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "patient_transactions_host_id_subject_type_patient_id_idx" \
......
......@@ -45,10 +45,27 @@
-- 生产实测 14 条合计 **220MB**(表 5.4GB,占 4%)。写入侧同理:插一行特征至多多维护 1 条索引,
-- 画像重算不会被拖垮。
--
-- 【CONCURRENTLY 与上线方式】同 20260727110000:普通 CREATE INDEX 会 ACCESS EXCLUSIVE 锁全表,
-- 画像重算与摄入全挂,故用 CONCURRENTLY。**推荐部署前手动先建**(deploy 时 pac-service 要等
-- pac-migrate 退出,14 条索引串行构建 = 白等的停机);手动建好后本文件全部 IF NOT EXISTS 空跑。
-- 生产实测单条 4-17s,14 条共 **88 秒**(不锁表,期间业务照常);测试服磁盘慢,单条约 42s。
-- 【⚠️ 上线方式:大表环境必须部署前手工 CONCURRENTLY 先建】
-- 本文件里写的是**普通** CREATE INDEX(会 ACCESS EXCLUSIVE 锁全表),这是被迫的,见下条。
-- 所以对**已有数据的库**,部署前必须先手工建好,让本文件的 IF NOT EXISTS 空跑:
-- docker exec pac-postgres-1 psql -U pac -d pac -c "CREATE INDEX CONCURRENTLY IF NOT EXISTS ..."
-- (托管 PG 无 postgres 容器,用 pac-service 容器里的 node + prisma $executeRawUnsafe 发同样的 DDL)
-- 生产实测单条 4-17s、14 条共 88 秒;测试服盘慢,单条约 42s、共 238s。全程不锁表,业务照常。
-- 从零建库的环境不用管:那时 persona_features 是空表,普通 CREATE INDEX 瞬间完成。
--
-- 【为什么不能在这里写 CONCURRENTLY —— 2026-07-28 踩爆过,别再改回去】
-- Prisma 把整份 migration.sql 用**一次 simple query** 发给 Postgres,而 Postgres 对
-- 「一个查询串里多条语句」会隐式开事务块;CONCURRENTLY 明确禁止在事务块内跑:
-- ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block (25001)
-- 分界线是**语句条数,不是 Prisma 版本**:
-- 20260701060607 / 20260727070000 / 20260727110000 各只有 1 条语句 → 无隐式事务 → 跑得通
-- 本文件 14 条 → 隐式事务 → 必挂
-- 20260727110000 里那句「Prisma 6.19 已能直接跑通 CONCURRENTLY」是**错的归因**(单条侥幸),
-- 我照抄进本文件,结果测试服 deploy 挂在 migrate 步,pac-service 起不来。
-- 更坑的是失败后 _prisma_migrations 留下一条 finished_at/rolled_back_at 全空、
-- applied_steps_count=0 的记录,Prisma 从此拒绝执行**任何**后续迁移(P3018)——
-- 整条部署流水线被堵死,跟部署什么内容无关。解法:
-- npx prisma migrate resolve --rolled-back 20260728020000_persona_features_tag_filter_indexes
--
-- 【上线后实测】生产 2026-07-28 已手动建完,同口径复测(count + findMany 并发,墙钟):
-- contraindication:surgery 23,379ms → **84ms** (278×,命中 1,347 单)
......@@ -62,33 +79,33 @@
-- 禁忌 4,210ms → **344ms**。那台盘慢,绝对值别拿去跟生产比,看倍数即可。
-- ── 标量维度(equals):btree(表达式, persona_id),第二列盖住 EXISTS 的输出列 ──────────────
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_rfm_segment_idx"
CREATE INDEX IF NOT EXISTS "persona_features_rfm_segment_idx"
ON "persona_features" (((data #> '{segment}'::text[])), "persona_id") WHERE "key" = 'rfm';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_lifecycle_stage_stage_idx"
CREATE INDEX IF NOT EXISTS "persona_features_lifecycle_stage_stage_idx"
ON "persona_features" (((data #> '{stage}'::text[])), "persona_id") WHERE "key" = 'lifecycle_stage';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_urgency_level_level_idx"
CREATE INDEX IF NOT EXISTS "persona_features_urgency_level_level_idx"
ON "persona_features" (((data #> '{level}'::text[])), "persona_id") WHERE "key" = 'urgency_level';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_age_bracket_bracket_idx"
CREATE INDEX IF NOT EXISTS "persona_features_age_bracket_bracket_idx"
ON "persona_features" (((data #> '{bracket}'::text[])), "persona_id") WHERE "key" = 'age_bracket';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_gender_gender_idx"
CREATE INDEX IF NOT EXISTS "persona_features_gender_gender_idx"
ON "persona_features" (((data #> '{gender}'::text[])), "persona_id") WHERE "key" = 'gender';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_family_structure_structure_idx"
CREATE INDEX IF NOT EXISTS "persona_features_family_structure_structure_idx"
ON "persona_features" (((data #> '{structure}'::text[])), "persona_id") WHERE "key" = 'family_structure';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_referral_champion_type_idx"
CREATE INDEX IF NOT EXISTS "persona_features_referral_champion_type_idx"
ON "persona_features" (((data #> '{type}'::text[])), "persona_id") WHERE "key" = 'referral_champion';
-- ── 数组维度(array_contains → @>):GIN jsonb_path_ops ────────────────────────────────────
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_contraindication_domains_idx"
CREATE INDEX IF NOT EXISTS "persona_features_contraindication_domains_idx"
ON "persona_features" USING gin (((data #> '{domains}'::text[])) jsonb_path_ops) WHERE "key" = 'contraindication';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_entitlement_status_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_entitlement_status_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'entitlement_status';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_treatment_history_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_treatment_history_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'treatment_history';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_treatment_sensitivity_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_treatment_sensitivity_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'treatment_sensitivity';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_special_attention_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_special_attention_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'special_attention';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_time_preference_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_time_preference_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'time_preference';
CREATE INDEX CONCURRENTLY IF NOT EXISTS "persona_features_potential_treatment_types_idx"
CREATE INDEX IF NOT EXISTS "persona_features_potential_treatment_types_idx"
ON "persona_features" USING gin (((data #> '{types}'::text[])) jsonb_path_ops) WHERE "key" = 'potential_treatment';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment