DuckDB 90% 兼容 PostgreSQL,剩下的 10% 是它自己的扩展——多数是”用过就回不去”的语法糖。写惯 DuckDB 再回 PG 会反复想念 QUALIFY 和 GROUP BY ALL。
这篇按实操频率列出 12 个最值得记的方言扩展,每个配 SQL 例子。
1. QUALIFY:窗口函数的 HAVING
问题:取每个用户最近 3 条订单。
传统 SQL(PG / MySQL / SQLite):
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC
) AS rn
FROM orders
) WHERE rn <= 3;
DuckDB QUALIFY:
SELECT * FROM orders
QUALIFY ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC
) <= 3;
类比:QUALIFY 之于窗口函数 = HAVING 之于聚合 = WHERE 之于行。
常用模式:
| 需求 | QUALIFY 写法 |
|---|---|
| 每组 top N | QUALIFY RANK() OVER (...) <= N |
| 去重保留最新 | QUALIFY ROW_NUMBER() OVER (... ORDER BY ts DESC) = 1 |
| 异常值 | QUALIFY ABS(x - AVG(x) OVER ()) > 3 * STDDEV(x) OVER () |
| 第二高 | QUALIFY DENSE_RANK() OVER (ORDER BY x DESC) = 2 |
PostgreSQL 16+ 部分支持,MySQL / SQLite 不支持。
2. GROUP BY ALL / ORDER BY ALL
按 SELECT 里所有非聚合列自动分组:
-- 传统
SELECT region, category, quarter, SUM(amount)
FROM orders
GROUP BY region, category, quarter;
-- DuckDB
SELECT region, category, quarter, SUM(amount)
FROM orders
GROUP BY ALL;
优点:DRY,加列时只改一处。
陷阱:
- SELECT 里多加一列就被自动加进 GROUP BY,可能改变结果集行数
- 团队代码可读性差——长查询不容易看出按什么分组
- 跨数据库不兼容(仅 Snowflake / BigQuery / DuckDB / Databricks 支持)
实务:ad-hoc 探索用 ALL 飞起,生产 ETL 代码写明确列名。
3. SELECT * EXCLUDE / REPLACE
不用列出几十个列名:
-- 排除敏感列
SELECT * EXCLUDE (password, secret_token) FROM users;
-- 替换几列的表达式
SELECT * REPLACE (
upper(name) AS name,
amount * 1.13 AS amount
) FROM orders;
-- 组合
SELECT * EXCLUDE (password) REPLACE (lower(email) AS email)
FROM users;
JOIN 时妙用:
SELECT * EXCLUDE (cust_id)
FROM orders o JOIN customers c USING (cust_id);
USING 自动合并 cust_id,配合 EXCLUDE 排除冗余字段。
4. COLUMNS 表达式:正则选列
SELECT COLUMNS('.*_at') FROM events;
-- 选所有以 _at 结尾的列
SELECT COLUMNS('amount_.*'), SUM(COLUMNS('amount_.*'))
FROM transactions;
-- 对所有 amount_ 开头的列分别求和
5. lambda + LIST 函数
数组上做 map / filter / reduce,避免 UNNEST:
-- 保留 > 100 的元素
SELECT list_filter([50, 120, 80, 200], x -> x > 100);
-- [120, 200]
-- 每个元素 ×2
SELECT list_transform([1, 2, 3], x -> x * 2);
-- [2, 4, 6]
-- 折叠求和
SELECT list_reduce([1, 2, 3, 4], (acc, x) -> acc + x);
-- 10
实战:
-- 每个用户金额 > 100 的订单数
SELECT user_id, length(list_filter(amounts, x -> x > 100)) AS big_orders
FROM (
SELECT user_id, list(amount) AS amounts
FROM orders GROUP BY user_id
);
注意:DuckDB 数组1-indexed(arr[1] 取第一个),切片 arr[1:3]。
6. UNNEST:数组展开成行
SELECT unnest([1, 2, 3]);
-- 输出 3 行:1、2、3
SELECT id, unnest(tags) AS tag FROM products;
-- 每个 tag 展开一行
SELECT unnest(range(1, 100));
-- 生成 1-99 的序列
7. STRUCT / LIST / MAP:嵌套类型
DuckDB 把嵌套类型当一等公民:
-- STRUCT 字段访问
SELECT order_id, items.product_name, items.price
FROM orders, unnest(line_items) AS t(items);
-- 创建 STRUCT
SELECT {name: 'Alice', age: 30} AS person;
-- 访问
SELECT person.name, person['age'] FROM users;
何时用嵌套类型 vs 关联表:
| 场景 | 选择 |
|---|---|
| 不变 / 只读复杂结构(订单 line_items) | STRUCT / LIST |
| 频繁更新的子实体(订单状态历史) | 关联表 |
| 多对多关系 | 关联表 + JOIN |
| 半结构化数据 | JSON 字段 + JSON 函数 |
8. 表函数:read_csv_auto / read_parquet / read_json
直接把文件当表查,不需要 LOAD DATA:
-- 自动推断 schema
SELECT * FROM read_csv_auto('data.csv') WHERE date >= '2026-01-01';
-- 通配符多文件
SELECT COUNT(*) FROM read_parquet('s3://bucket/year=2026/*.parquet');
-- JSON 数组
SELECT * FROM read_json_auto('events.jsonl');
-- 远程 URL
SELECT * FROM read_csv_auto('https://example.com/data.csv');
自动 schema 推断准吗:
- 多数情况准,扫前 100-1000 行猜类型
- 含 NULL 多的列可能猜错,用
columns={'name':'VARCHAR'}显式指定 - 超大文件可能漏掉后面的怪数据,用
auto_detect=false+ 显式 schema
9. PIVOT / UNPIVOT:宽表 / 长表互转
-- 长表转宽表(按 quarter 展开)
PIVOT sales ON quarter USING SUM(amount) GROUP BY region;
-- 宽表转长表
UNPIVOT sales ON q1, q2, q3, q4 INTO NAME quarter VALUE amount;
PostgreSQL 没有这个,只能用 CASE WHEN + GROUP BY 模拟。
10. DESCRIBE / SUMMARIZE / SHOW
DESCRIBE my_table;
-- 列出所有列名 + 类型 + 是否 nullable
SUMMARIZE my_table;
-- 每列:min/max/mean/std/null_count/unique_count,一行搞定数据探索
SHOW TABLES;
SHOW DATABASES;
SUMMARIZE 替代以下传统写法:
SELECT
COUNT(*), COUNT(DISTINCT col), MIN(col), MAX(col), AVG(col),
SUM(CASE WHEN col IS NULL THEN 1 ELSE 0 END)
FROM t;
11. 位置参数 + 列别名引用
-- GROUP BY 用位置参数(PG 也支持)
SELECT region, category, SUM(amount) FROM orders
GROUP BY 1, 2;
-- 同 SELECT 内引用别名(PG 不支持)
SELECT amount * 1.13 AS amount_with_tax,
amount_with_tax - amount AS tax_amount
FROM orders;
PG 的 amount_with_tax - amount 在同一 SELECT 子句里要重写表达式,DuckDB 直接引用别名。
12. 类型转换简写 + 字符串字面量
-- :: 类型转换(PG 也有)
SELECT '2026-05-08'::DATE, 3.14::INTEGER;
-- INTERVAL 字面量
SELECT NOW() + INTERVAL 7 DAY;
SELECT NOW() - INTERVAL '1 month 3 days';
-- DECIMAL 精确数学
SELECT 0.1::DECIMAL + 0.2::DECIMAL;
-- 0.3,不像 FLOAT 有浮点误差
速查对照表
| 需求 | DuckDB 写法 | 兼容性 |
|---|---|---|
| 每组 top N | QUALIFY ROW_NUMBER() OVER (...) <= N | DuckDB / Snowflake / BigQuery |
| 自动按 SELECT 列分组 | GROUP BY ALL | DuckDB / Snowflake / BigQuery / Databricks |
| 排除几列 | SELECT * EXCLUDE (a, b) | DuckDB |
| 转换几列 | SELECT * REPLACE (expr AS col) | DuckDB |
| 正则选列 | COLUMNS('.*_at') | DuckDB |
| 数组过滤 | list_filter(arr, x -> x > 5) | DuckDB / Snowflake(不同语法) |
| 直接读文件 | FROM 'data.csv' 或 read_csv_auto() | DuckDB |
| 长 / 宽表互转 | PIVOT / UNPIVOT | DuckDB / Snowflake / BigQuery |
| 数据探索 | SUMMARIZE table | DuckDB |
| 别名同 SELECT 引用 | SELECT a AS x, x + 1 | DuckDB |
| INTERVAL 字面量 | INTERVAL 7 DAY | DuckDB / Postgres |
实操检查清单
写 DuckDB SQL 时记得用上:
- 取每组 top N → QUALIFY 而不是嵌套子查询
- ad-hoc 查询 → GROUP BY ALL 节省键盘
- 选大部分列 → SELECT * EXCLUDE
- 数据探索 → SUMMARIZE 而不是写 N 个统计 SQL
- 数组操作简单 → lambda + list_filter / list_transform
- 数组操作复杂 → UNNEST + 关系操作
- 直接查文件 →
read_csv_auto/read_parquet,不用先 LOAD - 类型精确数学 →
::DECIMAL而不是 FLOAT - 跨数据库迁移 → 用 sqlglot 把 DuckDB 方言翻成 PG / Snowflake
- 生产 ETL → 写明确列名(不用 GROUP BY ALL)便于团队维护
DuckDB 的方言扩展几乎都是”语法糖”——能让人少打字、少写嵌套子查询。学会后写 ad-hoc 分析的速度会显著提升。