perf(persona): 画像圈人筛选补 14 条偏索引 —— 生产实测 23.4s → 84ms
生产现象:召回池按「禁忌:手术禁忌」圈人,前端等 20+ 秒
GET /pac/v1/plans?view=pool&personaTags=contraindication:surgery
服务端复现:count 23,379ms + findMany 22,208ms(两句并发,墙钟 ≈23s)。
**不是禁忌特有** —— 测试服同口径按 rfm:重要价值 更慢,57s。14 个圈人维度全踩同一个坑,
禁忌只是这次先点到的那个。
根因:Prisma 把画像筛选编译成两层 EXISTS,最内层
persona_features.key = 'contraindication'
AND (data #> '{domains}') @> '"surgery"'
而 persona_features 只有单列 key 索引,JSON 那半截没有索引可下推。
生产 EXPLAIN (ANALYZE, BUFFERS) 实测(7,657,944 行 / 5.4GB,堆 3.4GB):
Index Scan using persona_features_key_idx actual time=3567..3954ms
Index Cond: (key = 'contraindication')
Filter: ((data #> '{domains}') @> '"surgery"')
rows=2,156 Rows Removed by Filter: 188,054 ← 命中率 1.1%
Buffers: shared read=162,830 ← 约 1.3GB 随机堆读
即为了捞 2,156 行,把该维度 190,210 行的 jsonb 全从磁盘搬了一遍。维度越大越惨:
rfm / gender / lifecycle_stage 各 869,043 行,一次筛人搬 869k 行 data。
改动:每个可筛维度一条偏索引(WHERE key='<维度>'),表达式与 Prisma 生成的完全一致。
- 数组维度 7 条(array_contains → @>)→ GIN jsonb_path_ops
- 标量维度 7 条(equals → =) → btree (表达式, persona_id)
第二列不是为了排序,是为了 index-only scan:内层 EXISTS 只 SELECT persona_id,
索引自带就不回堆。低选择性维度非它不可 —— 测试服实测 rfm 走上 btree 后
Bitmap Index Scan 只花 217ms,回堆 143,187 个块却花了 103s(work_mem 不够还退化成
lossy,Rows Removed by Index Recheck 1,564,553)。
Prisma schema 表达不了表达式/偏索引,只能落迁移 SQL;本地 migrate dev 会把它们报成 drift,
别按提示 reset(同类先例 20260727070000 / 20260727110000)。
persona-tag-filters.ts 加纪律注释:往 PERSONA_TAG_FILTER_DIMS 加维度必须同步补索引,
漏建就是 20 秒起步 —— 这次禁忌踩的正是这个。
上线(两台均已 CONCURRENTLY 手工建完,不锁表,业务无感;迁移全 IF NOT EXISTS 故 deploy 空跑):
生产 14 条 / 88s / 220MB,pg_index 无 invalid
contraindication:surgery 23,379ms → 84ms(278×,1,347 单)
contraindication:implant 842ms(25,900 单)
rfm:important_value 1,558ms(61,300 单)
treatment_history:implant_history 854ms(17,179 单)
gender:female 2-4s (105,110 单)
测试服 14 条 / 238s / 195MB(盘慢,看倍数)
rfm:important_value 57,299ms → 8,576ms
contraindication:surgery 4,210ms → 344ms
剩下的耗时性质已经变了:不再是 JSON 过滤,而是结果集本身大(count(*) 要数完十万级 plan)。
gender 这类宽维度哪天嫌慢,方向是给 count 做近似/缓存,不是继续加索引。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found errors in your .gitlab-ci.yml:
- jobs:openapi-drift config contains unknown keys: rules
| Status | Job ID | Name | Coverage |
|---|