子查询是作为更大、外部查询的一部分出现的括号内的查询表达式。子查询通常基于SELECT ... FROM,但在DuckDB中,其他查询结构如PIVOT也可以作为子查询出现。
标量子查询
标量子查询是返回单个值的子查询。它们可以在任何可以使用表达式的地方使用。如果标量子查询返回多个值,则会引发错误(除非scalar_subquery_error_on_multiple_rows设置为false,在这种情况下会随机选择一行)。
考虑以下表格:
成绩
| 成绩 | 课程 |
|---|---|
| 7 | 数学 |
| 9 | 数学 |
| 8 | CS |
CREATE TABLE grades (grade INTEGER, course VARCHAR);
INSERT INTO grades VALUES (7, 'Math'), (9, 'Math'), (8, 'CS');
我们可以运行以下查询来获取最低成绩:
SELECT min(grade) FROM grades;
| 最低成绩 |
|---|
| 7 |
通过在WHERE子句中使用标量子查询,我们可以找出这个成绩是为哪门课程获得的:
SELECT course FROM grades WHERE grade = (SELECT min(grade) FROM grades);
| 课程 |
|---|
| 数学 |
子查询比较:ALL, ANY 和 SOME
在标量子查询部分中,标量表达式直接与使用相等比较运算符(=)的子查询进行了比较。这种直接比较仅在标量子查询中有意义。
标量表达式仍然可以通过指定量词与返回多行的单列子查询进行比较。可用的量词是ALL、ANY和SOME。量词ANY和SOME是等价的。
ALL
ALL 限定符指定当比较运算符左侧的表达式与比较运算符右侧的子查询中的每个值的单独比较结果全部评估为true时,整个比较评估为true:
SELECT 6 <= ALL (SELECT grade FROM grades) AS adequate;
返回:
| 足够的 |
|---|
| true |
因为6小于或等于子查询结果7、8和9中的每一个。
然而,以下查询
SELECT 8 >= ALL (SELECT grade FROM grades) AS excellent;
返回
| 优秀 |
|---|
| false |
因为8不大于或等于子查询结果7。因此,由于并非所有比较都评估为true,>= ALL整体上评估为false。
ANY
ANY 量词指定当至少一个单独的比较结果评估为 true 时,整个比较评估为 true。
例如:
SELECT 5 >= ANY (SELECT grade FROM grades) AS fail;
返回
| 失败 |
|---|
| false |
因为子查询的结果没有小于或等于5的。
量词 SOME 可以用来代替 ANY:ANY 和 SOME 是可以互换的。
EXISTS
EXISTS 操作符用于测试子查询中是否存在任何行。当子查询返回一个或多个记录时,它返回 true,否则返回 false。EXISTS 操作符通常作为相关子查询来表达半连接操作时最为有用。然而,它也可以用作非相关子查询。
例如,我们可以用它来找出某个课程是否有任何成绩:
SELECT EXISTS (FROM grades WHERE course = 'Math') AS math_grades_present;
| 数学成绩存在 |
|---|
| true |
SELECT EXISTS (FROM grades WHERE course = 'History') AS history_grades_present;
| 历史成绩展示 |
|---|
| false |
上述示例中的子查询利用了在DuckDB中可以省略
SELECT *的事实,这要归功于FROM-first语法。在其他SQL系统中,子查询中需要SELECT子句,但在EXISTS和NOT EXISTS子查询中无法实现任何目的。
NOT EXISTS
NOT EXISTS 操作符用于测试子查询中是否没有任何行。当子查询返回空结果时,它返回 true,否则返回 false。NOT EXISTS 操作符通常作为相关子查询来表达反连接操作最为有用。例如,查找没有兴趣的 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 | 简 |
DuckDB 自动检测当
NOT EXISTS查询表达反连接操作时。无需手动重写此类查询以使用LEFT OUTER JOIN ... WHERE ... IS NULL。
IN 操作符
IN 操作符检查左侧表达式是否包含在子查询或右侧表达式集(RHS)定义的结果中。如果表达式存在于 RHS 中,IN 操作符返回 true;如果表达式不在 RHS 中且 RHS 没有 NULL 值,则返回 false;如果表达式不在 RHS 中且 RHS 包含 NULL 值,则返回 NULL。
我们可以以类似于使用EXISTS操作符的方式使用IN操作符:
SELECT 'Math' IN (SELECT course FROM grades) AS math_grades_present;
| 数学成绩存在 |
|---|
| true |
相关子查询
到目前为止,这里展示的所有子查询都是非相关子查询,这些子查询本身是完全独立的,可以在没有父查询的情况下运行。存在第二种类型的子查询,称为相关子查询。对于相关子查询,子查询使用来自父查询的值。
从概念上讲,子查询为父查询中的每一行运行一次。也许一个简单的想象方式是,相关子查询是一个应用于源数据集中每一行的函数。
例如,假设我们想要找到每门课程的最低分数。我们可以按照以下方式操作:
SELECT *
FROM grades grades_parent
WHERE grade =
(SELECT min(grade)
FROM grades
WHERE grades.course = grades_parent.course);
| 成绩 | 课程 |
|---|---|
| 7 | 数学 |
| 8 | CS |
子查询使用了父查询中的一列(grades_parent.course)。从概念上讲,我们可以将子查询视为一个函数,其中相关列是该函数的参数:
SELECT min(grade)
FROM grades
WHERE course = ?;
现在当我们为每一行执行这个函数时,我们可以看到对于Math,这将返回7,而对于CS,它将返回8。然后我们将其与实际行的成绩进行比较。结果,行(Math, 9)将被过滤掉,因为9 <> 7。
将子查询的每一行作为结构体返回
在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} |