跳到主要内容

表之间的连接

在关系型数据库中,数据通常按主题拆分到多个表中。例如,用户信息放在 users,订单信息放在 orders。拆分可以减少重复数据,但查询时又常常需要把相关表组合起来,这就是表连接(JOIN)。

本文使用下面的两张表说明连接过程。示例采用通用 SQL,MySQL、PostgreSQL 和 SQLite 的基本写法都适用。

CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(50) NOT NULL
);

CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER,
amount DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
usersorders
idnameiduser_idamount
1Alice101199.00
2Bob102125.00
3CarolNULLNULLNULL

先理解连接条件​

最基本的连接是根据两张表中表示同一关系的列进行匹配:

SELECT
users.name,
orders.id AS order_id,
orders.amount
FROM users
JOIN orders ON orders.user_id = users.id;

ON orders.user_id = users.id 是连接条件。它逐行比较两张表:当条件为真时,才把两行拼成一行。查询结果是:

nameorder_idamount
Alice10199.00
Alice10225.00

Bob 没有订单,Carol 也没有订单,所以不会出现在这个结果中。这里的 JOIN 没有写类型时,通常等价于 INNER JOIN。

为表设置别名可以让多表查询更容易阅读:

SELECT
u.name,
o.id AS order_id,
o.amount
FROM users AS u
INNER JOIN orders AS o ON o.user_id = u.id;

INNER JOIN:只保留两边都匹配的行​

INNER JOIN 返回连接条件在两张表中都能找到匹配项的行:

SELECT u.name, o.id AS order_id
FROM users AS u
INNER JOIN orders AS o ON o.user_id = u.id;

它适合回答“同时存在于两张表中的数据是什么”,例如查询已经下过订单的用户。由于一个用户可以对应多条订单,用户信息会在结果中重复出现;这不是重复数据,而是连接后的关系基数发生了变化。

LEFT JOIN:保留左表的全部行​

LEFT JOIN(也叫 LEFT OUTER JOIN)会保留 FROM 后面的左表所有行。右表找不到匹配项时,右表列使用 NULL:

SELECT
u.name,
o.id AS order_id,
o.amount
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
ORDER BY u.id, o.id;

结果会包含没有订单的用户:

nameorder_idamount
Alice10199.00
Alice10225.00
BobNULLNULL
CarolNULLNULL

因此,LEFT JOIN 很适合查找“全部用户及其订单”,也适合找没有关联记录的对象:

SELECT u.id, u.name
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
WHERE o.id IS NULL;

注意判断空值必须使用 IS NULL 或 IS NOT NULL,不能写成 o.id = NULL。在 SQL 的三值逻辑中,和 NULL 比较的结果不是 TRUE。

RIGHT JOIN 与 FULL OUTER JOIN​

RIGHT JOIN 与 LEFT JOIN 对称,保留右表全部行:

SELECT u.name, o.id AS order_id
FROM users AS u
RIGHT JOIN orders AS o ON o.user_id = u.id;

实际项目中通常把表交换位置后改写成 LEFT JOIN,因为后者更常见、更容易读懂。

FULL OUTER JOIN 保留两边所有行:能匹配的合并,不能匹配的那一侧补 NULL:

SELECT u.name, o.id AS order_id
FROM users AS u
FULL OUTER JOIN orders AS o ON o.user_id = u.id;

并非所有数据库都原生支持 FULL OUTER JOIN。例如,在不支持它的数据库中,可以用 LEFT JOIN、RIGHT JOIN 和 UNION 组合出类似结果;具体写法要根据数据库产品确认。

CROSS JOIN:生成所有组合​

CROSS JOIN 不需要连接条件,会把左表每一行与右表每一行组合。左表有 m 行、右表有 n 行时,结果最多有 m × n 行:

SELECT u.name, o.id AS order_id
FROM users AS u
CROSS JOIN orders AS o;

它可以用于生成日期与门店、商品与规格等组合,但结果增长很快。没有明确意图时,不要漏写 ON 条件,否则隐式笛卡尔积可能造成严重的性能和数据问题。

连接三张或更多表​

连接可以逐步扩展。假设还有商品表和订单明细表:

CREATE TABLE products (
id INTEGER PRIMARY KEY,
name VARCHAR(80) NOT NULL
);

CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders (id),
FOREIGN KEY (product_id) REFERENCES products (id)
);

查询 Alice 购买的商品时,可以把每一步关系写清楚:

SELECT
u.name AS user_name,
o.id AS order_id,
p.name AS product_name,
oi.quantity
FROM users AS u
JOIN orders AS o ON o.user_id = u.id
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON p.id = oi.product_id
WHERE u.name = 'Alice';

阅读多表连接时,可以按 FROM 到最后一个 JOIN 的顺序画出关系链:

users -> orders -> order_items -> products

每个 JOIN 都应该有明确的业务关系。若一条查询很难说明每个连接为什么存在,通常应该拆分查询或重新检查数据模型。

ON 和 WHERE 的区别​

对 INNER JOIN 来说,把部分过滤条件写在 ON 或 WHERE 中,很多时候结果相同;但对外连接来说,位置会改变结果。

下面的写法仍会保留没有订单的用户,只把金额大于 50 的订单匹配进来:

SELECT u.name, o.id AS order_id, o.amount
FROM users AS u
LEFT JOIN orders AS o
ON o.user_id = u.id
AND o.amount > 50;

如果把条件写到 WHERE:

SELECT u.name, o.id AS order_id, o.amount
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
WHERE o.amount > 50;

WHERE 会过滤掉右表为 NULL 的行,结果实际上接近 INNER JOIN,Bob 和 Carol 会消失。需要保留左表全部行时,应把右表过滤条件放进 ON,或者显式处理 NULL。

常见陷阱与排查方法​

连接列不唯一导致行数增加​

连接不是“一行对一行”的操作。一个用户有三条订单时,连接结果会产生三行用户记录;两张表分别有多条匹配记录时,结果可能按组合数量增长。

如果只想统计用户数,应使用 COUNT(DISTINCT u.id),而不是对连接结果直接 COUNT(*):

SELECT COUNT(DISTINCT u.id) AS user_count
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;

忘记限定同名列​

多张表都有 id、created_at 等列时,应使用 u.id、o.id 这样的限定名。这样可以避免“列名不明确”的错误,也能让读者知道列来自哪张表。

把外连接误写成内连接​

需要“包括没有关联记录的主体”时,选择 LEFT JOIN,并确认没有在 WHERE 中无意过滤右表的 NULL。可以先不加过滤条件查看结果行数,再逐项增加条件。

忽略 NULL 和数据质量​

连接条件 o.user_id = u.id 不会匹配 NULL。如果外键允许为空,先明确这代表“未知用户”还是“尚未关联”,再决定使用内连接还是外连接。生产查询还应检查外键约束、重复键和孤立记录。

性能不只取决于 JOIN 类型​

数据库优化器可能重排连接顺序,因此不能简单认为 LEFT JOIN 一定比 INNER JOIN 慢。更重要的是:

  • 为常用连接列和过滤列建立合适的索引,例如 orders.user_id。
  • 只选择需要的列,避免无意义的 SELECT *。
  • 用 EXPLAIN 或数据库提供的执行计划工具检查实际扫描、排序和连接方式。
  • 先用小范围条件验证结果,再处理大表上的全量查询。

一套实用的选择思路​

可以先问自己“哪一侧的行必须全部保留”:

需求通常选择
只要两边都存在的关联数据INNER JOIN
左表每一行都要显示,右表可缺失LEFT JOIN
右表每一行都要显示,左表可缺失RIGHT JOIN 或交换表后使用 LEFT JOIN
两边所有行都要显示FULL OUTER JOIN(产品支持时)
明确需要所有可能组合CROSS JOIN

连接的核心不是记住关键字,而是确认数据之间的基数(一对一、一对多、多对多)和“没有匹配时是否保留这一行”。先写出关系和预期行数,再写 SQL,通常比写完后再猜结果更可靠。

小结​

  • JOIN 依据 ON 条件把多张表的相关行组合起来;不写类型时通常是 INNER JOIN。
  • INNER JOIN 只保留匹配行,LEFT JOIN 保留左表全部行,外连接缺失的一侧用 NULL 补齐。
  • CROSS JOIN 会生成笛卡尔积,必须确认结果规模。
  • 多表连接应沿着清晰的关系链书写,并为同名列添加表别名。
  • 外连接中的过滤条件放在 ON 和 WHERE 的位置不同,可能导致完全不同的结果。
  • 面对重复行、NULL 和性能问题,应结合数据基数、索引和执行计划验证,而不是只凭关键字判断。