FROM
子句指定了查询其余部分操作数据的来源。从逻辑上讲,FROM
子句是查询开始执行的地方。FROM
子句可以包含单个表、使用 JOIN
子句连接的多个表的组合,或者子查询节点内的另一个 SELECT
查询。DuckDB 还提供一种可选的 FROM
-优先语法,使您无需 SELECT
语句即可查询。
示例
从名为 table_name
的表中选择所有列
SELECT *
FROM table_name;
使用 FROM
-优先语法从表中选择所有列
FROM table_name
SELECT *;
使用 FROM
-优先语法并省略 SELECT
子句来选择所有列
FROM table_name;
通过别名 tn
从名为 table_name
的表中选择所有列
SELECT tn.*
FROM table_name tn;
使用前缀别名
SELECT tn.*
FROM tn: table_name;
从模式 schema_name
中的表 table_name
中选择所有列
SELECT *
FROM schema_name.table_name;
从表函数 range
中选择列 i
,其中 range 函数的第一列被重命名为 i
SELECT t.i
FROM range(100) AS t(i);
从名为 test.csv
的 CSV 文件中选择所有列
SELECT *
FROM 'test.csv';
从子查询中选择所有列
SELECT *
FROM (SELECT * FROM table_name);
将表的整行作为结构体选择
SELECT t
FROM t;
将子查询的整行作为结构体选择(即,单列)
SELECT t
FROM (SELECT unnest(generate_series(41, 43)) AS x, 'hello' AS y) t;
连接两个表
SELECT *
FROM table_name
JOIN other_table
ON table_name.key = other_table.key;
从表中选择 10% 的样本
SELECT *
FROM table_name
TABLESAMPLE 10%;
从表中选择 10 行的样本
SELECT *
FROM table_name
TABLESAMPLE 10 ROWS;
将 FROM
-优先语法与 WHERE
子句和聚合一起使用
FROM range(100) AS t(i)
SELECT sum(t.i)
WHERE i % 2 = 0;
连接
连接是一种基本的关系操作,用于水平连接两个表或关系。这些关系根据它们在连接子句中的写法,被称为连接的左侧和右侧。每个结果行都包含来自两个关系的列。
连接使用规则来匹配每个关系中的行对。通常这是一个谓词,但也可能指定其他隐含规则。
外部连接
如果指定了 OUTER
连接,没有匹配项的行仍然可以返回。外部连接可以是以下之一:
LEFT
(左关系中的所有行至少出现一次)RIGHT
(右关系中的所有行至少出现一次)FULL
(两个关系中的所有行至少出现一次)
不是 OUTER
的连接是 INNER
(只返回配对的行)。
当返回未配对的行时,来自另一个表的属性被设置为 NULL
。
交叉乘积连接(笛卡尔积)
最简单的连接类型是 CROSS JOIN
。这种连接没有条件,它只返回所有可能的配对。
返回所有行对
SELECT a.*, b.*
FROM a
CROSS JOIN b;
这等同于省略 JOIN
子句
SELECT a.*, b.*
FROM a, b;
条件连接
大多数连接通过一个谓词来指定,该谓词将一侧的属性连接到另一侧的属性。条件可以使用连接中的 ON
子句明确指定(更清晰),或者由 WHERE
子句隐含(旧式)。
我们使用 TPC-H 模式中的 l_regions
和 l_nations
表
CREATE TABLE l_regions (
r_regionkey INTEGER NOT NULL PRIMARY KEY,
r_name CHAR(25) NOT NULL,
r_comment VARCHAR(152)
);
CREATE TABLE l_nations (
n_nationkey INTEGER NOT NULL PRIMARY KEY,
n_name CHAR(25) NOT NULL,
n_regionkey INTEGER NOT NULL,
n_comment VARCHAR(152),
FOREIGN KEY (n_regionkey) REFERENCES l_regions(r_regionkey)
);
返回国家的区域
SELECT n.*, r.*
FROM l_nations n
JOIN l_regions r ON (n_regionkey = r_regionkey);
如果列名相同且要求相等,则可以使用更简单的 USING
语法
CREATE TABLE l_regions (regionkey INTEGER NOT NULL PRIMARY KEY,
name CHAR(25) NOT NULL,
comment VARCHAR(152));
CREATE TABLE l_nations (nationkey INTEGER NOT NULL PRIMARY KEY,
name CHAR(25) NOT NULL,
regionkey INTEGER NOT NULL,
comment VARCHAR(152),
FOREIGN KEY (regionkey) REFERENCES l_regions(regionkey));
返回国家的区域
SELECT n.*, r.*
FROM l_nations n
JOIN l_regions r USING (regionkey);
表达式不一定是等式——可以使用任何谓词
返回运行时间更长但成本更低的作业对
SELECT s1.t_id, s2.t_id
FROM west s1, west s2
WHERE s1.time > s2.time
AND s1.cost < s2.cost;
自然连接
自然连接基于共享相同名称的属性连接两个表。
例如,以下是包含城市、机场代码和机场名称的示例。请注意,这两个表都是故意不完整的,即它们在另一个表中没有匹配的对。
CREATE TABLE city_airport (city_name VARCHAR, iata VARCHAR);
CREATE TABLE airport_names (iata VARCHAR, airport_name VARCHAR);
INSERT INTO city_airport VALUES
('Amsterdam', 'AMS'),
('Rotterdam', 'RTM'),
('Eindhoven', 'EIN'),
('Groningen', 'GRQ');
INSERT INTO airport_names VALUES
('AMS', 'Amsterdam Airport Schiphol'),
('RTM', 'Rotterdam The Hague Airport'),
('MST', 'Maastricht Aachen Airport');
要基于共享的 IATA
属性连接这些表,请运行
SELECT *
FROM city_airport
NATURAL JOIN airport_names;
这将产生以下结果
city_name | iata | airport_name |
---|---|---|
Amsterdam | AMS | Amsterdam Airport Schiphol |
Rotterdam | RTM | Rotterdam The Hague Airport |
请注意,结果中只包含 iata
属性同时存在于两个表中的行。
我们也可以使用带有 USING
关键字的普通 JOIN
子句来表达查询
SELECT *
FROM city_airport
JOIN airport_names
USING (iata);
半连接和反连接
半连接返回左表中至少在右表中有一次匹配的行。反连接返回左表中在右表中没有匹配的行。当使用半连接或反连接时,结果行的数量绝不会超过左侧表。半连接提供与 IN
运算符语句相同的逻辑。反连接提供与 NOT IN
运算符相同的逻辑,只是反连接会忽略右表中的 NULL
值。
半连接示例
返回 city_airport
表中城市-机场代码对的列表,其中机场名称在 airport_names
表中可用
SELECT *
FROM city_airport
SEMI JOIN airport_names
USING (iata);
city_name | iata |
---|---|
Amsterdam | AMS |
Rotterdam | RTM |
此查询等效于
SELECT *
FROM city_airport
WHERE iata IN (SELECT iata FROM airport_names);
反连接示例
返回 city_airport
表中城市-机场代码对的列表,其中机场名称在 airport_names
表中不可用
SELECT *
FROM city_airport
ANTI JOIN airport_names
USING (iata);
city_name | iata |
---|---|
Eindhoven | EIN |
Groningen | GRQ |
此查询等效于
SELECT *
FROM city_airport
WHERE iata NOT IN (SELECT iata FROM airport_names WHERE iata IS NOT NULL);
横向连接
LATERAL
关键字允许 FROM
子句中的子查询引用先前的子查询。此功能也称为横向连接。
SELECT *
FROM range(3) t(i), LATERAL (SELECT i + 1) t2(j);
i | j |
---|---|
0 | 1 |
2 | 3 |
1 | 2 |
横向连接是相关子查询的泛化,因为它们可以为每个输入值返回多个值,而不仅仅是一个值。
SELECT *
FROM
generate_series(0, 1) t(i),
LATERAL (SELECT i + 10 UNION ALL SELECT i + 100) t2(j);
i | j |
---|---|
0 | 10 |
1 | 11 |
0 | 100 |
1 | 101 |
将 LATERAL
视为一个循环可能会有所帮助,在该循环中我们遍历第一个子查询的行,并将其用作第二个(LATERAL
)子查询的输入。在上述示例中,我们遍历表 t
,并从表 t2
的定义中引用其列 i
。t2
的行在结果中形成列 j
。
可以从 LATERAL
子查询中引用多个属性。使用第一个示例中的表
CREATE TABLE t1 AS
SELECT *
FROM range(3) t(i), LATERAL (SELECT i + 1) t2(j);
SELECT *
FROM t1, LATERAL (SELECT i + j) t2(k)
ORDER BY ALL;
i | j | k |
---|---|---|
0 | 1 | 1 |
1 | 2 | 3 |
2 | 3 | 5 |
DuckDB 会检测何时应使用
LATERAL
连接,因此LATERAL
关键字的使用是可选的。
位置连接
当处理大小相同的数据帧或其他嵌入式表时,行可能基于其物理顺序具有自然的对应关系。在脚本语言中,这很容易使用循环来表达
for (i = 0; i < n; i++) {
f(t1.a[i], t2.b[i]);
}
这在标准 SQL 中很难表达,因为关系表是无序的,但导入的表(如 数据帧 或磁盘文件(如 CSV 或 Parquet 文件))确实具有自然顺序。
使用此顺序连接它们称为位置连接:
CREATE TABLE t1 (x INTEGER);
CREATE TABLE t2 (s VARCHAR);
INSERT INTO t1 VALUES (1), (2), (3);
INSERT INTO t2 VALUES ('a'), ('b');
SELECT *
FROM t1
POSITIONAL JOIN t2;
x | s |
---|---|
1 | a |
2 | b |
3 | NULL |
位置连接始终是 FULL OUTER
连接,即缺失值(较短列中的最后一个值)被设置为 NULL
。
As-Of 连接
在处理时间或类似顺序的数据时,常见的操作是在参考表(如价格)中查找最近(第一个)事件。这称为As-Of 连接:
将价格附加到股票交易
SELECT t.*, p.price
FROM trades t
ASOF JOIN prices p
ON t.symbol = p.symbol AND t.when >= p.when;
ASOF
连接要求排序字段上至少有一个不相等条件。不相等可以是任何数据类型上的任何不相等条件(>=
, >
, <=
, <
),但最常见的形式是在时间类型上的 >=
。任何其他条件必须是等式(或 NOT DISTINCT
)。这意味着表的左右顺序很重要。
ASOF
连接将左侧的每行与右侧的最多一行连接。它可以指定为 OUTER
连接,以查找未配对的行(例如,没有价格的交易或没有交易的价格)。
将价格或 NULL 附加到股票交易
SELECT *
FROM trades t
ASOF LEFT JOIN prices p
ON t.symbol = p.symbol
AND t.when >= p.when;
ASOF
连接也可以使用 USING
语法指定匹配列名上的连接条件,但列表中的最后一个属性必须是不相等条件,即大于或等于(>=
)
SELECT *
FROM trades t
ASOF JOIN prices p USING (symbol, "when");
返回 symbol, trades.when, price(但不是 prices.when)
如果像这样将 USING
与 SELECT *
结合使用,查询将返回匹配项的左侧(探测)列值,而不是右侧(构建)列值。要在示例中获取 prices
时间,您需要明确列出这些列
SELECT t.symbol, t.when AS trade_when, p.when AS price_when, price
FROM trades t
ASOF LEFT JOIN prices p USING (symbol, "when");
自连接
DuckDB 允许所有类型的自连接。请注意,表需要使用别名,不使用别名而直接使用相同表名将导致错误
CREATE TABLE t (x INTEGER);
SELECT * FROM t JOIN t USING(x);
Binder Error:
Duplicate alias "t" in query!
添加别名后,查询可以成功解析
SELECT * FROM t AS t t1 JOIN t t2 USING(x);
JOIN
子句中的简写
您可以在 JOIN
子句中指定列名
CREATE TABLE t1 (x INTEGER);
CREATE TABLE t2 (y INTEGER);
INSERT INTO t1 VALUES (1), (2), (4);
INSERT INTO t2 VALUES (2), (3);
SELECT * FROM t1 NATURAL JOIN t2 t2(x);
x |
---|
2 |
您还可以在 JOIN
子句中使用 VALUES
子句
SELECT * FROM t1 NATURAL JOIN (VALUES (2), (4)) _(x);
x |
---|
2 |
4 |
FROM
-优先语法
DuckDB 的 SQL 支持 FROM
-优先语法,即它允许将 FROM
子句放在 SELECT
子句之前,或完全省略 SELECT
子句。我们使用以下示例进行演示
CREATE TABLE tbl AS
SELECT *
FROM (VALUES ('a'), ('b')) t1(s), range(1, 3) t2(i);
带有 SELECT
子句的 FROM
-优先语法
以下语句演示了 FROM
-优先语法的用法
FROM tbl
SELECT i, s;
这等同于
SELECT i, s
FROM tbl;
i | s |
---|---|
1 | a |
2 | a |
1 | b |
2 | b |
不带 SELECT
子句的 FROM
-优先语法
以下语句演示了可选 SELECT
子句的用法
FROM tbl;
这等同于
SELECT *
FROM tbl;
s | i |
---|---|
a | 1 |
a | 2 |
b | 1 |
b | 2 |
使用 AT
进行时间旅行
DuckDB v1.3.0 引入了对 时间旅行查询 的支持,适用于 Delta、DuckLake 和 Iceberg 等数据湖格式。
要指定时间旅行查询,请在 FROM
子句中使用 AT
修饰符。时间旅行查询可以使用版本号或时间戳,分别通过 VERSION => version
和 TIMESTAMP => timestamp or date
指定。例如
FROM my_ducklake.demo AT (VERSION => 2);
FROM my_ducklake.demo AT (TIMESTAMP => DATE '2025-05-26');