SQL JOIN探究
SQL JOIN 探究
1. 基本概念
平时在写 SQL 的时候,经常需要关联多张表查数据。比如在电力行业中,要查询电表的电量采集数据,就得把电表档案表和电量采集数据表关联起来。
比较常见的关联方式就下面两种:
- 内连接 ( JOIN 或 INNER JOIN ) ,只返回两表都匹配的数据。比如只返回电表档案表里有记录,并且有电量采集数据的电表。
-- 标准的内连接语法
SELECT a.*, b.*
FROM table_a a
JOIN table_b b ON a.id = b.id;
-- 还有一种等价的老式语法
SELECT a.*, b.*
FROM table_a a, table_b b
WHERE a.id = b.id;
- 左外连接 ( LEFT JOIN ) ,保留左表的所有数据,右表没匹配上的显示NULL。
-- 基础的左连接语法
SELECT a.*, b.*
FROM table_a a
LEFT JOIN table_b b ON a.id = b.id;
2. 内连接隐式 Join 跟显式 Join
内连接支持隐式连接跟显示连接,外连接正常情况下是必须使用显式连接语法的,除非是数据库方言支持。
准备表结构以及测试数据:
-- 电表档案表
CREATE TABLE meters (
meter_id varchar(20) -- 电表编号
);
-- 电表数据表
CREATE TABLE readings (
meter_id varchar(20), -- 电表编号
read_time timestamp, -- 采集时间
value decimal(10,2) -- 读到的数值
);
-- 插入测试数据
INSERT INTO meters VALUES
('M001'),
('M003'),
('M004');
INSERT INTO readings VALUES
('M001', '2019-01-01 10:00:00', 100.50),
('M001', '2019-01-01 11:00:00', 102.50),
('M002', '2019-01-01 10:00:00', 200.00), -- 注意这个表号在电表档案表中是不存在的
('M003', '2019-01-01 10:00:00', 300.75);
隐式连接:
SELECT m.meter_id, r.read_time, r.value
FROM meters m,
readings r
WHERE m.meter_id = r.meter_id;
-- 执行结果
meter_id | read_time | value
M001 | 2019-01-01 10:00:00 | 100.50
M001 | 2019-01-01 11:00:00 | 102.50
M003 | 2019-01-01 10:00:00 | 300.75
显式连接:
SELECT m.meter_id, r.read_time, r.value
FROM meters m
JOIN readings r ON m.meter_id = r.meter_id;
-- 执行结果
meter_id | read_time | value
M001 | 2019-01-01 10:00:00 | 100.50
M001 | 2019-01-01 11:00:00 | 102.50
M003 | 2019-01-01 10:00:00 | 300.75
可以看到两种写法的结果是一模一样的。分析下结果:
- M001出现两次是因为有两条采集记录。
- M002不出现是因为不在档案表中。
- M003出现一次是档案表和采集表都有数据。
- M004不出现是因为在档案表中有数据但没有采集数据。
看下执行计划:
-- 隐式连接
Hash Join (cost=26.65..170.13 rows=3478 width=82)
Hash Cond: ((m.meter_id)::text = (r.meter_id)::text)
-> Seq Scan on meters m (cost=0.00..19.40 rows=940 width=58)
-> Hash (cost=17.40..17.40 rows=740 width=82)
-> Seq Scan on readings r (cost=0.00..17.40 rows=740 width=82)
-- 显式连接
Hash Join (cost=26.65..170.13 rows=3478 width=82)
Hash Cond: ((m.meter_id)::text = (r.meter_id)::text)
-> Seq Scan on meters m (cost=0.00..19.40 rows=940 width=58)
-> Hash (cost=17.40..17.40 rows=740 width=82)
-> Seq Scan on readings r (cost=0.00..17.40 rows=740 width=82)
可以看到两种查询的查询计划也是一模一样的,这里肯定是数据库会把隐式连接转换成显式连接来处理掉了。
两种写法没有区别,但还是优先用显式连接,这样多表连接时逻辑更清晰。
3. 外连接中 ON 与 WHERE 子句过滤的坑
这里直接看两个例子。
在 ON 子句中加过滤条件:
SELECT m.meter_id, r.read_time, r.value
FROM meters m
LEFT JOIN readings r ON (m.meter_id = r.meter_id and r.value > 200);
-- 查询结果
meter_id | read_time | value
M001 | NULL | NULL -- 保留了这行
M003 | 2019-01-01 10:00:00 | 300.75
M004 | NULL | NULL -- 保留了这行
结果仍然返回所有电表的所有行数据,SQL 看起来是应该只返回 1 行的,但是实际上最后返回了 3 行。
这里的执行逻辑是先保留 meters 表所有行,对于每行检查ON条件。然后条件不满足时填充NULL,返回3行结果。
这里需要使用WHERE子句才能真正过滤行。
SELECT m.meter_id, r.read_time, r.value
FROM meters m
LEFT JOIN readings r ON m.meter_id = r.meter_id
WHERE r.value > 200;
-- 查询结果
meter_id | read_time | value
M003 | 2019-01-01 10:00:00 | 300.75
可以看到 M001 跟 M004 没有出现了, 因为对应的 value 为空或者没有 > 200。
这里的执行逻辑是先执行 LEFT JOIN ,然后再使用 WHERE 过滤,仅返回符合条件的这 1 行。
总结一下就是: 使用 ON 子句定义匹配条件,使用 WHERE 子句进行数据过滤 。