做SQL开发这些年,要我说面试题里出现频率最高、业务开发里最容易翻车的知识点,JOIN绝对排第一。不管是写报表、做统计、还是调接口取数,每天都要跟各种连接打交道。这个标题之所以叫"高频SQL基础版",就是因为连接这个知识点,看着简单,但真正能把它讲清楚、用明白的人不多,尤其是在字段为空、统计口径、性能优化这些细节上,坑一个接一个。
这篇内容我打算把JOIN的内功和外功一起讲:先帮你把连接的本质和五种基本形式吃透,再结合实际场景拆解EXISTS、IN、LEFT JOIN怎么选,最后把慢SQL排查和面试高频题串一遍。无论你是刚入门的新手,还是写了几年SQL想补漏的老手,这篇都值得花十分钟读完。
1. 连接的本质:为什么JOIN是SQL高频核心
1.1 关系型数据库的灵魂:数据为什么要拆表
要理解JOIN,先得理解为什么数据库要把数据拆成一张张表。很多人刚学SQL的时候都会问一个问题:把订单和客户信息放一张大表里不就行了,为什么要分开存?
这里有一个简单但重要的逻辑:数据冗余是万恶之源。假设你把客户姓名、地址、电话都冗余在订单表里,客户一改手机号,所有历史订单都要跟着改,改漏一条数据就错一条。而拆成订单表(存customer_id)和客户表(存姓名、电话),客户改一次信息,全系统的订单查询自动就是新数据。这就是关系型数据库的"关系"二字的含义,而JOIN就是把这种拆分后的数据重新组装起来的手段。
在业务系统里,最常见的拆分场景有这么几类:主表和明细表(比如订单头和订单明细)、业务表和维度表(比如销售记录和产品分类)、自身关联表(比如员工表的上级字段关联到同表员工ID)。每一类拆分的背后,都是为了减少重复存储、保证数据一致性。而在查询时,SQL通过JOIN把这些表按照指定的关联条件拼回去。
1.2 JOIN的执行逻辑:从笛卡尔积到连接条件
搞清楚JOIN的执行过程,你会发现后续的很多优化思路都是从这里推导出来的。JOIN最底层的逻辑是笛卡尔积:把左表的每一行和右表的每一行做全组合。两张表分别有100条和200条数据,笛卡尔积就是20000行。如果你写了JOIN但没写ON条件,或者ON条件写错导致永远为真,那查询结果就是这个全组合量级,数据量一大,数据库直接卡死。
加了ON连接条件之后,笛卡尔积的结果集会被筛选,只保留满足条件的行。比如订单表按customer_id关联客户表,那每个订单只会匹配到自己的客户,生成的行数基本等于订单数(特殊情况下会多,比如客户表有重复数据,这一点后面专门讲)。
理解这个执行逻辑有什么用处?最直接的一个判断:JOIN完的结果集行数到底是多少,取决于关联字段在右表是否有重复、是否有缺失。肉眼扫一遍就能大致判断出查询结果对不对。很多慢SQL和统计口径错误,根源都在"连接条件"和"表数据特征"之间的匹配出了问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五大连接的分类与实战案例
2.1 INNER JOIN:只留两边都对得上的
INNER JOIN(内连接)是使用频率最高的连接方式,也是最符合大多数人直觉的连接:只返回左表和右表匹配成功的行,匹配不上的数据直接丢弃。
语义用一句话概括:取交集。比如订单表和客户表做INNER JOIN,结果里只会出现"已经关联到客户"的订单,那些customer_id为空、或者对应客户已被物理删除的订单,不会出现在结果里。
举一个实际业务例子。你要统计"有有效客户信息的订单总额",就可以用INNER JOIN:
sql复制SELECT
o.order_id,
o.amount,
c.customer_name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
这里如果客户表里有一个customer_id在订单表里出现20次,那结果里这个客户会对应20行订单,这是正常的“一对多”展开。如果你本意是"每个订单一行",结果出来多行,那大概率是右表的customer_id有重复,而不是INNER JOIN本身有问题。
我在实际开发里有个小习惯:每一次写INNER JOIN,都会先确认右表的关联字段是否唯一。如果右表有关联字段重复,要么先用子查询或窗口函数去重,要么就要明确自己确实需要这种“一对多”的展开结果。否则,统计数据会比预期多出好几倍。
2.2 LEFT JOIN:左表数据一条都不能少
LEFT JOIN(左连接)的逻辑是:把左表的全部行保留,右表能匹配上的就带过来右表的字段,匹配不上的,右表字段用NULL填充。
这里面最容易让新人犯迷糊的点是:LEFT JOIN出来的行数,至少等于左表行数,但可能大于左表行数。为什么?因为左表一行如果匹配到右表多行,结果里就会有多个副本。很多人以为LEFT JOIN"不会丢数据,所以永远安全",其实丢掉的是右表字段的完整性,而多出来的是重复行。这一点做统计时特别容易踩坑。
举个例子。订单表LEFT JOIN产品表,如果产品表里product_id有重复(比如一个产品因为录入问题存在两条记录),那订单表里每一条订单都会被展开成多行,SUM汇总后金额翻倍。你在做运营报表的时候可能根本发现不了,因为数据量看起来差不多,但汇总数就是不对劲。
还有一种情况需要特别说明:LEFT JOIN没有关联上的行,右表字段都是NULL。如果你想对这个字段做过滤(比如"只要关联上客户信息的订单"),千万不要把过滤条件写在WHERE里,否则左表匹配不上的行会被WHERE过滤掉,LEFT JOIN退化成INNER JOIN。正确做法是把过滤条件写在ON子句里:
sql复制SELECT
o.order_id,
c.customer_name
FROM orders o
LEFT JOIN customers c
ON o.customer_id = c.customer_id
AND c.status = 'active'
这个细节非常关键。写在ON里,那么即使客户status不是active,订单依然会保留,只是customer_name为NULL;写在WHERE里,订单就直接没了。
2.3 RIGHT JOIN与FULL OUTER JOIN:对称与全集
RIGHT JOIN在逻辑上和LEFT JOIN完全对称,只是保留的是右表的全部行。MySQL的老版本之前没有完整支持FULL OUTER JOIN,很多开发者对它很陌生,但它在一些补全场景里非常有用。
FULL OUTER JOIN(全外连接)返回左右两表的全集:左表独有的行、右表独有的行、匹配上的行全部保留,未匹配到的一侧用NULL填充。这在做数据对比、数据质量检查、对账时非常有用,比如对比两个系统的订单数据,找出两边都有、单边缺失的订单。
由于MySQL早期不支持FULL OUTER JOIN,很多老开发会用LEFT JOIN UNION RIGHT JOIN来模拟:
sql复制SELECT a.id, b.id
FROM table_a a
LEFT JOIN table_b b ON a.id = b.id
UNION
SELECT a.id, b.id
FROM table_a a
RIGHT JOIN table_b b ON a.id = b.id
这套写法兼容性好,UNION会自动去重。现在MySQL 8.0.31以上其实已经原生支持FULL OUTER JOIN了,但遇到老库老项目,这种UNION写法还是能经常看到的。
实际业务里,FULL OUTER JOIN最典型的应用场景是"两侧数据都存在差异的对比报表"。比如线上订单表和历史归档表做全量对账,找出哪些订单只在线上、哪些订单只存在于归档、哪些两边都有但金额不一致。你可能会问,用LEFT JOIN两次再加一个UNION不也行吗?确实行,但FULL OUTER JOIN写起来更直白,语义更清晰。
2.4 CROSS JOIN:特殊场景下的交叉连接
CROSS JOIN(交叉连接)是三种连接里最容易被人忽略的,因为它不带ON条件,直接生成两表的笛卡尔积。日常业务中直接使用CROSS JOIN的情况不多,但有两个场景很典型。
第一个场景是"生成排列组合"。比如你有10个城市和5个渠道,要生成一份"城市×渠道"的全量组合清单,用于投放计划的初始化,CROSS JOIN一行就能搞定:
sql复制SELECT
city.city_name,
channel.channel_name
FROM city
CROSS JOIN channel
第二个场景是在SQL中生成序列号。比如要生成某段时间内的每一天,常见做法是用数字表CROSS JOIN日期函数来展开日期序列,这在做缺数补零的报表时非常实用。
不过要特别提醒,CROSS JOIN在线上环境一定要慎用。两张1万行的表做CROSS JOIN,结果就是1亿行,如果这个结果还要参与后续计算,性能基本是灾难。我见过一个真实事故,因为某同事在调试时把INNER JOIN误写成了CROSS JOIN,导致一个统计任务跑了40分钟没跑完,最后被运维kill掉。所以每次写完SQL,建议先扫一眼确认ON条件是否存在、连接方式是否合理。
3. JOIN的关联查询与集合语义:EXISTS、IN与JOIN的选型
3.1 判断存在:EXISTS、IN、LEFT JOIN哪个好
搜索热词里有一个非常高频的组合:EXISTS、IN和LEFT JOIN。这三者都能实现"根据另一张表的数据来过滤当前表"的效果,但语义和执行计划差别很大。
先说IN。它适合右表数据量小、且没有NULL值的情况。比如查"有订单的客户",可以写成:
sql复制SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders)
这里有一个经典陷阱:如果子查询结果集包含NULL,IN的判断逻辑会变成"既不是等于某个值,也不是不等于NULL",导致查询结果为空。更稳妥的写法是把子查询加上WHERE customer_id IS NOT NULL,或者干脆用EXISTS。
EXISTS的写法:
sql复制SELECT customer_id, customer_name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
)
EXISTS是"只要存在就返回TRUE",语义上是半连接(semi join),只要找到一条满足条件的记录就会停止扫描。在很多数据库中,优化器会把EXISTS改写成JOIN或半连接来执行,性能关键看关联字段的索引。值得一提的是,子查询里写SELECT 1还是SELECT *,实际上对执行没影响,因为EXISTS根本不关心子查询返回的是什么列。
再来说LEFT JOIN。用LEFT JOIN判断存在的逻辑是:关联后右表字段为NULL的就是不存在:
sql复制SELECT c.customer_id, c.customer_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL
这其实是"找出没有订单的客户"的经典写法。但要注意,如果orders表数据量大,LEFT JOIN会把所有匹配上的行都建出来再过滤,比EXISTS半连接的开销要大。所以在"只判断存在性,不需要右表字段"的场景下,我个人的优先级是:EXISTS优先于IN和LEFT JOIN。如果业务上必须用IN且子查询数据量小,问题也不大;如果数据量上来了,EXISTS的执行计划通常更稳定。
3.2 JOIN与DISTINCT:去重陷阱
热词里出现"SQL语句去重查询",这在JOIN场景里是一个高频翻车点。
很多人写了JOIN之后发现结果行数变多了,第一反应就是加DISTINCT。DISTINCT确实能去重,但代价极大:它需要把所有字段进行一次排序或哈希去重,在数据量大时非常慢。更关键的是,加了DISTINCT只是掩盖了JOIN本身的问题,没有解决"为什么多出来行"这个根因。
这个根因往往是右表关联字段有重复。比如你要查"每个客户最近一次下单时间",如果客户表存在两条相同的customer_id记录,JOIN后每个客户会被展开两行,此时你用DISTINCT去重,如果查询的字段组合恰好无法唯一标识一行数据,去重后就可能丢掉真实数据。
举一个具体的例子。你要统计每个类别的商品销售总额,类目表category_id如果有重复,JOIN后销售明细会被重复展开,金额翻倍,此时即使加DISTINCT也解决不了,因为销售额本身每一行都不同,DISTINCT会保留所有重复展开的行。正确的做法是先修复类目表的数据重复,或者在JOIN之前用ROW_NUMBER()按category_id去重后,再参与JOIN。
sql复制WITH dedup_category AS (
SELECT
category_id,
category_name,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY category_id) AS rn
FROM category
)
SELECT
s.category_id,
c.category_name,
SUM(s.amount) AS total_amount
FROM sales s
INNER JOIN dedup_category c
ON s.category_id = c.category_id
AND c.rn = 1
GROUP BY s.category_id, c.category_name
所以我的建议是:不要在JOIN后盲目用DISTINCT,先去查关联字段有没有重复,再决定是去重源表还是调整连接条件。
3.3 JOIN条件与WHERE条件的执行顺序差异
JOIN条件写在ON里和写在WHERE里,在某些数据库里结果一样,但语义不同,尤其在LEFT JOIN场景下,差异非常大。
对于INNER JOIN,ON条件和WHERE条件的过滤语义基本等价,优化器可以把条件下推到合适的步骤。但对于LEFT JOIN,ON里的条件是在"连接阶段"过滤右表,而WHERE里的条件是在"连接完成后"过滤最终结果。前面说过,如果把右表字段的过滤条件写在WHERE里,会导致左表中未匹配的行被过滤掉,LEFT JOIN就失去保左意义。
还有一个容易被忽视的内外差异:如果你在ON里写了左表的过滤条件,比如ON o.customer_id = c.customer_id AND o.amount > 100,这个过滤只影响右表匹配的行,不会减少左表的保留行数。而同样的条件写在WHERE里,左表中amount <= 100的订单也会被过滤。很多报表取数时发现"LEFT JOIN之后行数不对",八成就是没弄明白这个差异。
我在写复杂SQL的时候,有一个自查习惯:先看ON条件,再看WHERE条件,确保"哪些行必须保留"和"哪些字段要过滤"分别落在正确的子句中。这个习惯能帮你避免大量统计口径的返工。
4. 慢SQL排查与JOIN优化实战
4.1 加索引的正确姿势
JOIN相关的慢SQL,绝大多数问题出在索引上。JOIN的性能核心在于"被驱动表"的关联字段能否快速定位数据,如果在被驱动表的关联字段上没有索引,每驱动一行就要做一次全表扫描,复杂度就是驱动表行数乘以被驱动表行数,数据量一大直接跑不动。
这里要引入一个概念:驱动表和被驱动表。在嵌套循环连接(Nested Loop Join)里,优化器会选一张表作为外表(驱动表),循环遍历外表每一行,然后去内表(被驱动表)找匹配行。内表的JOIN字段有没有索引,决定了"找匹配行"这一步是索引点查还是全表扫描。大多数慢SQL的解决方案就是在被驱动表的JOIN字段上加索引:
sql复制CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_customers_customer_id ON customers(customer_id);
另外一个容易忽略的点:索引列要避免函数包裹和隐式类型转换。比如JOIN条件写成ON a.user_id = CAST(b.user_id AS INT),即使字段本身有索引,函数处理之后索引也可能失效。还有字符集不一致导致的隐式转换,也会让索引失效,这个问题在跨表JOIN时特别常见,两个表的相同字段一个是utf8mb4一个是utf8,JOIN时可能做不了索引点查,需要提前统一字符集。
4.2 避免大表驱动小表
JOIN性能优化的核心思想之一,是尽量让"小表驱动大表"。为什么?因为驱动表的行数决定了循环和外层遍历的次数。驱动表越小,整体扫描次数就越少。
不过要说明一点:在基于成本的优化器(CBO)成熟之后,数据库会自己评估选择哪张表作为驱动表,你手动指定的优先级不一定生效。在MySQL 8.0和Oracle里,优化器通常会根据统计信息和索引情况自动选最优执行计划。所以"小表驱动大表"更多是你在日常写SQL时的一种直觉和习惯,而不是需要硬编码到SQL里的规则。
实操中真正能改善性能的几个手段是:
- 先过滤再JOIN:把WHERE条件尽可能早地应用,减少参与JOIN的行数。可以用子查询先缩小表范围,再参与JOIN。
- 避免SELECT *:只取需要的字段,减少行数据扫描和传输成本。
- 避免JOIN后的再过滤:如果WHERE条件里对右表字段做过滤,而左表又很大,LEFT JOIN会先连接再过滤,可能造成大量中间结果。
举一个例子。有一个订单表和区域表,你要查"华东区域订单总额"。如果直接写:
sql复制SELECT o.order_id, o.amount, r.region_name
FROM orders o
LEFT JOIN region r ON o.region_id = r.region_id
WHERE r.region_name = '华东'
这个SQL先把所有订单和区域表连接,再过滤华东。如果在订单表量特别大时,中间结果量可能会非常大。更优的写法是先过滤区域表再JOIN,或者直接把过滤条件放在关联之前:
sql复制SELECT o.order_id, o.amount, r.region_name
FROM orders o
INNER JOIN (
SELECT region_id, region_name
FROM region
WHERE region_name = '华东'
) r ON o.region_id = r.region_id
这样区域表在JOIN之前就已经缩减到极小,减少大量无效连接。
4.3 临时表和JOIN的取舍
在实际开发中,遇到"需要多次关联同一张子查询结果"的情况,有些人会选择CTE(WITH子句),有些人选择建临时表,还有的用嵌套子查询。这几种方式各有适用场景。
CTE的优点是逻辑清晰,可读性好,而且现在主流数据库(MySQL 8.0、PostgreSQL、Oracle、SQL Server)都原生支持。但要注意,CTE在同一个查询里被引用多次时,有些数据库不一定物化CTE结果,可能每次引用都执行一遍子查询,性能反而下降。如果CTE特别复杂、结果集又大、还被引用多次,我建议把它物化成临时表:
sql复制CREATE TEMPORARY TABLE tmp_active_customer AS
SELECT customer_id, customer_name
FROM customers
WHERE status = 'active';
ALTER TABLE tmp_active_customer ADD INDEX idx_customer_id(customer_id);
SELECT ...
FROM orders o
INNER JOIN tmp_active_customer c ON o.customer_id = c.customer_id;
临时表的好处是:你可以在上面加索引,也可以多次复用,还可以在调试时单独查一下临时表内容,快速检查中间结果是否正确。缺点是需要额外的存储空间和写入开销。所以我的经验是:小结果集用CTE或者子查询,大结果集且复用次数多的场景,建临时表加索引反而更可控。
4.4 关于内存参数的一个实际案例
搜索热词里有一条"超出全局hash join空间,适当增加hj_buf_global_size",这是国产数据库(达梦)在走Hash Join时常见的报错。Hash Join用于大表连接的场景:先扫描一张表构建哈希表,再扫描另一张表去匹配。如果JOIN的数据量超过哈希表缓冲区,就会报告空间不足,这时需要调整优化器相关参数(比如hj_buf_global_size)来扩大允许使用的内存。
这条热词提醒我们一个通用原则:不同数据库对JOIN算法的实现细节不一样(嵌套循环、哈希连接、排序合并连接),当出现特定的JOIN相关报错时,第一反应不是改SQL,而是先看执行计划确认走了什么JOIN算法,再去查对应参数。遇到这种情况,可以联系DBA一起看,因为调整数据库实例级别的参数会影响整个实例其他SQL的运行,不能随便调大,需要评估后执行。
5. 高频踩坑与面试要点速查
5.1 空值字段导致的JOIN丢失
热词里那条"access inner join 如字段为空就无法"看起来是在说ACCESS里的问题,其实这个坑在所有数据库里都通用:如果JOIN的关联字段存在NULL值,那么INNER JOIN不会匹配这些行,因为NULL不等于任何值,包括NULL本身。
比如客户表的customer_id字段允许为空,某些订单的customer_id是NULL,你拿订单表去做INNER JOIN客户表,这些空值订单会被静默丢弃。如果你没有意识到这一点,报表统计的订单数会少一部分。解决思路是,先确认业务上"空值字段如何定义":
- 如果空值代表"匿名订单",且这类订单也要统计,就改用LEFT JOIN并单独处理NULL。
- 如果空值代表脏数据,那就需要数据治理,先把空值修复或排除后再统计。
这里有一个SQL技巧,如果你确实想在关联时把NULL和NULL匹配起来,可以用ON a.id = b.id OR (a.id IS NULL AND b.id IS NULL),但这会严重破坏索引查找效率,非特殊情况不建议使用。更稳妥的做法是先用COALESCE给空值填充一个业务上不会出现的哨兵值(比如-1),再去做JOIN,这样既保持语义,又能走索引。
5.2 多表JOIN时统计口径错误
多表JOIN最容易出问题的地方在于统计口径。比如要统计"每个客户的订单总金额和积分总流水",分别关联订单表和积分表:
sql复制SELECT
c.customer_id,
SUM(o.amount) AS total_order_amount,
SUM(p.points) AS total_points
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN points_log p ON c.customer_id = p.customer_id
GROUP BY c.customer_id
这个SQL看起来没问题,但结果大概率是错的。为什么?因为orders和points_log同时和customers做一对多连接,结果是两者的笛卡尔积展开:如果一个客户有10笔订单、8条积分流水,JOIN后中间结果是80行,订单金额被重复算了8次,积分也被重复算了10次。
正确的做法是先分别聚合,再JOIN:
sql复制WITH order_agg AS (
SELECT customer_id, SUM(amount) AS total_order_amount
FROM orders
GROUP BY customer_id
),
points_agg AS (
SELECT customer_id, SUM(points) AS total_points
FROM points_log
GROUP BY customer_id
)
SELECT
c.customer_id,
COALESCE(o.total_order_amount, 0) AS total_order_amount,
COALESCE(p.total_points, 0) AS total_points
FROM customers c
LEFT JOIN order_agg o ON c.customer_id = o.customer_id
LEFT JOIN points_agg p ON c.customer_id = p.customer_id
这个多重JOIN导致统计翻倍的坑,我在面试题里见过无数次,在实际数据开发中也踩过。凡是涉及"多个一对多表同时JOIN"的取数,脑子里第一根弦就得绷紧:先聚合,再连接。
5.3 面试中关于JOIN的常见考点
SQL面试题里JOIN相关的高频考点,集中在以下几类:
第一类,概念辨析。INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN、CROSS JOIN的结果集是啥?会不会丢数据?行数会怎么变化?这些基础题要求你把结果集形态描述清楚。
第二类,改写题。给你一条带子查询的SQL,让你用JOIN改写;或者反过来,把JOIN改写为EXISTS。这里考的不是语法,而是你对执行计划的理解。我会建议面试者用"半连接"、"驱动表"这些词去解释改写思路,比单纯背语法得分高。
第三类,数据对账题。两张表各有一些数据,要求找出左表有右表没有、右表有左表没有、两边都有的行。这类题直接用LEFT JOIN + IS NULL和FULL OUTER JOIN来解,核心是理解连接结果的补集。
第四类,过滤陷阱题。给出一段LEFT JOIN带WHERE过滤的SQL,问你结果和预期有什么差异。这类题专门考察ON与WHERE的语义区别,需要你解释清楚"为什么结果行数变少了"。
每次看候选人答这种题,我都能明显感受到,能够用"实际数据特征"来说明判题思路的人,比单纯答"LEFT JOIN保留左表所有行"的人要稳得多。JOIN不只是填空语法,它是对数据关系的建模,平时多想想"这一条SQL在数据上是如何展开的",积累多了,面试和写代码都会顺很多。
5.4 高频错误速查表
这里整理一份JOIN相关的常见问题和排查方向,适合贴在工位上随时翻:
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| JOIN后行数爆炸 | 右表关联字段重复 | 先用GROUP BY检查右表关联字段是否有重复 |
| LEFT JOIN后左表行数变少 | 右表字段过滤条件写在WHERE里 | 把右表过滤条件移到ON子句 |
| 统计金额翻倍 | 多个一对多表同时JOIN | 先分别聚合,再JOIN |
| NULL关联行丢失 | 关联字段存在NULL | 确认业务上NULL的语义,改用LEFT JOIN |
| 带IN的子查询结果为空 | 子查询返回NULL | 子查询加IS NOT NULL过滤,或改用EXISTS |
| JOIN慢到卡死 | 被驱动表关联字段无索引 | 给JOIN字段加索引,查看执行计划 |
| INNER JOIN结果有重复行 | 右表数据有重复 | 去重源表或使用ROW_NUMBER去重后再JOIN |
这张表看着简单,但每一条背后都是真实生产环境里踩过的坑。如果你某天排查SQL查不出来问题,可以按这个表格顺序逐条比对,通常能很快定位到方向。
6. 写在最后的几句实在话
JOIN这个知识点,说基础是真的基础,但说深也真的很深。我见过不少写了三五年SQL的人,写复杂报表时依然会在多表JOIN上栽跟头,而且往往是栽在同一个坑上:统计口径被人为放大或缩小,自己还没察觉。
按照我个人的习惯,写任何一条带JOIN的SQL之前,都会先在心里过一遍这几个问题:这张表之间的关联关系是一对一、一对多还是多对多?关联字段有没有重复和空值?条件写在ON和WHERE里语义分别是什么?最终结果集行数是否符合业务预期?这套自检流程花不了几秒钟,却能帮你挡掉大量返工。
最后一句话送给大家:别把JOIN只当成一种语法,它背后是数据建模思维。你的数据表之间是什么关系,JOIN就会怎么展开。想明白了这一点,SQL水平自然而然就上一个台阶。
