JOIN 为什么让行数增加:连接键、约束与结果粒度
通过统计实际连接键、检查约束和确定结果粒度,定位 JOIN 后重复出现的行。
本文内容
简明答案
JOIN 返回的是满足条件的行对,因此一条来源记录可能合理地出现多次。先明确结果中的一行表示订单、订单明细,还是订单汇总,再检查两侧真正用于连接的键,并应用相应输入过滤条件。对于非 NULL 的等值连接键,左侧出现 L 次、右侧出现 R 次,就会产生 L × R 个匹配行对。当前数据没有重复,并不等于数据库已经保证该键唯一;还要单独查看主键、唯一约束和外键。
JOIN 实际统计什么
对于 INNER JOIN,每一对满足 ON 条件的来源行都会贡献一条结果行。外连接还可能保留没有匹配对象的行,并在另一侧填入 NULL。使用笛卡尔积来解释 SQL,描述的是逻辑含义,不代表数据库一定先在物理上生成全部组合。分析时先检查连接条件,再查看完整查询中的其他部分。连接之后的 WHERE 过滤可能删除部分行,因此最终数量不能只由 JOIN 的名字推断。
笛卡尔积与重复键
CROSS JOIN 将每条左侧记录与每条右侧记录配对。两边各有三条记录时,就有九个行对。等值连接只保留键相等的组合。同一个键在两边都重复时,该键产生的匹配数量等于两侧出现次数的乘积,可能形成多对多的放大。仅存在一对多关系,并不意味着结果一定超过最大输入表的行数。应按实际匹配键计算,而不是把总行数当成关系类型的直接证明。
WITH t1(num) AS (VALUES (1), (2), (3)),
t2(num) AS (VALUES (1), (3), (5))
SELECT t1.num AS left_num, t2.num AS right_num
FROM t1 CROSS JOIN t2;
-- Nine pairs in this illustrative dataset.一对多关系的独立示例
示例中,订单 1 有两条明细,订单 2 有一条明细。连接得到三行,其中订单 1 的标识出现两次,但对应的明细标识不同。这些行表达的是有效关系,直接当作错误重复删除会损失信息。如果报表需要每条明细一行,这种粒度正合适;如果需要每个订单一行,就应先汇总明细,或者采用其他获取所需信息的方式。先定义输出对象,才能判断行数增加是否需要处理。
WITH orders(order_id) AS (VALUES (1), (2)),
order_items(item_id, order_id) AS (VALUES (10, 1), (11, 1), (12, 2))
SELECT o.order_id, i.item_id
FROM orders AS o
JOIN order_items AS i ON i.order_id = o.order_id
ORDER BY o.order_id, i.item_id;
-- Three pairs: order 1 appears with two different items.LEFT JOIN 与没有匹配的行
LEFT JOIN 会保留没有右侧匹配的左行,并把右侧列设为 NULL。不过,一条左行找到多个右侧对象时,仍然会输出多行。小示例中键 1 和 3 匹配,键 2 没有伙伴,因此得到三行。这个结论描述的是后续过滤之前的连接结果。如果 WHERE 对右侧列设置条件,原本保留的 NULL 行可能被删除。检查问题时要阅读完整查询,不能仅凭 LEFT JOIN 就断言全部左侧记录最终仍然存在。
WITH t1(num) AS (VALUES (1), (2), (3)),
t2(num) AS (VALUES (1), (3), (5))
SELECT t1.num AS left_num, t2.num AS right_num
FROM t1 LEFT JOIN t2 ON t1.num = t2.num
ORDER BY t1.num;
-- Three rows; the right value for left key 2 is NULL.统计实际使用的连接键
对两侧真正参与连接的键分别统计出现次数,并先应用查询所需的过滤。示例按 order_id 对明细分组,找出拥有两条明细的订单。诊断普通等值连接时应排除 NULL 键,因为在这种条件下,NULL 不等于另一个 NULL。当前样本中每个键只出现一次,也不能证明存在永久的一对一关系或唯一约束。后续导入可能产生重复,因此需要同时检查数据分布和数据库模式中的保证。
WITH order_items(item_id, order_id) AS (VALUES (10, 1), (11, 1), (12, 2))
SELECT order_id, COUNT(*) AS item_count
FROM order_items
WHERE order_id IS NOT NULL
GROUP BY order_id
HAVING COUNT(*) > 1;
-- Order 1 has two items in this illustrative dataset.外键、唯一性与 NULL
外键把子表列关联到受到唯一性保证的被引用键,但不会自动让子表的引用值也唯一。只要没有其他约束,多条明细就能引用同一个订单。外键本身也不必要求引用值始终填写,因为 NULL 是否允许需要单独确定。示例模式明确对 order_id 添加 NOT NULL,使这个要求清楚可见。还要核对 ON 实际使用的列与声明的约束,尤其是复合键或带有附加连接条件的查询。
CREATE TABLE orders (
order_id integer PRIMARY KEY
);
CREATE TABLE order_items (
item_id integer PRIMARY KEY,
order_id integer NOT NULL REFERENCES orders(order_id)
);选择需要的结果粒度
连接多个明细集合时,一个集合中的值可能随着另一个集合的每个对象重复,导致汇总金额或数量失真。如果需要每个订单一行,应先把子表聚合到该粒度,再进行连接。示例先统计订单明细数,再把汇总结果连接到订单。这里的 INNER JOIN 只显示有明细的订单;如果还要保留没有明细的订单,应使用 LEFT JOIN,并明确缺少的计数是显示为零还是采用其他表示方式。
SELECT o.order_id, i.item_count
FROM orders AS o
JOIN (
SELECT order_id, COUNT(*) AS item_count
FROM order_items
GROUP BY order_id
) AS i ON i.order_id = o.order_id;用明确数据比较三种连接
最后的示例直接通过 VALUES 定义小型输入,所以全部来源键都能在查询中看到。两边共同拥有的只有 1 和 3,因此 INNER JOIN 得到两行。之前的 LEFT JOIN 示例还保留键 2,得到三行;CROSS JOIN 则形成全部九个行对。这些数量是根据列出的教学数据推算的,不是性能测试结果。可以在脑中增加一条相同键的记录,逐一分析它会带来哪些新的匹配组合。
WITH t1(num) AS (VALUES (1), (2), (3)),
t2(num) AS (VALUES (1), (3), (5))
SELECT t1.num
FROM t1 INNER JOIN t2 ON t1.num = t2.num
ORDER BY t1.num;
-- Two rows: matching keys 1 and 3.检查清单
- 对两侧实际连接键统计次数,并考虑输入过滤与 NULL。
- 分别检查 PRIMARY KEY、UNIQUE 和 FOREIGN KEY,不用当前数据代替模式保证。
- 非 NULL 键在两侧分别出现 L 次和 R 次时,等值连接产生 L × R 个匹配行对。
- EXPLAIN 可用于查看估计,但连接算法本身不能证明重复,应检查匹配键的数量。
适用范围
示例针对等值连接和 PostgreSQL 语法。其他条件、复合键、WHERE、分组以及结果列的选择都可能改变输出。所列数量来自教学 VALUES;没有声称实际执行查询或测量速度。