DuckDB SQL 方言速查:QUALIFY / GROUP BY ALL / EXCLUDE / lambda 等 PostgreSQL 没有的语法

· 约 6 分钟 🦆 DuckDB SQL 工作台

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 NQUALIFY 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-indexedarr[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 NQUALIFY ROW_NUMBER() OVER (...) <= NDuckDB / Snowflake / BigQuery
自动按 SELECT 列分组GROUP BY ALLDuckDB / 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 / UNPIVOTDuckDB / Snowflake / BigQuery
数据探索SUMMARIZE tableDuckDB
别名同 SELECT 引用SELECT a AS x, x + 1DuckDB
INTERVAL 字面量INTERVAL 7 DAYDuckDB / 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 分析的速度会显著提升。

❓ 常见问题

DuckDB SQL 和 PostgreSQL 兼容性怎么样?

DuckDB 90% 与 PostgreSQL 语法兼容,剩下 10% 是 DuckDB 自己的扩展(多数是友好语法糖)完全兼容的部分:(1) DDL(CREATE TABLE / CREATE INDEX / ALTER);(2) DML(INSERT / UPDATE / DELETE / UPSERT);(3) SELECT(CTE、窗口函数、子查询、JOIN);(4) 类型系统(INTEGER / VARCHAR / TIMESTAMP / DECIMAL / JSON 大致一致);(5) 大部分内置函数(字符串、数学、日期)。不兼容或差异大的部分:(1) 存储过程 / 触发器——DuckDB 不支持;(2) PL/pgSQL——DuckDB 没有;(3) 权限管理——DuckDB 没有 GRANT;(4) 扩展生态——PostGIS / pgvector 没有,DuckDB 有自己的 spatial / vss 扩展;(5) 类型细节——DuckDB 的 ARRAY / LIST / MAP / STRUCT 比 PG 的 array 更现代化;(6) NULL 排序——DuckDB 默认 NULLS LAST,PG 默认 NULLS FIRST。DuckDB 多出来的部分(这篇重点):(1) QUALIFY——窗口函数后过滤;(2) GROUP BY ALL / ORDER BY ALL;(3) SELECT * EXCLUDE / REPLACE;(4) lambda 函数——list_filter(arr, x -> x > 5);(5) UNNEST 展开数组;(6) PIVOT / UNPIVOT;(7) read_csv_auto / read_parquet 等表函数;(8) DESCRIBE / SUMMARIZE;(9) 位置参数引用 —— GROUP BY 1, 2;(10) 隐式类型转换 比 PG 宽松。实务:(1) PG 代码大部分能直接跑在 DuckDB;(2) DuckDB 写惯了再回 PG 会想念 QUALIFY 和 GROUP BY ALL;(3) 跨数据库迁移用 sqlglot 转换。

QUALIFY 是干什么的?什么时候用?

QUALIFY 是窗口函数的过滤子句——让你不用嵌套子查询就能"取每组前 N 名"问题场景:取每个用户最近 3 条订单。传统 SQL(PG / MySQL / SQLite):``sql\nSELECT * FROM (\n SELECT *,\n ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn\n FROM orders\n) WHERE rn <= 3;\n`DuckDB QUALIFY`sql\nSELECT * FROM orders\nQUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) <= 3;\n`优势:(1) 不用嵌套子查询,逻辑流更清晰;(2) 不用为窗口函数结果起列名(rn);(3) 编辑时改条件不用动外层结构。类比 GROUP BY → HAVING、SELECT → WHERE——QUALIFY 之于窗口函数 = HAVING 之于聚合。典型用法:(1) 取每组 top N——QUALIFY RANK() OVER (PARTITION BY ...) <= N;(2) 去重保留最新——QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) = 1;(3) 找异常值——QUALIFY ABS(value - AVG(value) OVER ()) > 3 * STDDEV(value) OVER ();(4) 第二高 / 第二多——QUALIFY DENSE_RANK() OVER (ORDER BY x DESC) = 2`。陷阱:(1) QUALIFY 在 SELECT、WHERE、GROUP BY、HAVING、窗口函数计算之后执行;(2) 不能引用未在 SELECT 里出现的窗口函数?错——QUALIFY 内可以重新写窗口函数;(3) PostgreSQL 16+ 才有 QUALIFY(部分支持),其他主流数据库(MySQL / SQLite)目前不支持。

GROUP BY ALL 是什么?有什么坑?

GROUP BY ALL 让 DuckDB 自动按 SELECT 里所有非聚合列分组——少写一遍列名问题场景:按 region、product_category、quarter 分组求 SUM。传统 SQL:``sql\nSELECT region, product_category, quarter, SUM(amount)\nFROM orders\nGROUP BY region, product_category, quarter;\n`—— SELECT 和 GROUP BY 各列一遍,改字段时两边都要改。DuckDB GROUP BY ALL`sql\nSELECT region, product_category, quarter, SUM(amount)\nFROM orders\nGROUP BY ALL;\n`—— 自动按 region、product_category、quarter 分组。类似的还有:(1) ORDER BY ALL —— 按 SELECT 里所有列排序;(2) GROUP BY 1, 2, 3 位置参数(PG 也支持)。优点:(1) DRY ——不重复列名;(2) 加列时只改 SELECT 一处;(3) 写 ad-hoc 查询快很多。陷阱:(1) 可读性——团队代码看不出按什么分组,长查询 + 多次 SELECT 列变化时易出 bug;(2) 副作用 ——SELECT 里多加一列就被自动加进 GROUP BY,可能改变结果集行数;(3) 复杂表达式列——SELECT date_trunc('month', ts), SUM(x) GROUP BY ALL 也能识别函数表达式作为分组键,但建议给表达式加别名(date_trunc(...) AS month`)便于阅读;(4) 跨数据库不兼容——PG / MySQL / SQLite 都不支持 GROUP BY ALL(仅 Snowflake、BigQuery、DuckDB、Databricks 等现代分析数据库支持)。实务:(1) ad-hoc 探索 / Jupyter 用 GROUP BY ALL 飞起;(2) 生产 ETL 代码 / 长期维护 SQL 写明确的 GROUP BY 列名更稳;(3) 跨数据库迁移时要展开 ALL。

SELECT * EXCLUDE 和 REPLACE 怎么用?

两个都是 SELECT * 的语法糖,让你不用列出几十个列名SELECT * EXCLUDE——选所有列除了指定的:``sql\nSELECT * EXCLUDE (password, secret_token, internal_id) FROM users;\n`—— 比写出 50 个字段名干净得多。SELECT * REPLACE——选所有列但替换某几列的表达式:`sql\nSELECT * REPLACE (\n upper(name) AS name,\n amount * 1.13 AS amount -- 加 13% 税\n) FROM orders;\n`—— name 列变大写、amount 列乘 1.13,其他列原样。组合用`sql\nSELECT * EXCLUDE (password) REPLACE (lower(email) AS email) FROM users;\n`典型场景:(1) 脱敏——EXCLUDE 掉敏感字段;(2) 类型转换 / 单位换算——REPLACE 几列;(3) 多表 JOIN 后——一边 EXCLUDE 重复的 join key 列,一边保留;(4) 快速 ad-hoc ——不用 desc 表就能选大部分列。JOIN 时的妙用`sql\nSELECT * EXCLUDE (cust_id) FROM orders o JOIN customers c USING (cust_id);\n`—— USING 自动合并 cust_id,配合 EXCLUDE 排除冗余字段。陷阱:(1) EXCLUDE 列必须实际存在——拼错列名报错(这是好事);(2) 跨 JOIN 时 EXCLUDE 不会区分表前缀——同名列要用 o.cust_id 这种限定;(3) PG / MySQL 不支持,跨数据库要展开;(4) REPLACE 改了的列保持原列名(除非显式 AS),别名没用。结合 COLUMNS 表达式——DuckDB 还有 COLUMNS(*) / COLUMNS('col_pattern') 用正则选列:`sql\nSELECT COLUMNS('.*_at') FROM events; -- 选所有以 _at 结尾的列\n``

DuckDB 的 lambda 函数和 LIST 操作怎么用?

lambda 让你在数组上用函数式 map / filter / reduce,避免 UNNEST 后再聚合基本语法x -> x > 5 单参数 lambda;(x, y) -> x + y 双参数 lambda。常用 LIST 函数:(1) list_filter(arr, x -> ...)——保留满足条件的元素;(2) list_transform(arr, x -> ...) (别名 list_apply)——map 每个元素;(3) list_reduce(arr, (acc, x) -> ...)——折叠;(4) list_aggregate(arr, 'sum')——简单聚合;(5) list_sort(arr)list_distinct(arr)list_concat(a, b)例子:``sql\n-- 数组里只保留 > 100 的元素\nSELECT list_filter([50, 120, 80, 200], x -> x > 100);\n-- 结果:[120, 200]\n\n-- 数组每个元素 ×2\nSELECT list_transform([1, 2, 3], x -> x * 2);\n-- 结果:[2, 4, 6]\n\n-- 数组求和(不展开)\nSELECT list_reduce([1, 2, 3, 4], (acc, x) -> acc + x);\n-- 结果:10\n`实战场景`sql\n-- 找出每个用户购买金额 > 100 的订单数\nSELECT user_id, length(list_filter(amounts, x -> x > 100)) AS big_orders\nFROM (\n SELECT user_id, list(amount) AS amounts\n FROM orders GROUP BY user_id\n);\n`优势 vs 传统写法:(1) 不需要 UNNEST 后再 GROUP BY;(2) 一行代替多行 SQL;(3) 更接近 Python / JS 的列表推导思维。LIST 类型——DuckDB 把 ARRAY / LIST / STRUCT / MAP 作为一等公民:(1) list(amount) 把列折叠成数组;(2) unnest(arr) 展开数组成行;(3) arr[1]arr[1:3]` 切片(1-indexed!)。陷阱:(1) DuckDB 数组 1-indexed,PG 也是,但 Python / JS 习惯是 0;(2) lambda 不支持闭包(不能引用外部 SQL 列);(3) 嵌套 lambda 可读性差,复杂逻辑还是 UNNEST + GROUP BY 清楚。

UNNEST 怎么用?什么时候用 ARRAY / LIST / STRUCT 而不是关联表?

UNNEST 把数组展开成多行——是数组类型和关系表之间的桥梁基本用法:``sql\nSELECT unnest([1, 2, 3]);\n-- 输出 3 行:1、2、3\n\nSELECT id, unnest(tags) AS tag FROM products;\n-- products.tags 是 LIST 类型,每个 tag 展开成一行\n`UNNEST 嵌套结构`sql\nSELECT unnest({a: 1, b: 2});\n-- 用 recursive => true 递归展开 STRUCT 字段\n`常用场景:(1) JSON 数组字段展开——SELECT id, unnest(json_keys) FROM events;(2) CSV 中的多值字段——tags = "tech,ml,nlp"unnest(string_split(tags, ","));(3) 生成序列——unnest(range(1, 100)) 生成 1-99 的行。ARRAY / LIST / STRUCT 的设计原则用嵌套类型替代关联表的好处:(1) 少 JOIN——一行表达完整对象;(2) 批量处理快——列存对嵌套类型有专门优化;(3) 类型自描述——schema 里包含完整结构。用关联表的好处:(1) 更新单元素方便——嵌套类型要重写整行;(2) 关系数据库范式严谨;(3) 跨数据库可移植典型选择:(1) 不变 / 只读的复杂结构 → STRUCT / LIST(如:订单的 line_items、用户的 addresses);(2) 频繁更新的子实体 → 关联表(如:订单状态历史);(3) 多对多关系 → 关联表 + JOIN;(4) 半结构化数据 → JSON 字段 + JSON 函数。STRUCT 字段访问obj.fieldobj['field'],类似 Python:`sql\nSELECT order_id, items.product_name, items.price\nFROM orders, unnest(line_items) AS t(items);\n``陷阱:(1) UNNEST 必须放 FROM 里(部分场景)或当 SELECT 表达式(PG 风格);(2) 嵌套结构在 BI 工具(Tableau / Looker)里不友好——很多 BI 不识别 STRUCT;(3) 真要导出给非 DuckDB 工具用,先 UNNEST 平铺。

🦆 打开 DuckDB SQL 工作台 拖入 Parquet/CSV/JSON/Excel→SQL 查询/修改·多文件 JOIN·导出 Parquet/CSV/JSON/Excel·DuckDB-Wasm 浏览器内运行

📖 同一工具的其他教程