联合查询这块,很多人都会用,但一到多表、一到亿级数据就翻车。借着这次整理,我把日常开发里最常用的 JOIN 写法、UNION 的细节、以及用 EXPLAIN 排查联合查询性能问题的完整思路都捋了一遍,最后附一个真实订单报表的优化复盘,希望对正在折腾 MySQL 的你有帮助。
1. 联合查询的底层逻辑:先搞清楚 JOIN 在内存里到底干了什么
1.1 一个走了弯路的真实场景
前几天看到热搜词里有“mysql存储过程”“mysql锁的分类”“mysql事务处理”,说明很多人已经开始碰 MySQL 里比较复杂的东西了。但不管你是写存储过程、调事务还是排查慢查询,都绕不开一个基础操作:联合查询。
我印象很深的一次,是帮一个朋友调慢查询。他的业务逻辑很简单:用户表 JOIN 订单表,统计每个用户的订单数,数据量也就几十万行。按理说这个量级随便怎么 JOIN 都不会慢,结果那个 SQL 跑了 8 秒。我一看他的写法:
SELECT u.name, COUNT(o.id) FROM users u, orders o WHERE u.id = o.user_id GROUP BY u.name;他没写 JOIN 关键字,直接用了逗号 + WHERE。这写法其实是个隐式 INNER JOIN,本身没问题,但问题在于——users 表和 orders 表的关联字段都没有索引。联合查询一旦关联字段没索引,MySQL 就只能一遍遍地全表扫描,几十万行也能把磁盘 IO 打满。
1.2 JOIN 的本质:临时表的组装过程
很多教程喜欢直接给你讲语法,但我觉得得先搞懂 JOIN 的底层逻辑,后面排错才不慌。
打个比方,JOIN 就像你在 Excel 里开了两个 Sheet,Sheet A 是用户,Sheet B 是订单,现在要把它们并成一张表。怎么并呢?常见的做法是:
- 拿 Sheet A 的第一行,去 Sheet B 里找匹配的行,找到了就拼成一行,放到新表里。
- 然后拿 Sheet A 的第二行,继续去 Sheet B 里找……
- 直到 Sheet A 所有行都处理完。
这就是最早 MySQL 最常用的Nested-Loop Join(嵌套循环连接)的思路。关联字段有索引时,去 Sheet B 里“找”的动作走索引,效率很高;没索引就是全表扫,效率直线下降。
到了 MySQL 8.0,引入了一个很重要的改进:Hash Join。这是针对等值 JOIN 的优化——它会把驱动表的数据放进内存里建一个哈希表,然后扫描另一个表,用哈希查找匹配。对于大表 JOIN 大表、且关联字段没索引的场景,Hash Join 能比 Nested-Loop 快很多。我实测过一个三百万行的表做 INNER JOIN,MySQL 8.0 下从原来的 12 秒降到 1 秒多。
注意:Hash Join 主要适用于等值连接。如果你用的是 LIKE、范围比较这类非等值 JOIN,MySQL 还是大概率走 Nested-Loop。
1.3 笛卡尔积是怎么产生的
新手最容易踩的坑就是忘写 ON 条件。比如:
SELECT * FROM users JOIN orders;这会让 MySQL 把 users 的每一行都和 orders 的每一行配对。users 有 1 万行、orders 有 10 万行,结果就是 1 亿行。这个结果集就叫笛卡尔积。你看到的 Join Buffer 飙升、临时表膨胀,很多时候都是这个原因。
联合查询的核心原则是:永远带着 ON 写 JOIN,永远用 WHERE 去缩小输入表的行数。哪怕是一个很快的 JOIN,如果把两张千万级大表先全部加载再关联,也很难救回来。
2. INNER JOIN 和 LEFT JOIN 的选择:别被需求文档带偏
2.1 三种 JOIN 的实际行为差异
联合查询最常碰到的就是这三种:
| 类型 | 行为 | 结果特征 |
|---|---|---|
| INNER JOIN | 两边都匹配才返回 | 只留交集 |
| LEFT JOIN | 左表全保留,右表无匹配填 NULL | 左表的超集 |
| RIGHT JOIN | 右表全保留,左表无匹配填 NULL | 右表的超集 |
举例。users 表:
| id | name |
|---|---|
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
orders 表:
| id | user_id | amount |
|---|---|---|
| 101 | 1 | 200 |
| 102 | 1 | 300 |
| 103 | 3 | 500 |
执行:
SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;得到的是:
| name | amount |
|---|---|
| 张三 | 200 |
| 张三 | 300 |
| 王五 | 500 |
李四因为没有订单,不会出现在结果里。这是很多人容易误判的地方:INNER JOIN 并不等于“两表都有数据才查出来”,而是等于“只返回匹配上的行组合”。张三有两条订单,所以结果里就有两行张三。
再看 LEFT JOIN:
SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id;得到的是:
| name | amount |
|---|---|
| 张三 | 200 |
| 张三 | 300 |
| 李四 | NULL |
| 王五 | 500 |
李四没有订单,但 LEFT JOIN 会保留他的记录,订单字段填 NULL。**“以左表为主”**就是这个意思。
2.2 业务里到底该用哪个
很多开发在写需求时容易犯一个毛病:看到“查询用户的订单详情”就写 LEFT JOIN,看到“查询有订单的用户”就写 INNER JOIN。其实,需求描述里有没有“所有用户”“全部”“包含未下过单的”这类字样,才是判断的关键。
- 需求是“列出每个用户的订单情况和消费总额,没买过东西的也要显示”——用 LEFT JOIN。
- 需求是“统计有订单记录的用户数”——用 INNER JOIN,或者用 LEFT JOIN + WHERE o.id IS NOT NULL,但前者效率更高、语义更清晰。
- 需求是“查出所有商品以及它被加购的次数,没被加购的商品也要列出来”——LEFT JOIN 从商品表出发。
这里我要多说一句:RIGHT JOIN 我很少用。一方面它不如 LEFT JOIN 直观,因为绝大部分人阅读 SQL 的顺序是从左到右、从 FROM 开始,RIGHT JOIN 会打乱思路;另一方面,完全可以通过调换表的顺序把 RIGHT JOIN 改写成 LEFT JOIN。
2.3 FULL JOIN 的模拟写法
MySQL 直到现在也没有原生 FULL JOIN。热搜词里有“mysql联合查询”,我看到不少人搜 FULL OUTER JOIN 怎么实现。其实不用纠结,用 LEFT JOIN + RIGHT JOIN + UNION 就能模拟:
SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id UNION SELECT u.name, o.amount FROM users u RIGHT JOIN orders o ON u.id = o.user_id;意思是:先把左表没匹配上的行找出来,再把右表没匹配上的行找出来,合并去重,就是全外连接。当然,真到这一步,我会先停下来想想——是否有必要把两张表所有不匹配的数据都查出来?这种需求往往出现在对账、数据清洗场景,实际业务里频率很低。
2.4 JOIN 写错的危险信号
写 JOIN 最典型的错误,是把 ON 里的过滤条件和 WHERE 混用,结果导致 LEFT JOIN 悄悄退化成 INNER JOIN。
比如:
SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.amount > 100;这个 SQL 表面上用了 LEFT JOIN,但 WHERE 把 o.amount 为 NULL 的行全过滤掉了。李四的订单是 NULL,NULL 不满足 WHERE 条件,所以结果里不会出现李四。LEFT JOIN 形同虚设,变成了 INNER JOIN。
正确的做法是把过滤条件放进 ON 后面:
SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.amount > 100;这样李四依然会出现在结果里,只是订单字段为 NULL。这个坑我在实际开发里见过至少三次,每次都是统计数据对不上,最后一行行排查 SQL 才发现是这里出了问题。
3. UNION 和 UNION ALL:纵向联合查询的另一条路
3.1 什么时候需要 UNION
JOIN 是横向拼接——把两个表的列拼到一起。UNION 是纵向拼接——把两个查询的结果行堆到一起。虽然都叫“联合查询”,但应用场景完全不同。
我记得有个需求:查“当月既下了大额订单,又写了退货申请的用户”。大额订单在 order 表,退货申请在 return_application 表,如果只是简单 JOIN,会让数据大量重复。这时候用 UNION 更合理:
SELECT user_id FROM orders WHERE amount > 5000 UNION SELECT user_id FROM return_application WHERE status = 'pending';UNION 会自动去重。如果一个用户既下了大额订单,又写了退货申请,他只会出现在结果里一次。而 UNION ALL 不去重:
SELECT user_id FROM orders WHERE amount > 5000 UNION ALL SELECT user_id FROM return_application WHERE status = 'pending';同一个用户可能出两行。
3.2 UNION 和 UNION ALL 的性能差距
很多人不知道,UNION 的去重是有代价的。MySQL 要创建一个临时表,然后把两次查询的结果放进去,再对临时表做去重。数据量大的时候,这一步非常耗时。
如果你确定两个查询的结果不会重复,或者业务上允许重复,直接用 UNION ALL 就好。比如分页查询的合并、不同时间段数据的拼接,这些场景几乎不可能重复,别让 MySQL 白白做一次去重。我自己写大数据量统计时,默认都用 UNION ALL,只有明确需要去重才用 UNION。
提示:UNION 的每条 SELECT 语句要有相同数量的列,列类型要能兼容。排序和分页要放在最后一条 SELECT 后面,单独对其中一条排序是无效的。
3.3 UNION 与子查询的配合
有时候单条 SQL 搞不定的场景,用 UNION + 子查询反而更清爽。举个例子:
SELECT t.category, SUM(t.total) FROM ( SELECT '线上' AS category, SUM(amount) AS total FROM online_orders UNION ALL SELECT '线下' AS category, SUM(amount) AS total FROM offline_orders ) t GROUP BY t.category;这样的核心思路是:先纵向把两张结构相近的表合并,再作为一个整体去统计。比起 CASE WHEN 加两个 LEFT JOIN,这种写法在某些场景下可读性更好,MySQL 的查询规划器也更喜欢处理这种简单结构。
3.4 子查询里面的 JOIN
除了 UNION,子查询和 JOIN 也经常组合使用。比如“查最近三个月内有订单的用户及订单金额”,你既可以先写一个子查询把最近的订单筛出来,再和用户表 JOIN:
SELECT u.name, recent.amount FROM users u INNER JOIN ( SELECT user_id, amount FROM orders WHERE order_date >= DATE_SUB(NOW(), INTERVAL 3 MONTH) ) recent ON u.id = recent.user_id;很多人问我到底该用 JOIN 还是子查询。我的经验是:能用 JOIN 表达清楚的,优先用 JOIN。子查询在 MySQL 5.7 及之前的版本,优化得不如 8.0 好,容易产生临时表;但 MySQL 8.0 引入了对子查询的提升优化,很多场景子查询和 JOIN 已经被改写成了很接近的执行计划。不过从可维护性来说,JOIN 的执行计划更稳定、更可控,这是我在生产环境里更推荐它的原因。
4. 联合查询的性能问题:EXPLAIN、索引与驱动表
4.1 联合查询慢,第一步先看 EXPLAIN
每次有人把慢 SQL 发给我,第一步永远是执行:
EXPLAIN SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.created_at >= '2024-01-01' GROUP BY u.id;EXPLAIN 输出里,我最关心的字段是这几个:
| 字段 | 含义 | 我关注的点 |
|---|---|---|
| type | 访问类型 | 若出现 ALL,说明全表扫描,优先优化 |
| key | 实际用到的索引 | 为 NULL 说明没走索引 |
| rows | 预估扫描行数 | 数值越大风险越高 |
| Extra | 额外信息 | 出现 Using temporary / Using filesort 要警惕 |
set type 常见等级是 system > const > eq_ref > ref > range > index > ALL。如果联合查询驱动的表访问方式是 ALL,几乎可以肯定这是慢查询的元凶。但这里有个细节:两张表 JOIN,驱动表大一点没关系,被驱动表的关联字段必须要有索引。被驱动表如果全靠 ALL 扫描,那才是灾难。
4.2 驱动表怎么选:小表驱动大表
联合查询中“哪张表是驱动表”很关键。MySQL 默认会选一个“代价最小”的执行计划,但不一定每次都对。比如加 WHERE 过滤后,某张表被过滤得很窄,那这张表大概率会成为驱动表。
一个老生常谈的原则是:小表驱动大表。也就是用数据量小、过滤后行数少的表作为外层循环,大表作为内层循环“被查找”的表。内层表走索引查找,这样整体开销最小。
强制指定驱动表可以用 STRAIGHT_JOIN,但我不建议新手用它。它会让 MySQL 按你指定的顺序执行,用得好性能起飞,用不好反而更慢。我实际工作中更倾向于:
- 给被驱动表的关联字段加索引。
- 调整 WHERE 条件,让某张表先过滤。
- 必要时用 LEFT JOIN 的类型来引导方向。
4.3 联合查询最容易出现的两个 Extra
第一类是 Using temporary。这意味着 MySQL 创建了一张临时表来存中间结果。GROUP BY、ORDER BY、UNION、DISTINCT 都容易触发临时表。如果临时表是磁盘临时表,性能会断崖式下跌。解决办法通常是:让 GROUP BY 的字段和被驱动表的关联字段一致,或者减少 GROUP BY 的字段数。
第二类是 Using filesort。这并不代表真的使用了磁盘文件,只代表 MySQL 没法直接利用索引完成排序,必须额外做一次排序动作。联合查询带 ORDER BY 时,如果排序字段不是索引的一部分,就会触发 filesort。优化思路是让 ORDER BY 的字段参与到已经建立好的联合索引里,或者先缩小数据量再排序。
我举一个实际遇到的例子。原 SQL:
SELECT o.order_no, u.name FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.create_time >= '2024-06-01' ORDER BY o.create_time DESC;orders 表有几百万行,EXPLAIN 看到 Extra 是 Using filesort,而且用了两秒多。后来我发现 orders 表上已经有一个idx_create_time单列索引,但因为查询里 JOIN 了 users,优化器要先 JOIN 完才能排序,单列索引没能直接派上用场。最终我把 ORDER BY 去掉,改成在 WHERE 直接按创建时间分批取数据,再用程序排序——一次查询只查 5000 条,排序开销完全可接受,速度提升非常明显。
4.4 联合查询与索引设计的关系
热搜词里有“mysql创建索引”“mysql索引失效”,联合查询恰恰是索引设计的最佳实践场景。这里有个建议:
- 被驱动表的关联字段一定要建索引。也就是 LEFT JOIN 中右侧表的 ON 字段。
- WHERE 里的过滤字段优先建索引。因为它决定了驱动表返回多少行。
- GROUP BY / ORDER BY 字段尽量与索引一致。
我举一个典型的索引组合。orders 表经常有这种查询:
SELECT u.name, SUM(o.amount) FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.status = 1 GROUP BY o.user_id;单纯在status上建索引不行,在user_id上建索引也不行。最理想的是建立一个(status, user_id)联合索引,这样 WHERE 过滤走 status,GROUP BY 走 user_id,一次索引扫描就能完成。性能从原来的 3 秒降到 0.1 秒。
注意:联合索引的顺序很重要。等值条件放前面,范围条件放后面,这个原则在 order_status + create_time 这类组合里尤其明显。
4.5 JOIN 太多导致的死锁与事务问题
热搜词里有“mysql锁的分类”“mysql事务处理”。联合查询如果出现在事务里,尤其是多个 JOIN 更新多张表,很容易碰到锁的问题。我见过一个案例:事务里先 UPDATE orders 再 UPDATE users,另一个事务先 UPDATE users 再 UPDATE orders,两边互相等锁,产生了死锁。
这个问题和 JOIN 本身关系不大,但和“多表操作顺序”密切相关。我的经验是:事务里涉及到多表操作时,固定统一的表访问顺序。比如都先 users 再 orders,可以大大降低死锁概率。另外,事务里 JOIN 查询尽量控制在合理范围内,不要在一次事务里 JOIN 十几张表,因为锁的粒度越大,事务持锁时间越长。
5. 实战复盘:一个订单报表慢查询从 12 秒到 0.3 秒
5.1 原始需求与 SQL
这是一个真实项目里的需求:统计一段时间内每个销售顾问的业绩,包括订单总数、订单总金额、最新订单时间。涉及三张表:
- sales_rep 销售顾问表,约 2000 行。
- orders 订单表,约 300 万行,有 sales_rep_id 字段。
- order_items 订单明细表,约 1200 万行。
原始 SQL 是一个同事写的:
SELECT sr.name, COUNT(DISTINCT o.id) AS order_cnt, SUM(oi.amount) AS total_amount, MAX(o.created_at) AS last_order_time FROM sales_rep sr LEFT JOIN orders o ON sr.id = o.sales_rep_id LEFT JOIN order_items oi ON o.id = oi.order_id WHERE o.created_at >= '2024-01-01' AND o.created_at < '2024-02-01' GROUP BY sr.id;执行时间 12.6 秒。这个 SQL 看起来没什么问题,但实际跑了之后问题很大。我们一步步看怎么查的。
5.2 用 EXPLAIN 定位问题
EXPLAIN 的结果让我很意外:
- 驱动表是 sales_rep,这没问题。
- 访问 orders 时,type 是 ref,key 是
idx_sales_rep_id,看起来还行。 - 访问 order_items 时,type 是 ALL,rows 是 1200 万,Extra 里还有 Using where; Using join buffer。
问题一下就清楚了:order_items 表在关联字段 order_id 上没有索引,导致每次都要全表扫描,一次性扫描 1200 万行。虽然只有 1 月份的数据匹配,但 MySQL 无法直接从 order_items 里快速取出对应订单明细,只能把 1200 万行都过一遍。
热知识:实际在 orders 表上已经有唯一主键 id 了,order_items 表的 order_id 并不是主键,也没建索引。这就是慢的根因。
5.3 第一次优化:加索引 + 调整 JOIN 顺序
直接给 order_items.order_id 加索引:
ALTER TABLE order_items ADD INDEX idx_order_id (order_id);再次执行,时间从 12.6 秒降到 3.8 秒。这步收益最大,但还没到理想水平。继续看 EXPLAIN,发现访问 order_items 时 type 变成了 ref,但 Extra 里仍有 Using join buffer,说明 MySQL 在处理某些匹配时还在用 join buffer 缓冲。而且 orders 表过滤 WHERE 之后还有约 18 万行,这 18 万行逐行去 order_items 里查,每行查一次索引,总代价也不低。
于是做第二步优化:缩小 order_items 的扫描范围,从源头减少 JOIN 参与的行数。因为查询只需要 1 月份的明细,可以对 order_items 的 order_id 做一次提前过滤,或者先算出 1 月订单的 order_id 集合再关联。
这里我采用了先 LIMIT 子查询的思路:
SELECT sr.name, COUNT(DISTINCT o.id) AS order_cnt, SUM(oi.amount) AS total_amount, MAX(o.created_at) AS last_order_time FROM sales_rep sr LEFT JOIN ( SELECT id, sales_rep_id, created_at FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01' ) o ON sr.id = o.sales_rep_id LEFT JOIN ( SELECT oi.order_id, oi.amount FROM order_items oi INNER JOIN orders tmp ON tmp.id = oi.order_id WHERE tmp.created_at >= '2024-01-01' AND tmp.created_at < '2024-02-01' ) oi ON o.id = oi.order_id GROUP BY sr.id;这样 order_items 的扫描范围从一开始就被限制在 1 月订单的明细里,可能只有几十万行,而不是 1200 万行。
执行时间降到 1.2 秒。
5.4 第二次优化:COUNT DISTINCT 改 EXISTS 和 SUM 分离
3.8 秒降到 1.2 秒后,我再用 EXPLAIN 看,发现 GROUP BY sr.id 的时候 Extra 里还有 Using temporary;而且 COUNT(DISTINCT o.id) + SUM 组合让临时表负担很大。
COUNT(DISTINCT o.id) 的本质是去重统计,在 JOIN 明细之后再做去重,其实做了很多无用功——因为一次 JOIN 可能产生同一个 order 对应多条 order_items,明明一个订单只需要统计一次,却被重复 JOIN 了很多行。
我把 SQL 拆成了两步,先聚合 order_items 得到每单的金额,再 JOIN 订单和销售顾问:
SELECT sr.name, COUNT(o.id) AS order_cnt, SUM(item_summary.total_amount) AS total_amount, MAX(o.created_at) AS last_order_time FROM sales_rep sr LEFT JOIN ( SELECT id, sales_rep_id, created_at FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01' ) o ON sr.id = o.sales_rep_id LEFT JOIN ( SELECT order_id, SUM(amount) AS total_amount FROM order_items GROUP BY order_id ) item_summary ON o.id = item_summary.order_id GROUP BY sr.id;这里把 order_items 提前按 order_id 聚合,明细行先压缩成一个订单一行,再 JOIN,临时表大小和计算结果都大大减少。执行时间从 1.2 秒降到 0.3 秒。
5.5 这次复盘的经验教训
这个案例里,优化的顺序特别重要。我最先做的是加索引,这步收益最大;然后再做“提前聚合+缩小 JOIN 范围”,这步彻底解决了明细膨胀的问题。如果你问我哪一步最关键,我会说加索引是保底,提前聚合是质变。
几个经验总结:
- 先看 EXPLAIN,别瞎猜。12 秒到 0.3 秒,每一步都有 EXPLAIN 数据支撑。
- 永远先给被驱动表关联字段加索引。这是最便宜、最有效的一步。
- GROUP BY 和 COUNT DISTINCT 不要放在明细 JOIN 之后。先在子查询里聚合好,再 JOIN,是通用解法。
- 不要迷信一张大 SQL 搞定所有需求。拆成两步甚至三步,既好维护又好优化。
6. 聊一点 MySQL 8.0 的联合查询新特性和后续扩展
6.1 Hash Join 在等值 JOIN 里的实际效果
MySQL 8.0 的 Hash Join 是很多从 5.7 升上来的人没注意到的点。它的适用条件是:等值连接且关联字段没有索引,或优化器认为 hash join 代价更小。
我拿一个简单例子验证过。两张表各 100 万行,关联字段都没索引,执行:
SELECT * FROM a INNER JOIN b ON a.id = b.a_id;MySQL 5.7 是 Nested-Loop,跑了 20 多秒;MySQL 8.0 自动切成 Hash Join,大概 3 秒左右。这个优势非常明显。如果你的生产环境还在 MySQL 5.7,并且经常有表 JOIN 表但没索引的情况,升级 8.0 是值得考虑的。
不过要注意,Hash Join 需要两块内存来存哈希表,如果内存不足,MySQL 会把数据放到磁盘临时文件,这样反而更慢。所以,不给关联字段建索引,完全依赖 Hash Join,在大数据量下还是有风险的。最佳实践仍然是:建索引为主,Hash Join 作为兜底。
6.2 联合查询在高并发下的注意事项
热搜词里有“mysql高并发解决方案”,联合查询在高并发场景下的使用要更克制。我参与过一个项目,高峰期 QPS 几千,接口里有一个五表 JOIN 的查询,数据量不大,但每个请求都要 JOIN,数据库压力骤增。
后来我们把那个 JOIN 拆成了两步:
- 先用主键或唯一索引快速查出基础数据。
- 再根据结果里的关联 ID,批量查另一张表。
这样每个查询简单清晰,也更容易走索引。如果你在高并发接口里遇到 JOIN,建议先考虑能否换成“两次简单查询 + 内存拼装”。绝大多数场景是能换的,而且换完之后性能更稳定。
当然,这不是说 JOIN 不能用。对于数据量可控、索引完善、并发不高的场景,JOIN 依然是表达数据关系最优雅的方式。我只是建议你在高并发入口处多留个心眼。
6.3 联合查询的常见扩展方向
最后聊一下联合查询还能往哪些方向扩展:
- 分页 JOIN:JOIN 结果分页时,不要先 JOIN 再 LIMIT,可以先用子查询查出订单 ID,再 JOIN 明细,能大幅减少扫描行数。
- JOIN + 窗口函数:MySQL 8.0 的窗口函数可以在 JOIN 之后做分组排名,比如查每个客户最近一笔订单。
- JOIN 更新:UPDATE 也可以联合多张表,但要小心行锁和死锁,务必加好条件。
- JOIN 的 NULL 处理:LEFT JOIN 产生 NULL 后,在统计时要注意 COUNT(字段) 不会计入 NULL,COUNT(*) 会计入。这个细节很容易让报表数据对不上。
这些方向都是可以单独展开的,这里就不展开了。
我自己在实际项目里摸索下来,JOIN 这东西最怕的不是不会写,而是不知道它背后怎么执行。只要每次写完 JOIN 都跑一遍 EXPLAIN,多观察 type、key、rows、Extra 这四个字段,你写 JOIN 的能力会立刻上一个台阶。哪怕暂时看不懂所有细节,养成这个习惯,也能避开大部分性能坑。