ANALYZE TABLE后查询仍慢因直方图未被优化器采用——仅当WHERE列无索引时启用;有索引则优先用索引统计,需用USE_HISTOGRAM提示强制触发,且低基数列效果差。

为什么 ANALYZE TABLE 后查询还是慢?直方图没生效
直方图不是收集完就自动起作用的——MySQL 8.0+ 默认只对 WHERE 条件中出现的列、且该列上没有索引时,才考虑用直方图估算行数。如果列上有索引,优化器大概率直接走索引统计信息,忽略直方图。
常见错误现象:EXPLAIN 显示 rows 估算严重偏高(比如实际 100 行,预估 50 万),但 SHOW CREATE TABLE 确认直方图已存在。
- 检查是否真被用上:执行
SELECT * FROM information_schema.COLUMN_STATISTICS WHERE TABLE_NAME = 'your_table' AND COLUMN_NAME = 'your_col';,确认HISTOGRAM字段非空且类型合理(如SINGLE_PRECISION或DOUBLE_PRECISION) - 强制触发直方图参与估算:在查询中加
/*+ USE_HISTOGRAM(your_col) */提示(MySQL 8.0.26+),验证是否改善 - 避免“伪热点”:直方图对低基数列(如状态字段只有 'pending'/'done')效果差,此时应优先建索引而非依赖直方图
WHERE 条件写成 IN 还是 = 更利于打散倾斜?
数据倾斜常出现在某几个值占了 90%+ 的记录,比如用户表里 tenant_id = 1 占 85%。这时单条 = 查询必然压到同一组分区或同一块数据页,而 IN 并不天然分散——如果 IN (1, 1, 1),照样全打在 tenant_id=1 上。
真正起作用的是「条件值分布」,不是语法形式。
- 用
IN前先查分布:SELECT tenant_id, COUNT(*) FROM users GROUP BY tenant_id ORDER BY COUNT(*) DESC LIMIT 5;,挑出高频值之外的几个低频值组合测试 - 慎用
IN大列表:MySQL 对IN列表长度超过 1000 项时可能退化为全表扫描,反而更慢 - 业务层可主动拆分:把一个查 10 万行的
tenant_id = 1,改成 10 个并发查tenant_id = 1 AND user_id BETWEEN ? AND ?,配合主键范围切分
分区表 + 直方图能缓解倾斜吗?
不能。MySQL 的分区是存储层面的物理切分,但查询计划仍由优化器统一生成。如果所有查询都命中同一个分区(比如按 tenant_id 分区,而查询总是 tenant_id = 1),那直方图再准也没用——瓶颈在单一分区的 I/O 和锁竞争。
分区本身不解决倾斜,还可能让问题更隐蔽:比如 EXPLAIN PARTITIONS 显示只扫一个分区,但这个分区太大,内部仍有严重倾斜。
- 分区键必须和查询条件强相关:若常查
created_at,就别用tenant_id分区 - 直方图需在每个分区单独收集(MySQL 不支持跨分区直方图),执行
ANALYZE TABLE t PARTITION(p1); - 更现实的替代:用
SHARDING(应用层分库)或物化视图预聚合高频值,而不是依赖分区+直方图组合
为什么 OPTIMIZER_USE_SQL_PLAN_BASELINES 开启后倾斜更严重?
SQL Plan Baseline 锁定了执行计划,包括访问路径、连接顺序、是否使用索引等。当数据分布变化(比如新租户涌入导致原“低频值”变高频),旧基线仍强行复用老计划,结果就是本该走索引的现在全表扫,本该用直方图估算的现在硬套旧统计值。
典型表现:某天凌晨批量导入后,原本 200ms 的查询突然飙到 12s,EXPLAIN 显示从 range 变成 ALL,且 key 列为空。
- 检查基线是否过期:
SELECT SQL_HANDLE, PLAN_NAME, ORIGIN, ENABLED, ACCEPTED FROM mysql.slow_log_plan;,重点关注ACCEPTED = 'NO'的计划 - 临时绕过基线:加
/*+ NO_PLAN_BASELINE */提示,看是否恢复 - 不要长期开启
OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES:它会无差别捕获所有慢查询,包括那些本就因倾斜而慢的语句,把坏计划也固化了
直方图和基线本质冲突:一个靠动态统计适应数据变化,一个靠静态计划拒绝变化。生产环境若用基线,就得接受定期人工校验+更新,不能指望它自动适配倾斜演变。











