告警:一条 JOIN 把从库 CPU 拉满
七月五号,DBA 在群里 @我:「你那条报表 SQL 把从库 CPU 干到 100%,跑了 40 秒还没出」。这是一条订单表(8000 万行)和用户表(2000 万行)的关联查询,原本是凌晨跑的批,被临时拉到白天查。我拿 EXPLAIN 一看,问题很清楚。
排查:驱动表选反了
原始 SQL 大致是:
SELECT o.order_id, u.user_name, o.amount
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.dt BETWEEN '2023-07-01' AND '2023-07-05'
AND u.city = '上海';
EXPLAIN 显示 users 成了驱动表,type=ALL 全表扫描 2000 万行,再对每行去 orders 找匹配。优化器以为 city='上海' 过滤性强,但关联键没用上索引,结果反而是大表被反复扫。
驱动表选择:小表驱动大表
NLJ(Nested Loop Join)的原则是「用小结果集驱动大结果集」。这里应该先按 dt 把 orders 缩到 5 天的小集合(约 1200 万),再用 user_id 索引去 users 查。关键是给关联键和过滤条件都建索引:
ALTER TABLE orders ADD INDEX idx_dt_user (dt, user_id);
ALTER TABLE users ADD INDEX idx_user_city (user_id, city);
改完后 users 走 idx_user_city 的 ref 访问,驱动关系纠正,耗时从 40 秒降到 6 秒。
BNL 与 Hash Join:大表对大表怎么办
如果两张都是大表、且 join 列没索引,MySQL 8.0 的优化器可能选 BNL(Block Nested Loop)——把驱动表分批读进 join buffer 再匹配。问题是 buffer 装不下时会多次扫描被驱动表,慢得离谱。我们那次就是 buffer 不够导致反复扫。
MySQL 8.0.18 之后引入了真正的 Hash Join,对大表等值关联友好得多:
EXPLAIN FORMAT=tree
SELECT ... FROM orders o JOIN users u ON o.user_id = u.user_id;
-- 输出里出现 "Hash join" 即生效
Hash Join 把小表建哈希表、大表流式探测,比 BNL 少很多次扫描。但它依赖优化器判断,join buffer 大小(join_buffer_size,默认 256KB)和统计信息准确性都影响选型。我们把它调到 4MB 后,Hash Join 稳定生效。
索引优化的收尾
- 关联键必须有索引,否则必退化为 BNL/全扫。
- 过滤条件放驱动表的索引前缀,先缩小驱动集。
- 用
EXPLAIN FORMAT=tree确认实际用了哪种 join 算法,别只信rows估算。
先到这
《一次大表 JOIN 的性能优化》这块我前前后后踩了不止一次。今天先写这些,后面想到新的再补。