子查询是作为更大的外层查询的一部分出现的带括号的查询表达式。子查询通常基于 SELECT ... FROM,但在 DuckDB 中,其他查询结构(例如 PIVOT)也可以作为子查询出现。
标量子查询
标量子查询是返回单个值的子查询。它们可以用在任何可以使用表达式的地方。如果标量子查询返回多于一个值,则会引发错误(除非将 scalar_subquery_error_on_multiple_rows 设置为 false,在这种情况下会随机选择一行)。
考虑以下表格
Grades
| grade | course |
|---|---|
| 7 | Math |
| 9 | Math |
| 8 | CS |
CREATE TABLE grades (grade INTEGER, course VARCHAR);
INSERT INTO grades VALUES (7, 'Math'), (9, 'Math'), (8, 'CS');
我们可以运行以下查询来获取最低成绩
SELECT min(grade) FROM grades;
| min(grade) |
|---|
| 7 |
通过在 WHERE 子句中使用标量子查询,我们可以找出该成绩是针对哪门课程获得的
SELECT course FROM grades WHERE grade = (SELECT min(grade) FROM grades);
| course |
|---|
| Math |
ARRAY 子查询
返回多个值的子查询可以使用 ARRAY 进行包装,以将所有结果收集到一个列表中。
SELECT ARRAY(SELECT grade FROM grades) AS all_grades;
| all_grades |
|---|
| [7, 9, 8] |
子查询比较:ALL、ANY 和 SOME
在关于 标量子查询 的章节中,标量表达式直接使用相等 比较运算符 (=) 与子查询进行比较。这种直接比较仅在标量子查询中才有意义。
通过指定量词,标量表达式仍然可以与返回多行的单列子查询进行比较。可用的量词有 ALL、ANY 和 SOME。量词 ANY 和 SOME 是等价的。
ALL
ALL 量词指定:当比较运算符左侧的表达式与比较运算符右侧子查询中的每个值进行逐项比较,且结果全部为 true 时,整个比较表达式的结果才为 true。
SELECT 6 <= ALL (SELECT grade FROM grades) AS adequate;
返回
| adequate |
|---|
| true |
因为 6 小于或等于子查询结果 7、8 和 9 中的每一个。
但是,以下查询
SELECT 8 >= ALL (SELECT grade FROM grades) AS excellent;
返回
| excellent |
|---|
| false |
因为 8 不大于或等于子查询结果 7。因此,由于并非所有比较结果都为 true,整个 >= ALL 表达式的结果为 false。
ANY
ANY 量词指定:当至少有一个逐项比较的结果为 true 时,整个比较表达式的结果为 true。例如:
SELECT 5 >= ANY (SELECT grade FROM grades) AS fail;
返回
| fail |
|---|
| false |
因为子查询中没有结果小于或等于 5。
量词 SOME 可以代替 ANY 使用:ANY 和 SOME 是可以互换的。
EXISTS
EXISTS 运算符用于检测子查询内是否存在任何行。当子查询返回一条或多条记录时,它返回 true,否则返回 false。EXISTS 运算符通常作为相关子查询使用,以表达半连接(semijoin)操作。不过,它也可以作为非相关子查询使用。
例如,我们可以使用它来查明给定课程是否存在成绩
SELECT EXISTS (FROM grades WHERE course = 'Math') AS math_grades_present;
| math_grades_present |
|---|
| true |
SELECT EXISTS (FROM grades WHERE course = 'History') AS history_grades_present;
| history_grades_present |
|---|
| false |
上述示例中的子查询利用了 DuckDB 可以省略
SELECT *的特性,这得益于FROM-优先语法。在其他 SQL 系统中,子查询中必须包含SELECT子句,但在EXISTS和NOT EXISTS子查询中,SELECT子句没有任何实际作用。
NOT EXISTS
NOT EXISTS 运算符用于检测子查询内是否没有任何行。当子查询返回空结果时,它返回 true,否则返回 false。NOT EXISTS 运算符通常作为相关子查询使用,以表达反连接(antijoin)操作。例如,查找没有兴趣点的 Person 节点:
CREATE TABLE Person (id BIGINT, name VARCHAR);
CREATE TABLE interest (PersonId BIGINT, topic VARCHAR);
INSERT INTO Person VALUES (1, 'Jane'), (2, 'Joe');
INSERT INTO interest VALUES (2, 'Music');
SELECT *
FROM Person
WHERE NOT EXISTS (FROM interest WHERE interest.PersonId = Person.id);
| id | name |
|---|---|
| 1 | Jane |
DuckDB 会自动检测
NOT EXISTS查询何时表示反连接操作。无需手动将此类查询重写为LEFT OUTER JOIN ... WHERE ... IS NULL。
IN 运算符
IN 运算符用于检查左侧表达式是否包含在子查询定义的集合或右侧(RHS)的一组表达式中。如果表达式存在于右侧,则 IN 运算符返回 true;如果表达式不在右侧且右侧没有 NULL 值,则返回 false;如果表达式不在右侧但右侧包含 NULL 值,则返回 NULL。
我们可以以类似于使用 EXISTS 运算符的方式使用 IN 运算符。
SELECT 'Math' IN (SELECT course FROM grades) AS math_grades_present;
| math_grades_present |
|---|
| true |
相关子查询
到目前为止所介绍的所有子查询都是非相关子查询,这些子查询完全自包含,可以在没有父查询的情况下运行。存在第二种类型的子查询,称为相关子查询。对于相关子查询,子查询会使用父查询中的值。
从概念上讲,子查询针对父查询中的每一行运行一次。一个简单的设想方法是:相关子查询是一个应用于源数据集中每一行的函数。
例如,假设我们想要查找每门课程的最低成绩。我们可以这样做:
SELECT *
FROM grades grades_parent
WHERE grade =
(SELECT min(grade)
FROM grades
WHERE grades.course = grades_parent.course);
| grade | course |
|---|---|
| 7 | Math |
| 8 | CS |
子查询使用了来自父查询的列(grades_parent.course)。从概念上讲,我们可以将子查询视为一个函数,其中相关列是该函数的参数。
SELECT min(grade)
FROM grades
WHERE course = ?;
现在,当我们为每一行执行此函数时,可以看到对于 Math,它将返回 7,而对于 CS,它将返回 8。然后我们将其与该行的实际成绩进行比较。结果,行 (Math, 9) 将被过滤掉,因为 9 <> 7。
将子查询的每一行作为结构体(Struct)返回
在 SELECT 子句中使用子查询名称(而不引用特定列)会将子查询的每一行转换为一个结构体,其字段对应于子查询的列。例如:
SELECT t
FROM (SELECT unnest(generate_series(41, 43)) AS x, 'hello' AS y) t;
| t |
|---|
| {'x': 41, 'y': hello} |
| {'x': 42, 'y': hello} |
| {'x': 43, 'y': hello} |