到目前为止,我们的查询每次都只访问单张表。实际上,SQL 可以在单次查询中同时访问多张表,或者在同一张表内部同时比对多行数据。一次性访问多张表(或同一张表的多个镜像实例)的查询被称为连接(join)查询。连接操作通过指定连接表达式,将一张表中的数据行与另一张表中的数据行进行配对关联。例如,要将天气记录与对应城市的空间地理坐标一并返回,数据库需要将 weather 表每行的 city 字段与 cities 表每行的 name 字段进行比对,并筛选出两者匹配的行对。[4]
可以通过以下 SQL 语句实现连接:
SELECT * FROM weather JOIN cities ON city = name;
city | temp_lo | temp_hi | prcp | date | name | location
---------------+---------+---------+------+------------+---------------+-----------
San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | (-194,53)
San Francisco | 43 | 57 | 0 | 1994-11-29 | San Francisco | (-194,53)
(2 rows)
仔细观察上述返回结果集,可以发现两点:
结果集中没有包含 Hayward 市的数据。这是因为 cities 表中尚未录入 Hayward 的城市记录,因此标准连接直接忽略了 weather 表中未匹配上的孤立行。稍后我们将介绍如何解决这一问题。
输出结果中有两列都显示了城市名。这是符合预期的,因为 weather 与 cities 两张表的字段列表被直接水平拼接。在实际生产中通常不希望出现这种重复展示,因此推荐显式罗列所需的返回列,而不是使用通配符 *:
SELECT city, temp_lo, temp_hi, prcp, date, location
FROM weather JOIN cities ON city = name;
由于两张表中字段名称各不相同,SQL 解析器能够自动识别各字段归属于哪张表。如果两张表中存在重名的列名,你必须对列名加上表前缀进行限定(qualify),以明确指定引用来源:
SELECT weather.city, weather.temp_lo, weather.temp_hi,
weather.prcp, weather.date, cities.location
FROM weather JOIN cities ON weather.city = cities.name;
在编写连接查询时,显式为所有列带上表名或别名前缀是一种备受推崇的规范做法,这能防止未来若在某张表中新增了同名字段而导致既有 SQL 抛出字段歧义错误。
上述连接查询也可以采用如下传统语法书写:
SELECT *
FROM weather, cities
WHERE city = name;
这种语法早在 SQL-92 引入显式 JOIN/ON 语法之前就已经存在。它直接在 FROM 子句中平铺表名,并在 WHERE 子句中追加等值比对条件。这种较老的隐式连接与现代显式 JOIN/ON 的底层执行结果完全一致。但对于阅读代码的人来说,现代显式语法结构更加清晰:连接匹配条件由专用的关键字引入,与 WHERE 中普通的业务数据过滤条件清晰解耦。
现在我们来看如何让没有匹配记录的 Hayward 数据也出现在结果集中。我们希望查询扫描整张 weather 表,并尝试寻找对应的 cities 匹配行;若未找到匹配,则在结果中将 cities 表对应的字段填充为“空值”(Null)。这种查询被称为外连接(outer join,此前展示的连接均为内连接 inner join)。其语法如下:
SELECT *
FROM weather LEFT OUTER JOIN cities ON weather.city = cities.name;
city | temp_lo | temp_hi | prcp | date | name | location
---------------+---------+---------+------+------------+---------------+-----------
Hayward | 37 | 54 | | 1994-11-29 | |
San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | (-194,53)
San Francisco | 43 | 57 | 0 | 1994-11-29 | San Francisco | (-194,53)
(3 rows)
这种查询被称为左外连接(left outer join),因为位于连接操作符左侧的表(左表),其所有数据行都保证至少在结果中出现一次;而右侧的表(右表)则只输出与左表存在匹配的数据行。当输出某条没有右表匹配的左表记录时,右表对应的各个列将自动以 NULL 填充。
思考练习:. SQL 中还存在右外连接(right outer join)与全外连接(full outer join)。不妨思考并测试它们分别代表什么行为。
我们还可以将一张表与自身进行连接,这被称为自连接(self join)。举个例子,假设我们希望找出所有气温范围完全覆盖了其他天气记录的数据行。我们需要将每条天气记录的最低气温和最高气温,与表中所有其他行的气温范围进行比对:
SELECT w1.city, w1.temp_lo AS low, w1.temp_hi AS high,
w2.city, w2.temp_lo AS low, w2.temp_hi AS high
FROM weather w1 JOIN weather w2
ON w1.temp_lo < w2.temp_lo AND w1.temp_hi > w2.temp_hi;
city | low | high | city | low | high
---------------+-----+------+---------------+-----+------
San Francisco | 43 | 57 | San Francisco | 46 | 50
Hayward | 37 | 54 | San Francisco | 46 | 50
(2 rows)
此处我们分别为天气表指定了别名 w1 与 w2,从而能够清晰区分连接的左右两侧。表别名也可以广泛应用于其他日常查询中,以精简代码书写:
SELECT *
FROM weather w JOIN cities c ON w.city = c.name;
在实际工程中你将会频繁见到这种简写模式。