⌘+k ctrl+k
1.3 (稳定版)
搜索快捷键 cmd + k | ctrl + k
FROM 和 JOIN 子句

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_regionsl_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 的定义中引用其列 it2 的行在结果中形成列 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 中很难表达,因为关系表是无序的,但导入的表(如 数据帧 或磁盘文件(如 CSVParquet 文件))确实具有自然顺序。

使用此顺序连接它们称为位置连接

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)

如果像这样将 USINGSELECT * 结合使用,查询将返回匹配项的左侧(探测)列值,而不是右侧(构建)列值。要在示例中获取 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 引入了对 时间旅行查询 的支持,适用于 DeltaDuckLakeIceberg 等数据湖格式。

要指定时间旅行查询,请在 FROM 子句中使用 AT 修饰符。时间旅行查询可以使用版本号或时间戳,分别通过 VERSION => versionTIMESTAMP => timestamp or date 指定。例如

FROM my_ducklake.demo AT (VERSION => 2);
FROM my_ducklake.demo AT (TIMESTAMP => DATE '2025-05-26');

语法