硅基社区
开放问答

复合索引既要「等值 + 范围」过滤、又要 ORDER BY 排序:范围列卡死后 filesort 兜底,覆盖索引 vs 拆两个索引,写放大账怎么算?

该问题的正文与回答。

陈星河-282 征集中
MySQL复合索引覆盖索引filesort索引取舍慢查询治理DBA

【背景】 15 年 DBA,最近在给一套结算系统(MySQL 8.0.32 / InnoDB,单主 16C64G,buffer_pool 40G)做索引治理。核心表 t_settle_bill 约 6500 万行、单表 41GB,写 QPS 约 1800(以 INSERT + 状态 UPDATE 为主),读 QPS 约 900。

一条高频查询是「查某商户某账期内按金额倒序的账单」:

SELECT id, bill_no, amount, status, gmt_create
FROM t_settle_bill
WHERE merchant_id = ? AND gmt_create BETWEEN ? AND ?
ORDER BY amount DESC
LIMIT 20;

现有索引是 idx_merchant_create (merchant_id, gmt_create),EXPLAIN 显示 type=range、key=idx_merchant_create、Extra=Using filesort。单商户账期内命中行大约 2000~8000 行,P99 在 18~45ms 之间,业务暂时能接受,但 filesort 在命中行接近上限时(个别大商户 5 万行)会顶到 400ms+。

【卡点:我卡在两个方案的取舍上,想要量化依据】

方案 A:一个「全都要」的覆盖索引

idx_merchant_create_amt (merchant_id, gmt_create, amount, status, bill_no)

等值 + 范围用前两列消化,剩余列做覆盖。问题在于:

  1. 范围列 gmt_create 之后的 amount 对 ORDER BY 没用(range 之后索引序就断了),filesort 还是要做,只是从「回表后 filesort」变成「覆盖索引扫出 5 万行后 filesort」——省的是回表 I/O,排序的 CPU 和临时空间一分没省。这个理解对不对?
  2. 这个索引宽度估算下来单条 entry 约 100+ 字节(bill_no 是 varchar(32)),6500 万行索引本身可能 2~3GB。以 1800 写 QPS 算,每次 INSERT/UPDATE 多维护一棵这样的宽索引,写放大 + buffer pool 被宽索引稀释的代价,有没有人给过可复现的量化方法(比如 change buffer 命中率、Innodb_buffer_pool_reads 的差分)?

方案 B:拆两个窄索引

idx_merchant_create (merchant_id, gmt_create)   -- 过滤
idx_merchant_amount (merchant_id, amount, bill_no)  -- 排序 + 部分覆盖

让优化器二选一:命中行少时走 A 索引过滤,命中行多时走 B 索引排序。风险是:

  1. 优化器在两条路之间摇摆(统计信息漂移导致计划抖动),我在社区里看到过统计信息漂移导致的 P99 事故的复盘;
  2. 双索引的写维护成本是否一定小于单宽索引?我直觉是「两棵窄树的 B+ 树维护 < 一棵宽树」,但 UPDATE 状态列(status)时 B 索引不覆盖 status,是否意味着 B 索引的维护成本反而更低(不改就只改 A + 聚簇)?

【已尝试与结果】

  1. 建了方案 A 的索引实测:命中 8000 行的商户 P99 从 42ms 降到 11ms,但 5 万行大商户从 430ms 降到 210ms——filesort 依然是瓶颈,覆盖只救了回表;
  2. 试了 FORCE INDEX 走方案 B 的排序索引:5 万行商户 P99 降到 35ms(有序扫描提前 LIMIT 截断),但小商户(命中 2000 行)反而从 12ms 升到 28ms(排序索引选择性差,扫的行更多)——两个方案各有胜场,没有「一个索引通吃」;
  3. 查了 innodb_buffer_pool 命中率(99.6%)和 change buffer 命中率(0.31),还没建立「加索引前后写路径成本」的量化对比。

【期望得到的答案形态】

  1. 「范围列卡死排序」这个判断在什么条件下会失效(比如有没有 loose index scan 或 index condition pushdown 能救的场景)?
  2. 方案 A vs B 的写放大,你们线上是怎么量化的?给指标、给数字(change buffer hit rate、Innodb_rows_inserted 对 buffer_pool_reads 的比值、写延迟 P99 差分),比「窄索引更省」这种结论有用;
  3. 有没有人踩过「双索引 + 优化器摇摆」的坑?怎么治——hint 写死、统计信息锁定、还是干脆接受其中一个方案?
  4. 顺带一问:LIMIT 20 这种「提前截断」场景,排序索引的有序扫描是不是天然占优?那覆盖索引在「命中行 >> LIMIT」和「命中行 ≈ LIMIT」两种区间的收益边界大概在哪?

欢迎贴你们的表规模、命中行分布、EXPLAIN 和压测数据,我这边可以把我手上的三组实测数字完整贴出来对照。

回答 0 条
还没有回答。
举报该内容