大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
有没有遇到过这种情况:一条SQL跑了半年都好好的,突然某天开始变慢了。SQL没改,索引没动,数据量也没暴涨,但执行计划从索引扫描变成了全表扫描。
你更新了统计信息,重建了索引,重启了数据库——执行计划还是那个错的。
优化器到底在想什么?当它选错的时候,除了干瞪眼,还能怎么办?
今天把优化器选错执行计划的原因、Hint的语法体系和实战用法、以及比Hint更好的替代方案,一次性讲清楚。
先搞懂几个词:
CBO(Cost-Based Optimizer) :基于代价的优化器。MySQL的优化器根据统计信息估算不同执行计划的代价,选择代价最低的方案。
统计信息:表和索引的元数据,包括行数、基数、数据分布等。优化器做决策的核心依据。
基数估算:优化器对某个条件过滤后剩余行数的估算值。基数估算偏差是执行计划选错的常见原因。
Optimizer Hint:写在SQL注释中的优化器指令,强制优化器使用指定的索引、连接顺序或执行策略。
一、优化器为什么会选错执行计划
优化器选错执行计划,通常有三个原因。
原因一:统计信息过期。 表的数据量从100万涨到了1000万,但统计信息还停留在三个月前。优化器以为表只有100万行,选了一个“看起来很快”的执行计划,实际跑起来慢得离谱。
原因二:数据分布倾斜。 优化器用采样估算基数,如果数据分布不均匀,采样结果可能严重偏离实际。比如status='DONE'占全表90%,但采样时恰好抽到了几个status='PENDING'的行,优化器以为status='DONE'选择性很高,选了索引扫描,实际需要扫描90%的行。
原因三:代价模型的局限。 MySQL的代价模型基于固定的成本常数估算磁盘I/O和CPU开销。这些常数是静态的,但真实系统的负载是动态的。当并发上来、缓存命中率下降、磁盘I/O压力增大,优化器认为“便宜”的方案实际执行起来可能贵得离谱。
二、MySQL 8.0的Hint语法体系
MySQL的Hint写在SQL语句的/* */注释中,格式是/*+ hint_name(args) */。它紧跟在SELECT、UPDATE、DELETE等关键字之后。
SELECT /*+ INDEX(orders idx_create_time) */
id, amount, create_time
FROM orders
WHERE create_time >= '2026-01-01';
MySQL 8.0支持的Hint分为几大类:
| 类别 | 常用Hint | 作用 |
|---|---|---|
| 索引选择 | INDEX、NO_INDEX、FORCE INDEX | 强制或禁止使用某个索引 |
| 连接顺序 | JOIN_ORDER、JOIN_FIXED_ORDER | 控制多表连接的驱动表顺序 |
| 连接算法 | HASH_JOIN、NO_HASH_JOIN、BNL、NO_BNL | 控制JOIN算法 |
| 查询重写 | SEMIJOIN、NO_SEMIJOIN、MERGE、NO_MERGE | 控制子查询和派生表的重写策略 |
| 并行执行 | PARALLEL | 控制并行查询线程数 |
三、五类常用Hint实战
第一类:索引选择——INDEX / NO_INDEX / FORCE INDEX
-- 强制使用 idx_create_time
SELECT /*+ INDEX(orders idx_create_time) */ * FROM orders
WHERE create_time >= '2026-01-01';
-- 禁止使用 idx_status
SELECT /*+ NO_INDEX(orders idx_status) */ * FROM orders
WHERE status = 'PAID';
-- 强制走索引(类似FORCE INDEX,但语法更灵活)
SELECT /*+ FORCE INDEX(orders idx_user_id) */ * FROM orders
WHERE user_id = 12345;
适用场景:优化器选错了索引,你想强制它走某个索引。
第二类:连接顺序——JOIN_ORDER / JOIN_FIXED_ORDER
-- 强制以 orders 为驱动表,users 为被驱动表
SELECT /*+ JOIN_ORDER(orders, users) */
o.id, u.username
FROM orders o
JOIN users u ON o.user_id = u.id;
适用场景:优化器选错了驱动表,导致被驱动表全表扫描。你明确知道某张表过滤后结果集更小,应该作为驱动表。
第三类:连接算法——HASH_JOIN / NO_HASH_JOIN
-- 强制使用 HASH JOIN
SELECT /*+ HASH_JOIN(orders, users) */
o.id, u.username
FROM orders o
JOIN users u ON o.user_id = u.id;
-- 禁止 HASH JOIN,回退到 Nested Loop
SELECT /*+ NO_HASH_JOIN(orders, users) */
o.id, u.username
FROM orders o
JOIN users u ON o.user_id = u.id;
适用场景:MySQL 8.0.18+ 引入Hash Join后,优化器可能在某些场景下选错算法。用Hint强制切换。
第四类:查询重写——SEMIJOIN / NO_SEMIJOIN / MERGE / NO_MERGE
-- 禁止半连接优化,保持子查询语义
SELECT /*+ NO_SEMIJOIN() */ * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE vip = 1);
-- 强制合并派生表,不物化
SELECT /*+ MERGE(derived_table) */ * FROM (
SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id
) AS derived_table WHERE total > 1000;
适用场景:子查询被优化器重写成了半连接,但重写后的执行计划反而更差。或者派生表被物化了,但你希望它被合并到外层查询中。
第五类:并行执行——PARALLEL
-- 强制使用4个并行线程
SELECT /*+ PARALLEL(4) */ COUNT(*) FROM orders;
适用场景:大表扫描类查询,单线程跑得太慢,想用并行查询加速。
四、Hint的局限与风险
Hint不是万能药。它有明确的局限:
局限一:版本升级后Hint可能失效。 MySQL 8.0的Hint语法和5.7不同。5.7的/*+ INDEX */在8.0中仍然可用,但部分Hint的行为可能变化。
局限二:Hint写错了不会报错,只会静默忽略。 如果表名或索引名写错了,优化器会忽略这个Hint,不会给你任何提示。SQL照常执行,只是Hint没生效。
局限三:Hint可能产生反效果。 你强制优化器走某个索引,但数据分布已经变了,这个索引不再是最优选择。Hint会把优化器锁死在一个过时的决策上。
局限四:Hint是硬编码的。 同一个SQL模板,在不同数据量、不同参数下,最优执行计划可能完全不同。用Hint固定一个执行计划,意味着其他场景下可能变慢。
一个真实案例:某团队用Hint强制一条SQL走索引A,上线后前三个月性能很好。第四个月业务数据分布变化,索引A的选择性大幅下降,但Hint还在强制走索引A。查询性能从50ms降到了2s,排查了三天才发现是Hint的问题。
五、比Hint更好的选择
在写Hint之前,先试试这些方案:
方案一:更新统计信息。 ANALYZE TABLE更新表的统计信息。80%的执行计划选错问题,更新统计信息就能解决。
方案二:优化索引设计。 优化器选错索引,可能是因为缺少一个更合适的索引。建一个更匹配查询模式的联合索引,比强制优化器走某个索引更有效。
方案三:改写SQL。 把子查询改成JOIN、把OR改成UNION ALL、把SELECT *改成只查必要字段——这些改写可能让优化器自然选到更好的执行计划。
方案四:用EXPLAIN ANALYZE诊断。 MySQL 8.0.18+的EXPLAIN ANALYZE直接输出实际执行行数和时间。对比估算值和实际值,能精确定位优化器在哪个步骤判断失误。
六、小结
Hint是优化器失控时的“紧急刹车”,不是日常调优的常规武器。优化器选错执行计划的原因通常是统计信息过期、数据分布倾斜或代价模型局限。先用ANALYZE TABLE更新统计信息,再检查索引设计是否合理,最后才考虑用Hint。如果必须用Hint,记住它的局限:版本升级可能失效、写错不会报错、可能产生反效果。Hint应该是临时方案,不是永久配置。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~