已采纳
回答正文
回复
讨论正文
该问题的正文与回答。
8 年 DBA,慢查询治理日常。最近接手一套结算系统(MySQL 8.0.34 / InnoDB,单主 16C64G,buffer_pool 40G),核心表 t_settle_detail 约 5200 万行、单表 38GB,写 QPS 约 1200(INSERT + 状态 UPDATE 为主)。
一条 P99 要求 20ms 的高频点查:
SELECT id, settle_no, amount, status, created_at
FROM t_settle_detail
WHERE merchant_id = ? AND status = 'SUCCESS' AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 20;
对应联合索引 idx_merchant_status_time (merchant_id, status, created_at),覆盖了全部 SELECT 列——当初设计就是覆盖索引,上线时 P99 约 8ms。
最近 3 个月,随着软删(deleted_at IS NULL 过滤)比例从 ~5% 涨到 ~28%:
EXPLAIN FORMAT=TREE 显示走了 idx_merchant_status_time,但 rows 估算 3.1 万,实际回表过滤后只剩 7800 行有效——索引扫描路径本身没问题,问题是扫描的 3.1 万里近一半是已删除行,白扫。EXPLAIN 显示优化器直接放弃索引,选了全表扫 + 过滤,cost 估算 4.2M vs 索引路径 cost 估算 86K——但实际跑下来全表扫 P99 是 6.8s,索引路径(虽然要扫 41 万行)只要 340ms。优化器 cost 和真实耗时差了 20 倍,我基本判断是 InnoDB 的 index dive 只统计了二级索引叶子页的 page count,没把「逐行回表过滤 deleted_at」的随机 I/O 成本算进去。ANALYZE TABLE 后重跑 EXPLAIN,统计信息刷新了但 cost 估算没有本质变化——说明不是统计信息过期,而是 cost model 本身不含回表过滤成本。FORCE INDEX(idx_merchant_status_time) 实测 P99 340ms,确认「索引路径其实是对的,是优化器选错了」。deleted_at 挪进联合索引变成 (merchant_id, status, deleted_at, created_at),但 deleted_at IS NULL 放第三位会破坏 created_at 的范围/排序利用,ORDER BY 大概率退化成 filesort——索引结构怎么摆,两种方案各有硬伤。ALTER TABLE ... FORCE 重建索引把软删行物理挤出去(类似 PG 的 VACUUM 思路),但 5200 万行 Online DDL 的窗口和业务方风险还没评估。我不要「建议 FORCE INDEX」或「建议重建索引」的二选一,我要的是:
EXPLAIN ANALYZE(8.0.18+)对比过 cost 里 index dive 各项的构成,能拆出「叶子页扫描 cost」和「回表 cost」各占多少?EXPLAIN FORMAT=JSON 的 cost 字段,画出「命中行占比 vs 优化器选择」的拐点曲线?我需要一个别人能照着跑一遍的脚本/步骤,而不是一段文字描述。deleted_at 挪进索引(牺牲排序)、定期重建索引(写放大 + DDL 窗口)、改 SQL 去掉 ORDER BY 改应用层排序(内存代价),这三条路在「软删比例会持续增长」这个前提下,各自的失效时间点大概在哪?有没有人做过其中一条的线上 A/B 数据?