LEFT JOIN慢的根源在于左表全表扫描和右表关联低效,优化需聚焦执行计划分析、索引匹配、子查询切片、右表建索引及游标分页替代深分页。

LEFT JOIN慢,先看执行计划里哪张表在全表扫描
直接跑 EXPLAIN,重点盯住 type 列:如果出现 ALL,说明这张表没走索引,正在硬扫。尤其注意左表(orders、question_tasks 这类主业务表)——它被强制全量读取,一旦没索引或过滤条件写在 WHERE 里而不是提前下推,1000万行就真得扫完再关联。
常见错误现象:rows 值接近表总行数,Extra 里带 Using temporary; Using filesort;或者右表的 type=ALL,但左表其实已加了索引——这往往是因为关联字段类型/字符集不一致,导致索引失效。
- 检查
ON字段两边的数据类型、长度、字符集是否完全一致(比如utf8mb4_general_ci和utf8mb4_unicode_ci视为不匹配) -
WHERE中对左表的过滤条件,必须能命中索引;若用到函数(如DATE(created_at))、!=、OR或前导模糊匹配(LIKE '%abc'),索引大概率失效 - 联合索引要符合最左前缀原则;
ORDER BY和LIMIT涉及的字段,最好包含在索引中(覆盖索引可避免回表)
千万级左表不能直接JOIN,得先“切片”再关联
当左表本身超千万、且 WHERE 条件又无法大幅过滤时,硬 JOIN 就是灾难。优化核心是:不让数据库先读完整个左表,而是先用子查询把需要的 ID 拿出来,再用这些 ID 去精确关联。
比如原 SQL 是:
SELECT o.order_id, p.product_name FROM orders o LEFT JOIN products p ON o.product_id = p.id WHERE o.status = 2 AND o.created_at > '2023-01-01' ORDER BY o.id DESC LIMIT 20;
应改写为:
SELECT o.*, p.product_name FROM (SELECT id FROM orders WHERE status = 2 AND created_at > '2023-01-01' ORDER BY id DESC LIMIT 20) t INNER JOIN orders o ON t.id = o.id LEFT JOIN products p ON o.product_id = p.id;
- 子查询
t只返回 20 个id,rows从千万级降到个位数 - 外层
INNER JOIN orders是主键等值匹配,type=eq_ref,极快 - 注意:子查询里不能有
SELECT *,否则优化器可能放弃物化;也别用IN (子查询),MySQL 5.7+ 对IN子查询优化有限,容易退化成循环执行
右表关联字段没索引?立刻建,别犹豫
LEFT JOIN 的右表(products、question_task_audit 等)虽不强制保留全部数据,但若关联字段(如 product_id、taskid)没索引,数据库只能对每一行左表数据都做一次全表扫描右表——1000万 × 右表行数,指数爆炸。
建索引不是越多越好,但关键字段必须有:
- 右表的
ON字段单独建索引,例如CREATE INDEX idx_product_id ON products(id); - 若
ON同时带额外条件(如ON t.id = a.taskid AND a.auditindex = 1),考虑建联合索引(taskid, auditindex),顺序不能错 - 确认索引真正生效:
EXPLAIN里key列显示该索引名,key_len匹配预期长度
LIMIT 分页偏移大时,LEFT JOIN 必须重写逻辑
当 LIMIT 800000, 20 这种深分页出现,MySQL 实际会读取 800020 行再丢弃前 800000 行——LEFT JOIN 下这个代价被放大数倍。此时不能依赖 OFFSET,得用基于主键/时间戳的游标分页。
例如按 created_at 排序分页,上一页最后一条是 '2023-06-15 14:22:03',下一页就写:
SELECT o.*, p.product_name FROM orders o LEFT JOIN products p ON o.product_id = p.id WHERE o.created_at < '2023-06-15 14:22:03' AND o.status = 2 ORDER BY o.created_at DESC LIMIT 20;
- 避免
OFFSET,用WHERE + ORDER BY + LIMIT组合替代 - 确保
ORDER BY字段有索引,且和WHERE条件能组合使用(例如(status, created_at)联合索引) - 如果业务允许,前端分页按钮只显示前 100 页,后端对
OFFSET > 10000的请求直接拒绝或降级返回空
LIMIT offset, size 写法**。这三个点卡住,加再多索引也没用。

















