JOIN 查询优化策略
JOIN 的性能瓶颈
JOIN 是关系数据库最核心的特性,但也是性能问题的常见来源。理解 JOIN 的执行机制是优化的前提。
-- 常见的 JOIN 性能问题
SELECT * FROM A JOIN B ON A.id = B.a_id WHERE A.status = 1;
核心优化原则
原则一:小表驱动大表
MySQL 的 Nested Loop Join 是”外表驱动内表”:
执行过程:
1. 遍历外表(驱动表)的每一行
2. 对内表的每一行,查找匹配条件
3. 因此:外表扫描次数 × 内表查找成本 = 总成本
最优原则:数据量小的表作为驱动表,数据量大的表作为被驱动表。
-- 假设:order 1 万行,order_item 10 万行
-- 推荐:小表驱动大表
SELECT * FROM order o
JOIN order_item oi ON o.id = oi.order_id
WHERE o.user_id = 123;
-- MySQL 优化器通常会将 order 作为驱动表(数据少)
-- 但如果不写 WHERE 条件,可能会选错驱动表
SELECT * FROM order o
JOIN order_item oi ON o.id = oi.order_id;
-- 优化器会分析表大小,选择小表驱动
原则二:JOIN 字段必须有索引
这是 JOIN 优化中最重要的一条原则:
-- 高效 JOIN:被驱动表的关联字段有索引
-- 假设 order_item 的 order_id 有索引
SELECT * FROM order o
JOIN order_item oi ON o.id = oi.order_id;
-- 执行计划:o (驱动表,顺序扫描) → oi (被驱动表,索引查找)
-- 复杂度:O(N) + O(M * log N)→ 实际上接近 O(N) × O(log M)
-- 低效 JOIN:被驱动表的关联字段没有索引
-- 执行计划:全表扫描 + 全表扫描
-- 复杂度:O(N × M)→ 灾难
一定要检查:EXPLAIN 中,被驱动表的 type 是否为 ref/eq_ref。
原则三:尽量减少返回的行数
-- 先过滤,再 JOIN
SELECT * FROM (
SELECT * FROM order WHERE status = 1 LIMIT 100
) o
JOIN order_item oi ON o.id = oi.order_id;
-- 而不是
SELECT * FROM order o
JOIN order_item oi ON o.id = oi.order_id
WHERE o.status = 1;
-- 第一个版本:先过滤 order 表为 100 行,再 JOIN
-- 第二个版本:先 JOIN 所有数据,再过滤
原则四:使用 Small Result 提示
如果你确定驱动表很小,可以给优化器提示:
SELECT STRAIGHT_JOIN o.*, oi.*
FROM order o STRAIGHT_JOIN order_item oi ON o.id = oi.order_id;
-- STRAIGHT_JOIN 强制使用左表作为驱动表
不同 JOIN 类型的优化
INNER JOIN
-- 优化器可以自由选择驱动表
-- 自动选择数据量较小的表作为驱动表
-- 优化重点:确保被驱动表的 JOIN 字段有索引
INNER JOIN order_item oi ON o.id = oi.order_id
-- order_item.order_id 必须建索引
LEFT JOIN
-- 左表固定为驱动表,无法互换
-- 因此:驱动表应尽量小
-- 如果左表很大,考虑是否可以改写为 INNER JOIN
SELECT * FROM user u LEFT JOIN order o ON u.id = o.user_id
WHERE o.status = 1;
-- 这里 WHERE 条件是右表的,实际效果等同于 INNER JOIN
-- 用 INNER JOIN 让优化器选择驱动表
多表 JOIN 的顺序
-- 多表 JOIN 按小→大排序
FROM small_table s
JOIN medium_table m ON s.id = m.s_id
JOIN large_table l ON m.id = l.m_id
索引设计策略
联合索引匹配 JOIN + WHERE
-- 常见查询
SELECT * FROM order o
JOIN user u ON o.user_id = u.id
WHERE o.status = 1 AND o.create_time > '2024-01-01'
ORDER BY o.create_time DESC;
-- 推荐索引
-- order 表:idx_status_time(status, create_time, user_id)
-- user 表:id 主键索引足够
关键思是:让 ORDER BY 走索引,避免 Using filesort。
用 EXISTS 替代 JOIN
在某些场景下,EXISTS 比 JOIN 更高效:
-- 查询有订单的用户
-- JOIN 方式
SELECT DISTINCT u.* FROM user u
JOIN order o ON u.id = o.user_id;
-- EXISTS 方式
SELECT * FROM user u
WHERE EXISTS (
SELECT 1 FROM order o WHERE o.user_id = u.id
);
-- EXISTS 在子查询找到第一条匹配后就会停止
-- 适合"判断存在性"的场景
反范式化:用空间换时间
如果 JOIN 查询依然很慢,而且查询是只读的,可以考虑反范式化:
-- 将用户姓名冗余到订单表
ALTER TABLE order ADD COLUMN user_name VARCHAR(50);
-- 写入时设置,读取时可以直接 SELECT 无需 JOIN
面试要点
- JOIN 优化的核心:被驱动表 JOIN 字段必须建索引
- 小表驱动大表是 Nested Loop Join 的本质要求
- 先过滤再 JOIN,减少参与 JOIN 的数据量
- STRAIGHT_JOIN 可以强制驱动表顺序
- EXPLAIN 中 type = ALL 是 JOIN 索引缺失的典型信号
- 能用 EXISTS 替代不要用 JOIN(存在性判断场景)
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容