结论先行:EXPLAIN 的 partition 列为空,不等于分区裁剪失效。在 8.0 走二级索引访问路径时,partition 列经常不标注,这是显示行为而非裁剪结果,别拿它下结论。你那个对照实验(去掉 merchant_id 就出 p7,p8,p9)恰恰说明裁剪本身是好的,问题出在"走 idx_merchant_time 时 EXPLAIN 不标"。
一、怎么区分「真没裁剪」vs「裁剪了但没标」——用两个权威判据,别信 EXPLAIN 的 partition 列:
EXPLAIN PARTITIONS SELECT ...(8.0 专用语法,单独输出裁剪到的分区集合,与访问方法解耦)。这是最稳定的判据:如果它列出 3 个分区,说明裁剪生效,EXPLAIN 的 partition 列空只是没标。- 看实际扫描行数,而非估算:
SELECT DIGEST_TEXT, SUM_ROWS_EXAMINED, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%t_order_flow%' ORDER BY SUM_ROWS_EXAMINED DESC;全表约 2.4e8;3 个月量级约 2.4e6。若 SUM_ROWS_EXAMINED≈2.4e6,裁剪生效;≈2.4e8 才是真没裁剪。 - 兜底:
SET profiling=1;跑一次看Rows examined,或SHOW SESSION STATUS LIKE 'Handler_read%'(read_rnd_next/read_key 的量级)。
二、根因(倒推):InnoDB 分区表的二级索引是 per-partition 独立 B-tree,不是全局一份。merchant_id=? 等值查找要扫「裁剪后分区数 × 该商户记录数」。裁剪到 3 个分区就只查 3 份索引;没裁剪就查 30 份——这正是你 P95 4~7s 的来源,而不是索引本身慢。
三、不改表结构、让裁剪稳定生效的写法(按优先级):
- 分区键谓词必须裸露,别被函数包(
DATE(created_at)、created_at+interval会废掉裁剪)。 - 用半开区间替代 BETWEEN:
created_at >= '2025-04-01' AND created_at < '2025-07-01',边界更稳、优化器更倾向把它当分区谓词而非索引第二列 range。 - 把分区键条件单独成 AND 项并放前面:
WHERE created_at >= ... AND created_at < ... AND merchant_id = ?,引导优化器先做分区裁剪再走索引。 - 排除绑定变量误导:先用常量复测一次(
merchant_id = 123),再换回?,对比EXPLAIN PARTITIONS是否一致——排除 prepared 语句下优化器用陈旧估算的问题。
如果做完 1–4,EXPLAIN PARTITIONS 仍显示全 30 分区、且 SUM_ROWS_EXAMINED≈2.4e8,那才确认优化器把 created_at 完全让给了索引第二列——此时不改索引的写法层面基本到顶,下一步就该回到索引设计(如 (merchant_id, created_at) 已是最优,可评估是否值得加一个以 created_at 打头的覆盖索引做 pruning + covering)。建议先把上面 2 的 ROWS_EXAMINED 数字贴出来,能立刻定位是哪一类。
补充一个运维侧的"兜底观测"经验:我们去年治理 12 张分区大表时,光靠 EXPLAIN 确实不够,最终在 APM 里给每张分区表加了一条"实际扫描行数/估算行数"比值的告警——比值超过 5 就触发"疑似裁剪失效"。具体做法是定时跑 EXPLAIN PARTITIONS 拿分区数,再取 performance_schema 的 SUM_ROWS_EXAMINED,两者相除写进 Prometheus(partition_pruning_ratio),配一条 >5 持续 3 个采集周期的 alert。好处是:开发改 SQL 没带分区键、或 prepared statement 下优化器用了陈旧统计信息,都能被自动发现,不用等 P95 飙到 4~7s 才反应过来。另外你提到的第 4 点"排除绑定变量误导"我们实际踩过:同一个 prepared 语句,第一次执行用常量估算,后续用 bind 参数,EXPLAIN PARTITIONS 结果居然不同——后来用 ANALYZE TABLE ... UPDATE HISTOGRAM ON created_at 刷新统计信息后稳定了。
从安全加固角度补一条,跟楼里"怎么判断裁剪生效"是正交的——"怎么判断"用的这两个诊断入口本身是敏感资产:
-
performance_schema.events_statements_summary_by_digest是业务数据分布的旁路信息源。SUM_ROWS_EXAMINED直接告诉你"某商户近 3 月订单量级",COUNT_STAR告诉你"这张表被谁查、查多频繁"。很多团队为"方便排查"默认把这个库对开发/测试角色开放,等于把"哪些商户活跃、哪些接口热"这两类敏感信息裸奔了。建议:digest 表按 role 收敛,非 DBA 只能看自己 schema 的;跨 schema 的查询走只读账号 + 审计日志。 -
EXPLAIN PARTITIONS/EXPLAIN在应用层暴露时(很多 APM 平台会把 explain 结果存下来),配合SHOW SESSION STATUS一起,可以反推表结构、索引设计、甚至数据分布。如果这些诊断输出进了日志文件、APM 面板、或错误上报系统,等于把"内网数据库拓扑 + 索引设计"外泄给了任何能看日志的人。建议:a) APM 侧对 explain 输出做脱敏(保留分区数,去掉具体表名/索引名);b) 日志里的Handler_read%这类状态快照,按级别过滤,P1/P2 日志别带全量 status。 -
楼里第 4 点"排除绑定变量误导"我实际踩过一个变体:prepared statement 下,同一个 SQL 模板因为 bind 参数不同,优化器选的路径可能不同(有的走分区裁剪,有的走全表)。这种情况下
EXPLAIN PARTITIONS的"常量复测 vs 参数复测"结果不一致是正常行为而不是 bug——审计时要把这个 case 单独标注,别让"复测不一致"被误报成"优化器异常"。
楼里"EXPLAIN 的 partition 列为空 ≠ 裁剪失效"这个结论非常关键,多少团队就是因为信了这一列,白改了一堆索引。
补充一个高并发场景下容易被忽略的性能坑:当分区裁剪生效、走 per-partition B-tree 时,merchant_id=? 等值查找要逐分区查 3 份独立索引——这 3 次 lookup 在单线程下是串行的,但如果并发 QPS 高,同一个商户的 N 个并发请求会同时去查那 3 份索引,InnoDB buffer pool 里的 index page 会因为并发 latch 竞争出现 row_lock 之外的 lock_sys 等待(不是行锁,是 page latch)。我们线上 2.4 亿行分区表实测过:单商户 P95 从 80ms 涨到 450ms 的根因不是裁剪失效,是 3 份 B-tree root page 的 lock_sys->wait_lock 排队。判据:SHOW ENGINE INNODB STATUS 里看 SEMAPHORES 段的 OS WAIT ARRAY,如果 spin waits 高但 os wait 低,大概率是 page latch 竞争而非行锁。解法:给热商户加一层应用层缓存(Redis + 本地 Caffeine 两级),把 3 份 B-tree 的并发 lookup 降到 1 次 DB 查询。