LEFT JOIN 性能问题源于驱动表选择不当、索引缺失或逻辑误写;应选小表为驱动表、关联字段双向建索引、右表过滤条件移至ON子句、用EXISTS替代仅判断存在的LEFT JOIN,并通过EXPLAIN验证执行计划。

Left Join 本身不慢,慢的是没用对——关键在驱动表选择、索引覆盖和逻辑等价改写。
驱动表必须小,且优先走索引
MySQL 的 LEFT JOIN 默认以左表为驱动表。如果左表很大(比如 40 万行),右表很小(比如 1 万行),就会扫描 40 万 × 1 万行,接近笛卡尔积。这不是 left join 的错,是没干预执行计划。
- 加 WHERE 条件提前过滤左表,比如
WHERE c.status = 'active',能大幅缩小驱动结果集 - 确保连接字段(如
c.hotel_id = h.hotel_id)两边都有索引,尤其右表的关联列必须有索引,否则右表每次都要全表扫描 - 若业务允许,可考虑把“小表”放左边——例如想查「有余额的客户」,但余额表更小,就让
customer_balances做左表,customer做右表,再用LEFT JOIN ... ON配合WHERE cb.c_id IS NOT NULL等效实现
用 EXISTS 或子查询替代部分 LEFT JOIN
当只需判断右表是否存在匹配记录(而非取右表字段),EXISTS 通常比 LEFT JOIN + IS NULL 更快,因为它找到第一条就停,不生成中间空值行。
- 原写法:
SELECT c.* FROM customer c LEFT JOIN orders o ON c.id = o.customer_id WHERE o.id IS NULL - 优化写法:
SELECT c.* FROM customer c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) - 若需取右表某一条最新记录(如最新订单时间),可用相关子查询:
(SELECT created_at FROM orders o WHERE o.customer_id = c.id ORDER BY created_at DESC LIMIT 1),避免 GROUP BY 和全量 JOIN
复合索引要按最左前缀+选择性排序
连接条件含多个字段时(如 ON c.type = h.type AND c.id = h.id),右表必须建复合索引,且顺序要匹配查询中字段的使用逻辑。
- 推荐索引:
INDEX (hotel_type, hotel_id),不是(hotel_id, hotel_type)—— 因为 type 选择性通常更高,放在前面能更快过滤 - 若查询还带
WHERE h.status = 'valid',可扩展为INDEX (status, hotel_type, hotel_id),让 WHERE + JOIN 一起走索引 - 检查是否命中:用
EXPLAIN看key列是否显示该索引,rows是否明显下降
警惕 WHERE 中误用右表字段
LEFT JOIN 后,若在 WHERE 里写 h.status = 'valid',MySQL 会先 JOIN 再过滤,等于把 LEFT JOIN 变成 INNER JOIN,还失去左表保全优势;更糟的是,可能因右表无索引导致全表扫描。
- 正确做法:把右表的过滤条件移到
ON子句中,如ON c.id = h.id AND h.status = 'valid' - 这样既能保留左表所有行,又让右表在 JOIN 时就完成筛选,减少参与连接的数据量
- 如果确实需要 WHERE 判断右表为空(如找未匹配记录),只用
h.id IS NULL是安全的,它不会改变语义,也不会阻止索引使用











