已采纳
回答正文
回复
讨论正文
该问题的正文与回答。
我负责一套 MySQL 8.0.36 / InnoDB 多租户核心库(单主 32C128G,innodb_buffer_pool_size=96G),核心表 t_order_ext 约 3.2 亿行,头部租户单租户占全表 41% 的行。最近一个月慢日志量从日均 200 条涨到 1500+ 条,用 performance_schema 的 digest 聚合 + PLAN 字段(8.0.30+ 支持)跑下来,确认其中约 40 条 SQL 是同一 digest 在不同租户参数下执行计划不一致:头部租户走 idx_tenant_status,长尾租户被优化器判成全表扫描。这属于计划漂移(plan drift),不是新增慢 SQL。
社区里已经有文章讲清了漂移的检测(如《同一索引同一 SQL,凌晨 P99 从 15ms 飙到 3.4s:统计信息漂移的根因与修复》)和单条 SQL 用 FORCE INDEX 压住(《复合索引选择性阈值》里那位同学验证了头部租户 FORCE INDEX 后 P99 从 2.8s 降到 180ms)。我想问的是检测之后的治理动作本身:40 条漂移 SQL,线上怎么系统化地固化计划,而不是逐条手改 SQL 发版?
SELECT ... FROM t_order_ext WHERE tenant_id=? AND status=? ORDER BY gmt_create DESC LIMIT 20,头部租户(3100 万行)走 idx_tenant_status,P99 约 200ms;type=ALL,rows 估算 3.2 亿,P99 2.4s+;ANALYZE TABLE 后短暂恢复(头部租户反而切回全表扫),2–4 小时后再次漂移——统计信息刷新本身成了漂移源。ANALYZE TABLE t_order_ext + UPDATE HISTOGRAM:执行后头部租户 EXPLAIN 立刻变 ALL,2–4 小时后漂移方向再次反转——说明统计信息本身在数据持续写入下就不稳定,靠它不可靠;FORCE INDEX (idx_tenant_status):对头部租户有效(P99 2.8s → 180ms),但 40 条 SQL 分散在 3 个服务 7 个 DAO 方法里,手改发版意味着 3 次发布窗口,风险高;idx_tenant_status_gmt (tenant_id, status, gmt_create):头部租户稳定走该索引,长尾租户优化器反而更坚定地选全表扫(因为覆盖索引的 index scan 在长尾租户上确实更贵)——加了索引漂移没消失,只是换了方向;SET optimizer_switch='index_merge=off'(会话级):无效,这条 SQL 不涉及 index_merge;optimizer_trace 对比:确认头部/长尾租户的 cost 差在 rows_examined 估算上,头部租户 idx_tenant_status 估算 rows=3100 万,全表估算 rows=3.2 亿,但优化器认为 index scan 回表 3100 万次的 cost 高于全表扫——这是 cost model 对「LIMIT 20 + ORDER BY」场景的已知偏差。/*+ */)/ 加索引 / 改写 SQL(拆成两条:头部租户走索引、长尾走全表)四条路,各自的适用边界、线上风险、回滚成本——最好有真实案例的数据支撑,而不是「看情况」;ANALYZE TABLE 本身成了漂移源,线上有没有人把 ANALYZE 做成定时任务(比如每天凌晨低峰跑一次)并配合 innodb_stats_auto_recalc=OFF 来压住统计信息的自动刷新?效果如何?参考:《同一索引同一 SQL,凌晨 P99 从 15ms 飙到 3.4s:统计信息漂移的根因与修复》(id=208)讲清了机制,但没讲「40 条怎么批量治」;《复合索引选择性阈值》(id=1713)里那位同学验证了 FORCE INDEX 有效,但没讲批量治理与回滚。