一、五种 JOIN 的集合含义
| 类型 | 结果 |
|---|---|
INNER JOIN | 两边都匹配上的行 |
LEFT JOIN | 左表全部 + 右表匹配的(无匹配补 NULL) |
RIGHT JOIN | 右表全部 + 左表匹配的 |
FULL JOIN | 两边全部(MySQL 不直接支持,用 UNION 模拟) |
CROSS JOIN | 笛卡尔积,行数 = 左 × 右 |
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
结果包含「没有订单的用户」,o.amount 为 NULL。
二、ON 与 WHERE 的关键区别
这是最容易出错的地方:
-- A:条件在 ON 里 → 左表全部保留,右表只匹配 status=1 的
SELECT * FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 1;
-- B:条件在 WHERE 里 → 先连接,再过滤,没订单的行被 NULL 判断筛掉
SELECT * FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 1;
B 的结果里没有「没有订单的用户」,因为 NULL = 1 不成立。想让左表全部保留,右表的过滤条件必须写在 ON 里。
反过来,如果确实想「只要有订单的用户」,那 B 是对的——但这时用 INNER JOIN 表达意图更清楚。
三、判断 NULL 的坑
-- 错误:NULL = NULL 不成立,永远查不到
WHERE o.status = NULL
-- 正确
WHERE o.status IS NULL
左连接后想筛「没有匹配的」,标准写法是:
SELECT u.* FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
四、驱动表与索引
MySQL 的 JOIN 是「嵌套循环」:拿驱动表的每一行,去被驱动表里找匹配。所以:
- 驱动表要小,被驱动表的关联字段必须有索引。
- 没有索引时,每次匹配都要全表扫,复杂度变成 左行数 × 右行数。
-- 确保 o.user_id 有索引
ALTER TABLE orders ADD INDEX idx_user (user_id);
EXPLAIN 里第一行通常是驱动表;出现 Using join buffer 说明被驱动表没走索引。
五、笛卡尔积事故
漏写 ON 条件:
SELECT * FROM users, orders; -- 10 万 × 100 万 = 1000 亿行
查询会直接把数据库拖死。防护:
- 用显式
JOIN ... ON语法,不用逗号分隔。 - 多表关联时逐条检查
ON。 - 开发环境限制
max_execution_time。
六、多表关联的书写顺序
建议按「主表 → 维度表 → 明细表」的顺序:
SELECT u.name, o.id, i.product_name, i.qty
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items i ON i.order_id = o.id
WHERE o.created_at >= '2026-11-01'
ON 紧跟各自的 JOIN,不要把所有条件堆在最后。这样 review 时容易发现漏条件。
七、性能优化要点
- 只取需要的列,别
SELECT *,尤其带TEXT的表。 - 过滤条件尽量下推到驱动表,减少参与连接的行数。
- 避免在关联字段上做函数或类型转换(
ON a.id = CAST(b.uid AS CHAR)会失效)。 - 大表关联考虑反范式:把常用字段冗余一份,省一次 JOIN。
- 分页时先筛 ID 再关联,避免「关联完再 LIMIT」扫大量数据。
-- 先取一页 ID,再回表关联
SELECT u.name, o.amount
FROM (SELECT id, user_id, amount FROM orders ORDER BY id DESC LIMIT 20) o
JOIN users u ON u.id = o.user_id;
小结
LEFT JOIN 想保留左表全部,右表条件写 ON 而非 WHERE;判断空用 IS NULL;被驱动表的关联字段必须有索引;别漏 ON,笛卡尔积能直接把库打挂。