perf(db): 补齐三处"只差索引"的热查询缺口

审计方式:pg_stat_statements 按"每次调用读取块数"排序 + EXPLAIN (ANALYZE, BUFFERS) 复现,
每条都先在线上并发建索引验证有效再固化(生产上索引已存在,IF NOT EXISTS 跳过)。

① 后台订单列表:COALESCE(paid_at, created_at) 同时出现在 WHERE 与 ORDER BY,
   函数包裹列非 sargable —— 默认"近 7 天"视角本身就会全表扫 orders 再排序。
   实测 26 万行/443MB:Parallel Seq Scan 读 256MB、577ms
   → Index Scan 读 0.3MB、2.05ms(约 280 倍)。
② 重跑快手补单:NOT EXISTS 打在 work_orders.order_id 上,而该列此前**没有索引**,
   反连接只能 Hash Anti Join —— 为几十个候选 id 顺序扫完 500MB 宽表,LIMIT 100 拦不住。
   实测 188MB、120ms → Index Only Scan 1.2ms(约 100 倍)。同时补 orders(order_status)。
③ 快手凭证"同步源"列表的 (oid, updated_at DESC, id DESC):29.6 万行/678MB,实测 568MB、5.5 秒。
   仅建索引不足以改变计划(查询写法要求先物化全部 oid,见审计报告),但它是后续
   改写查询的前提,先建好避免再等一次大表建索引。

验证:后端 534 项单测全绿(新增 1 项守卫)。
This commit is contained in:
yml2213
2026-10-01 13:48:05 +08:00
parent 92d7887108
commit 2d75be702d
2 changed files with 58 additions and 0 deletions
@@ -0,0 +1,30 @@
-- 086_verified_hot_query_indexes.sql —— 三处"只差索引"的热查询缺口(生产已 EXPLAIN 前后对比验证)。
--
-- 审计方式:pg_stat_statements 按"每次调用读取块数"排序 + EXPLAIN (ANALYZE, BUFFERS) 复现。
-- 以下每条都先在线上并发建索引验证有效,再固化成迁移(迁移会在事务内执行,
-- 所以这里用普通 CREATE INDEX;生产上索引已存在,IF NOT EXISTS 直接跳过)。
-- ① 后台订单列表:COALESCE(paid_at, created_at) 同时出现在 WHERE 与 ORDER BY,
-- 函数包裹列 → 非 sargable,默认"近 7 天"视角本身就会全表扫 orders 再排序。
-- 实测(26 万行 / 443MB):Parallel Seq Scan 读 256MB、577ms
-- → Index Scan 读 0.3MB、2.05ms(约 280 倍)。
CREATE INDEX IF NOT EXISTS idx_orders_effective_ordered_at
ON orders ((COALESCE(paid_at, created_at)) DESC NULLS LAST, id DESC);
-- ② 重跑快手补单:NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_id = o.id)
-- 而 work_orders 上此前**没有 order_id 索引** → 反连接只能 Hash Anti Join,
-- 为几十个候选 id 顺序扫完整张 500MB 宽表(LIMIT 100 拦不住)。
-- 实测:Seq Scan on work_orders 读 188MB、120ms
-- → Index Only Scan 用 idx_work_orders_order_id,1.2ms(约 100 倍)。
CREATE INDEX IF NOT EXISTS idx_work_orders_order_id ON work_orders (order_id);
-- 同一查询里 orders.order_status 也没有索引,一并补上。
CREATE INDEX IF NOT EXISTS idx_orders_order_status ON orders (order_status);
-- ③ 快手凭证「同步源」列表:SELECT DISTINCT ON (oid) ... ORDER BY oid, updated_at DESC, id DESC。
-- 该表 29.6 万行 / 678MB,实测 568MB、5.5 秒。
-- ⚠️ 仅建此索引不足以让计划改走索引:查询把 DISTINCT ON 放在子查询里、外层再按
-- updated_at 排序,语义上必须先物化全部 oid 才能排序,规划器因此宁可顺序扫。
-- 索引是"改写查询"的前提(见审计报告:应改为以 updated_at 顺序驱动 + 反连接去重),
-- 先建好避免后续改查询时再等一次大表建索引。
CREATE INDEX IF NOT EXISTS idx_kuaishou_vouchers_oid_updated
ON kuaishou_industry_vouchers (oid, updated_at DESC, id DESC);