表之间的连接
在关系型数据库中,数据通常按主题拆分到多个表中。例如,用户信息放在 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)
);
users | orders | |||
|---|---|---|---|---|
id | name | id | user_id | amount |
| 1 | Alice | 101 | 1 | 99.00 |
| 2 | Bob | 102 | 1 | 25.00 |
| 3 | Carol | NULL | NULL | NULL |
先理解连接条件
最基本的连接是根据两张表中表示同一关系的列进行匹配:
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 是连接条件。它逐行比较两张表:当条件为真时,才把两行拼成一行。查询结果是:
name | order_id | amount |
|---|---|---|
| Alice | 101 | 99.00 |
| Alice | 102 | 25.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;
结果会包含没有订单的用户:
name | order_id | amount |
|---|---|---|
| Alice | 101 | 99.00 |
| Alice | 102 | 25.00 |
| Bob | NULL | NULL |
| Carol | NULL | NULL |
因此,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和性能问题,应结合数据基数、索引和执行计划验证,而不是只凭关键字判断。