perf(db): 统计的「状态+日期范围」组合补 created_at 键覆盖索引
087 以 id 为键覆盖了统计要读的窄列,但日期条件无法用它驱动扫描 (created_at 不是前导列),规划器因此对 work_orders 做整表 Hash Join: 生产实测该变体每次读 400~700MB —— 缓存热时 283~694ms,缓存有压力时 926ms+ 并触发慢查询告警。而"状态 + 近 7/30 天"正是运营最常用的筛选组合。 迁移 088 补一条 created_at 键、覆盖同一批窄列的索引,让规划器改为 Bitmap Index Scan 只取日期范围内的行。生产实测(真实 service 路径): 取 13,974 行 / 执行 75.7ms / 端到端 283ms(含列表查询),慢查询告警消失。 用"单次调用前后块差值"验证:51,521 块,与我 EXPLAIN 的 shared hit 完全一致。 与 087 的分工:无日期筛选走 id 键(index-only),有日期范围走本索引(bitmap)。 Bitmap Index Scan 无法 index-only,INCLUDE 的作用是让 heap 访问只落在日期范围内。 刻意不含 acceptance_json(勾 VIP 待补图时本就有意回表)。 验证:后端 541 项单测全绿(新增 1 项守卫)。
This commit is contained in:
@@ -2761,3 +2761,29 @@ test('三处"只差索引"的热查询缺口必须有迁移兜底(生产已前
|
||||
'迁移在事务内执行,不能使用 CONCURRENTLY',
|
||||
)
|
||||
})
|
||||
|
||||
test('统计的「状态+日期范围」组合必须有 created_at 键的覆盖索引', async () => {
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const migration = await readFile(
|
||||
new URL('../../db/migrations/088_work_order_statistics_date_cover_index.sql', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(
|
||||
/idx_work_orders_statistics_dates[\s\S]*?ON work_orders \(created_at\)/.test(migration),
|
||||
'缺少 created_at 键的覆盖索引:日期条件无法驱动 087 的 id 键索引,会退化成整表 Hash Join',
|
||||
)
|
||||
for (const column of [
|
||||
'status',
|
||||
'sharing_enabled',
|
||||
'sharing_total_quantity',
|
||||
'source_order_amount_fen',
|
||||
'reward_amount',
|
||||
'order_id',
|
||||
]) {
|
||||
assert.ok(migration.includes(column), `覆盖列缺少 ${column}`)
|
||||
}
|
||||
assert.ok(
|
||||
!/^\s*CREATE INDEX CONCURRENTLY/m.test(migration),
|
||||
'迁移在事务内执行,不能使用 CONCURRENTLY',
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user