本文档由 DuckDB 官方网站「核心入门」范围内的文档页面翻译整理而成,共七章:DuckDB 简介与特性、安装、快速上手与 CLI、核心 SQL 语法、常见数据类型、窗口函数、Python 入门。
翻译原则:正文逐句完整译为简体中文,语气正式、书面、客观;所有代码示例、SQL 语句、运行输出表格、命令行与校验值均原样保留(含英文注释,未作改动);SQL 关键词、数据类型名与函数名保留英文原文,首次出现处括注中文含义;全篇术语译法统一(如 relation=关系、schema=模式、aggregate function=聚合函数、window function=窗口函数、in-memory database=内存数据库、persistent database=持久化数据库、extension=扩展、DataFrame=DataFrame、CLI=命令行界面)。
文末附各章节对应的官方英文源页面地址(第八、附录)。
–虑得先生整理
目录
- DuckDB 简介与特性
- 安装
- 快速上手与 CLI 命令行
- 核心 SQL 语法(SQL 教程)
- 常见数据类型
- 窗口函数
- Python 入门
- 附录:官方英文源页面
一、DuckDB 简介与特性
原文来源:DuckDB 官网首页(https://duckdb.org/)。本节为官网首页「简介与特性」相关内容的完整中文翻译;其中所有代码示例原样保留。
1.1 定位与概述
官网主标语:你的通用数据整理工具(Your universal data wrangling tool)。
DuckDB 是一个分析型数据库(analytical database)系统,可部署在任意环境:从边缘设备(edge devices)到拥有数百个核心的服务器。
1.2 核心特性
- 随处部署(Database anywhere):DuckDB 是一个分析型数据库系统,可部署在任意环境——从边缘设备到拥有数百个核心的服务器。
- 友好的 SQL(Friendly SQL):DuckDB 提供一种友好且富有表现力的查询语言,其语法受 PostgreSQL 启发。
- 可扩展(Extensible):扩展(extensions)可以引入新的函数,并支持新的数据格式。
- 原生客户端(Native clients):DuckDB 为所有主流编程语言提供地道(idiomatic)且高性能的 API。
- 支持你的数据格式(Speaks your formats):可在本地或对象存储(object storage)中读写 CSV、JSON、Parquet、Iceberg 等流行格式。
- 免费且开源(Free & open-source):DuckDB 及相关项目采用 MIT 许可证,由独立的 DuckDB 基金会(DuckDB Foundation)治理。
1.3 不止是一个查询引擎:Duck 技术栈(The Duck Stack)
官网将 DuckDB 及其周边生态统称为「Duck Stack」:它包含一个数据库、一个协议,并为 lakehouse 格式提供一流支持。其组成如下:
- DuckDB:一种可进程内(in-process)运行的分析型 SQL 数据库。简单、快速、可移植。
- Quack:DuckDB 的客户端-服务器协议。借助 Quack,客户端和服务器都是功能完整的 DuckDB 实例,因此你既可以访问远程数据库,也可以在客户端一侧执行计算。
- DuckLake:一种构建在 SQL 之上的 lakehouse 格式。将数据保存在对象存储中,并使用 SQL 数据库作为目录(catalog),以获得可扩展性、简单性与速度。
- Iceberg:DuckDB 对 Apache Iceberg 提供一流支持:你可以直接以高性能从 DuckDB 读写 Iceberg 表。
1.4 生态集成(Works with your stack)
DuckDB 直接与各类编程语言、协议和数据格式集成:
- 协议与存储(Protocols & storage):Cloudflare、Amazon Web Services、Microsoft Azure、Google Cloud、Hugging Face
- 数据库(Databases):SQLite、MySQL、PostgreSQL、ODBC、MotherDuck
- 客户端(Clients):CLI、Python、Go、Java、Node.js、C、C++、R、Rust、ODBC
- 数据格式(Formats):Parquet、CSV、JSON、Vortex、Lance、Iceberg、Delta Lake、DuckLake、DuckDB、Geospatial、Avro、Excel、Arrow
1.5 特性示例(官网代码演示)
以下示例来自官网首页,直观展示了 DuckDB 的核心能力(代码原样保留):
查询最繁忙的前 3 个火车站(按服务班次数 num_services 排名,演示 GROUP BY ALL 语法与 LIMIT):
-- Get the top-3 busiest train stations
SELECT
station_name,
count(*) AS num_services
FROM train_services
GROUP BY ALL
ORDER BY num_services DESC
LIMIT 3;
将 CSV 文件加载为表——DuckDB 自动检测 CSV 的格式、列名与类型:
-- Load CSV file to a table. DuckDB auto-detects
-- the CSV's format, column name and types
CREATE TABLE stations AS
FROM 'https://blobs.duckdb.org/stations.csv';
通过 HTTPS 直接查询 Parquet 文件(无需先行导入):
Parquet 是一种开源的「列式存储」二进制文件格式(Apache 旗下,不是数据库,只负责 “存数据”),DuckDB、Spark、ClickHouse 等分析型工具读它特别快,因为它天生为 “只读某几列、做聚合统计” 而设计
-- Directly query Parquet file over HTTPS
SELECT
station_name,
count(*) AS num_services
FROM 'https://blobs.duckdb.org/train_services.parquet'
GROUP BY ALL
ORDER BY num_services DESC
LIMIT 10;
读写 Pandas DataFrame(Python 客户端示例):
# Reading and writing Pandas dataframes
import pandas as pd
import duckdb
df_in = pd.DataFrame({
'station': ['Delft', 'Delft', 'Gouda', 'Gouda'],
'day': ['Mon', 'Tue', 'Mon', 'Tue'],
'num_services' : [22, 20, 27, 25]})
# Run query on a dataframe and return a dataframe
df_out = duckdb.sql("""
SELECT station, sum(num_services)
FROM df_in
GROUP BY station
""").to_df()
此外,官网还提供了 Go、Java、Node.js、R、Rust、ODBC 等客户端的一体化示例;本文档后续章节将展开介绍 Python、CLI 与核心 SQL 的用法。
1.6 版本与许可
- DuckDB 及其相关项目采用 MIT 许可证,由独立的 DuckDB 基金会(DuckDB Foundation)治理。
- 官网当前展示的最新版本为 2.0.0(官网于 2026 年 10 月发布了 2.0.0 LTS「Cinnamon Teal」的版本公告)。
- 各平台安装方式详见下一章「二、安装」。
二、安装
原文来源:DuckDB 官方安装文档(https://duckdb.org/docs/installation/)。以下按客户端类型列出各平台支持的安装方式;所有安装命令、下载文件名与 SHA256 校验值均原样保留,未作改动。
2.1 命令行界面(CLI)
Linux / macOS
使用官方安装脚本:
curl https://install.duckdb.org | sh
macOS 还可通过 Homebrew 包管理器安装:
brew install duckdb
也可使用 Docker 镜像运行:
docker run --rm -it -v "$(pwd):/workspace" -w /workspace duckdb/duckdb
或直接下载对应平台的压缩包:
- macOS(universal):
duckdb_cli-osx-universal.zip- SHA256:
d6ac869dad8b197ce372b20162e2a95d20fc93b6e105505921e143da700bb263
- SHA256:
- Linux amd64:
duckdb_cli-linux-amd64.zip- SHA256:
82c75b22b8035204122da84528f6954fb80c8749f0c9eeb9709e33d0b7933fe5
- SHA256:
- Linux arm64:
duckdb_cli-linux-arm64.zip- SHA256:
3e29952507ebd202e8d0e2678df4490689bfc9c86534e240b541791b969df0e1
- SHA256:
Windows
通过 winget 包管理器安装:
winget install DuckDB.cli
或直接下载压缩包:
- Windows amd64:
duckdb_cli-windows-amd64.zip- SHA256:
3fab5174658bf16189c80eaa047b3e7737847491c9e4433a985adf014618cbd9
- SHA256:
- Windows arm64:
duckdb_cli-windows-arm64.zip- SHA256:
b753187a285f491bf7e2dfbb0b893f551390806f6944d9f1764a7aa7e69d9dbb
- SHA256:
注意:DuckDB 在 Windows 上运行需要安装 Microsoft Visual C++ Redistributable(微软 Visual C++ 运行库)。(译注:官网首页安装区给出的 CLI 安装命令为
curl https://install.duckdb.org | bash,与本页脚本安装方式一致。)
2.2 Python
通过 pip 安装:
pip install duckdb
2.3 Java
直接下载 JAR 包:
duckdb_jdbc-1.4.1.0.jar- SHA256:
038db35c2c8fdab337cf0592f1213ba3eac04cdb51b91485adf20b97a0aa3f93
- SHA256:
通过 Maven 引入依赖:
<dependency>
<groupId>org.duckdb</groupId>
<artifactId>duckdb_jdbc</artifactId>
<version>1.4.1.0</version>
</dependency>
通过 Gradle 引入依赖:
implementation("org.duckdb:duckdb_jdbc:1.4.1.0")
2.4 Go
go get github.com/duckdb/duckdb-go/v2
2.5 Node.js
npm install @duckdb/node-api
2.6 ODBC
按平台下载压缩包:
- Linux amd64:
duckdb_odbc-linux-amd64.zip- SHA256:
b98d08c15394e89aaf339eb97ca08b0f6354c3e3143fce4c7ea7583f92c5f115
- SHA256:
- Linux arm64:
duckdb_odbc-linux-arm64.zip- SHA256:
1a50bae51ee15290fccd9813620e4803e9c55d082538032aaff192d4e2e978de
- SHA256:
- macOS(universal):
duckdb_odbc-osx-universal.zip- SHA256:
6235d7fd73e956d60e4cf03cd2a5461026edb9ec1fb92962fd92ca4954b64ed6
- SHA256:
- Windows amd64:
duckdb_odbc-windows-amd64.zip- SHA256:
9ce35378ef7e6a5c6a47e37d0d2af0b87599aa4238ed513e21abf4a262d605f8
- SHA256:
注意:ODBC 客户端目前尚不提供 Windows / arm64 平台的二进制包。
2.7 C / C++
按平台下载库文件(libduckdb):
- Linux amd64:
libduckdb-linux-amd64.zip- SHA256:
ce859962fe96ca952d53571964dafa0168644283a76a2c669b3f73120d710edb
- SHA256:
- Linux arm64:
libduckdb-linux-arm64.zip- SHA256:
4aa05a74956b1d57f05a139623943231caeee36286c9f42ff10dc278b6df0b6e
- SHA256:
- macOS(universal):
libduckdb-osx-universal.zip- SHA256:
a82bde325dcbb6d1011e1014673eb288e8c1fd5f5b755d47fd245e8b37194817
- SHA256:
- Windows amd64:
libduckdb-windows-amd64.zip- SHA256:
704877e85b556640b0f20a0f07a1c1f404320210b245f51b6da0087a1c92bb76
- SHA256:
- Windows arm64:
libduckdb-windows-arm64.zip- SHA256:
851f0b89659f9ebfdecc99a9371942117fe4bc9fd9521af82fbe52be073534b6
- SHA256:
2.8 R
install.packages("duckdb")
2.9 Rust
cargo add duckdb --features bundled
三、快速上手与 CLI 命令行
原文来源:DuckDB 官方文档「CLI Client」页面(https://duckdb.org/docs/api/cli)。本节为该页全部实质内容的完整中文翻译;其中所有命令行、运行输出、SQL 语句与代码块均原样保留,未作改动。
DuckDB 命令行界面(CLI,Command Line Interface)客户端的最新版本为 1.3.2。
3.1 安装
DuckDB CLI 是一个单一、无任何依赖的可执行文件,已为 Windows、macOS 与 Linux 预编译了稳定版本以及由 GitHub Actions 产出的 nightly 构建。下载链接请参见安装文档中 CLI 标签页下的说明。
DuckDB CLI 基于 SQLite 命令行 shell 构建,因此 CLI 客户端特有的功能与 SQLite 文档中所述的用法相似(不过 DuckDB 的 SQL 语法遵循 PostgreSQL 的惯例,仅有少数例外)。
DuckDB 还有一个 tldr 页面,概括了 CLI 客户端最常见的用法。如果本机已安装 tldr,可以运行 tldr duckdb 来查看。
3.2 快速上手
下载 CLI 可执行文件后,将其解压并保存到任意目录。在终端中进入该目录,然后输入 duckdb 命令即可运行可执行文件。如果处于 PowerShell 或 POSIX shell 环境中,则使用 ./duckdb 命令。
3.3 命令行用法
duckdb 命令的典型用法如下:
duckdb [OPTIONS] [FILENAME]
选项(OPTIONS)
[OPTIONS] 部分用于传入 CLI 客户端的参数。常用选项包括:
-csv:将输出模式设置为 CSV-json:将输出模式设置为 JSON-readonly:以只读模式打开数据库(参见 DuckDB 中的并发)
完整的选项列表请参见命令行参数页面。
3.4 内存数据库与持久化数据库
当未提供 [FILENAME] 参数时,DuckDB CLI 将打开一个临时的内存数据库(in-memory database)。此时会显示 DuckDB 的版本号、连接信息,以及一个以 D 开头的提示符。
duckdb
DuckDB v1.3.2 (Ossivalis) 0b83e5d2f6
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D
要打开或创建一个持久化数据库(persistent database),只需在命令行参数中传入一个路径即可:
duckdb my_database.duckdb
3.5 在 CLI 中执行 SQL 语句
打开 CLI 之后,输入一条以分号结尾的 SQL 语句,然后按回车键即可执行。结果将以表格形式显示在终端中。如果省略分号,按回车键后可以继续输入多行 SQL 语句。
SELECT 'quack' AS my_column;
my_column
---------
quack
CLI 支持 DuckDB 全部丰富的 SQL 语法,包括 SELECT、CREATE 与 ALTER 等语句。
3.6 编辑器功能
CLI 支持自动补全(autocompletion),并在部分平台上提供完善的编辑器功能与语法高亮。
3.7 退出 CLI
要退出 CLI,如果所在平台支持,请按 Ctrl+D。否则按 Ctrl+C,或使用 .exit 命令。如果使用的是持久化数据库,DuckDB 将自动执行 checkpoint(将最新修改写入磁盘)并关闭连接。这会移除 .wal 文件(write-ahead log,预写式日志),并将所有数据合并到单文件数据库中。
3.8 点命令(Dot Commands)
除了 SQL 语法之外,CLI 客户端中还可以输入特殊的点命令(dot commands)。要使用这些命令,需在行首输入一个句点(.),紧随其后的是要执行的命令名称。命令的附加参数跟在命令名之后,以空格分隔。如果某个参数本身需要包含空格,可以用单引号或双引号将该参数包裹起来。点命令必须在单行内输入,句点之前不能有任何空白字符。行尾不需要加分号。
常用配置可以保存在 ~/.duckdbrc 文件中,CLI 客户端启动时会自动加载该文件。关于这些选项的更多信息,请参见下文「3.13 配置 CLI」一节。
提示:要阻止 DuckDB CLI 客户端读取
~/.duckdbrc文件,可以按如下方式启动:duckdb -init /dev/null
下面简要介绍几个重要的点命令。要查看全部可用命令,请参见点命令页面,或使用 .help 命令。
3.9 打开数据库文件(.open)
除了在启动 CLI 时连接数据库之外,还可以使用 .open 命令建立新的数据库连接。如果不提供额外参数,将创建一个新的内存数据库连接。当 CLI 连接关闭时,该数据库不会被持久化保存。
.open
.open 命令可以接受若干选项,最后一个参数用于指定持久化数据库的路径(或新建数据库的位置)。特殊字符串 :memory: 也可用于打开一个临时的内存数据库。
.open persistent.duckdb
警告:
.open会关闭当前数据库。如果想保留当前数据库,同时再添加一个新数据库,请使用 ATTACH 语句。
.open 接受的一个重要选项是 --readonly 标志。该标志禁止对数据库做任何修改。要以只读模式打开,数据库必须已经存在。这同时意味着不能以只读模式打开新的内存数据库,因为内存数据库是在连接建立时创建的。
.open --readonly preexisting.duckdb
3.10 输出格式(.mode)
.mode 点命令可用于调整终端输出中表格的呈现方式。可用模式包括默认的 duckbox 模式、供其他工具摄取数据的 csv 与 json 模式、用于文档排版的 markdown 与 latex 模式,以及用于生成 SQL 语句的 insert 模式。
3.11 将结果写入文件
默认情况下,DuckDB CLI 将结果输出到终端的标准输出。不过可以使用 .output 或 .once 命令来改变这一行为。详细信息请参见输出点命令的文档。
3.12 从文件读取 SQL(.read)
DuckDB CLI 可以使用 .read 命令从外部文件(而非终端)读取 SQL 命令和点命令。这使得多条命令可以按顺序执行,也便于将命令序列保存下来重复使用。
.read 命令只需要一个参数:包含要执行的 SQL 和/或命令的文件路径。执行完文件中的命令后,控制权将返回到终端。该文件执行的输出受前文讨论过的 .output 与 .once 命令控制。这使得输出可以像下面第一个示例那样显示回终端,也可以像第二个示例那样输出到另一个文件。
在本示例中,文件 select_example.sql 位于 duckdb.exe 所在目录,包含以下 SQL 语句:
SELECT *
FROM generate_series(5);
要在 CLI 中执行它,使用 .read 命令:
.read select_example.sql
默认情况下,输出如下返回到终端。表格的格式可以使用 .output 或 .once 命令调整。
| generate_series |
|----------------:|
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
多条命令(包括 SQL 和点命令)也可以在单次 .read 调用中执行。在本示例中,文件 write_markdown_to_file.sql 位于 duckdb.exe 所在目录,包含以下命令:
.mode markdown
.output series.md
SELECT *
FROM generate_series(5);
要在 CLI 中执行它,仍按之前的方式使用 .read 命令:
.read write_markdown_to_file.sql
在这种情况下,不会有任何输出返回到终端。取而代之的是,会创建(或覆盖,如果已存在)文件 series.md,其中包含如下 Markdown 格式的结果:
| generate_series |
|----------------:|
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
3.13 配置 CLI
有若干点命令可用于配置 CLI。启动时,CLI 会读取并执行 ~/.duckdbrc 文件中的所有命令,包括点命令和 SQL 语句。这样就可以保存 CLI 的配置状态。也可以使用 -init 参数指向一个不同的初始化文件。
3.13.1 设置自定义提示符
举个例子,在 DuckDB CLI 所在目录下创建一个名为 prompt.sql 的文件,它会将 DuckDB 提示符改为一个鸭头图案并执行一条 SQL 语句。注意,该鸭头是用 Unicode 字符拼出来的,并非在所有终端环境下都能正常显示(例如在 Windows 上,除非通过 WSL 运行并使用 Windows Terminal)。
.prompt '⚫◗ '
要在初始化时加载该文件,使用以下命令:
duckdb -init prompt.sql
输出如下:
-- Loading resources from prompt.sql
v⟨version⟩ ⟨git_hash⟩
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
⚫◗
3.14 非交互式用法
要读取/处理一个文件并立即退出,可以将文件内容重定向到 duckdb:
duckdb < select_example.sql
要执行一条通过命令行直接传入 SQL 文本的命令,可以给 duckdb 传两个参数:数据库位置(或 :memory:),以及一条包含待执行 SQL 语句的字符串。
duckdb :memory: "SELECT 42 AS the_answer"
3.15 加载扩展
要加载扩展(extensions),像执行其他 SQL 语句一样使用 DuckDB 的 INSTALL 和 LOAD 命令即可。
INSTALL fts;
LOAD fts;
详细信息请参见扩展(Extension)文档。
3.16 从 stdin 读取与向 stdout 写入
在 Unix 环境中,在多个命令之间通过管道传递数据非常有用。DuckDB 能够从 stdin 读取数据,也能向 stdout 写入数据——在 SQL 命令中分别使用 stdin 的文件位置(/dev/stdin)和 stdout 的文件位置(/dev/stdout)即可,因为管道的行为与文件句柄非常相似。
以下命令会创建一个示例 CSV 文件:
COPY (SELECT 42 AS woot UNION ALL SELECT 43 AS woot) TO 'test.csv' (HEADER);
首先,读取该文件并将其通过管道传给 duckdb CLI 可执行文件。作为 DuckDB CLI 的参数,传入要打开的数据库位置(本例为内存数据库),以及一条使用 /dev/stdin 作为文件位置的 SQL 命令。
cat test.csv | duckdb -c "SELECT * FROM read_csv('/dev/stdin')"
woot
----
42
43
要写回 stdout,可以将 COPY 命令与 /dev/stdout 文件位置配合使用。
cat test.csv | \
duckdb -c "COPY (SELECT * FROM read_csv('/dev/stdin')) TO '/dev/stdout' WITH (FORMAT csv, HEADER)"
woot
----
42
43
3.17 读取环境变量
getenv 函数可以读取环境变量。
示例
要从 HOME 环境变量中获取主目录路径,使用:
SELECT getenv('HOME') AS home;
home
-----------
/Users/user_name
getenv 函数的返回值也可以用于设置配置选项。例如,要根据环境变量 DEFAULT_NULL_ORDER 设置 NULL 排序顺序,使用:
SET default_null_order = getenv('DEFAULT_NULL_ORDER');
读取环境变量的限制
getenv 函数仅在 enable_external_access 选项设置为 true(默认设置)时才能运行。它仅在 CLI 客户端中可用,其他 DuckDB 客户端不支持该函数。
3.18 预处理语句
DuckDB CLI 除了支持常规的 SELECT 语句之外,还支持执行预处理语句(prepared statements)。要在 CLI 客户端中创建并执行预处理语句,使用 PREPARE 子句和 EXECUTE 语句。
四、核心 SQL 语法(SQL 教程)
原文来源:DuckDB 官方文档「SQL 入门教程」(https://duckdb.org/docs/sql/introduction)。本页概述如何使用 SQL 执行简单操作;该教程改编自 PostgreSQL 教程,仅作入门介绍,并非完整的 SQL 教程。所有 SQL 代码块与运行输出表格均原样保留。
本节介绍如何用 SQL 完成常见操作。需要说明的是,本教程仅作入门引导,并非一份完整的 SQL 教程,其内容改编自 PostgreSQL 官方教程。
DuckDB 的 SQL 方言(SQL dialect)基本沿用 PostgreSQL 方言的约定;少数差异点列于 PostgreSQL 兼容性页面。
在以下示例中,我们假定你已经安装了 DuckDB 命令行界面(Command Line Interface,CLI)shell。安装方式请参见安装章节。
4.1 基本概念(Concepts)
DuckDB 是一个关系型数据库管理系统(Relational Database Management System,RDBMS)。也就是说,它是一个用于管理存储在关系(relation)中的数据的系统。关系本质上是一个数学术语,对应到数据库语境就是表(table)。
每张表都是一组具名行(row)的集合。同一张表中的每一行都拥有同一组具名列(column),而每一列都具有特定的数据类型。表本身又存储在模式(schema)之中,若干个模式合起来构成你所能访问的整个数据库(database)。
4.2 创建新表(Creating a New Table)
你可以通过指定表名,以及所有列名及其数据类型,来创建一张新表:
CREATE TABLE weather (
city VARCHAR,
temp_lo INTEGER, -- minimum temperature on a day
temp_hi INTEGER, -- maximum temperature on a day
prcp FLOAT,
date DATE
);
在 shell 中输入时可以换行;一条命令直到出现分号(;)才算结束。
在 SQL 命令中,空白字符(即空格、制表符和换行符)可以自由使用。这意味着你可以把命令排成与上面不同的对齐方式,甚至全部写在一行上。两个短横线(--)表示注释:从 -- 开始到行尾的内容都会被忽略。SQL 对关键字和标识符不区分大小写;但在返回标识符时,会保留其原始大小写。
在上述 SQL 命令中,我们首先指定要执行的命令类型:CREATE TABLE。其后跟随该命令的参数:先是表名 weather,接着依次列出列名与列类型。
city VARCHAR 表示该表包含一个名为 city、类型为 VARCHAR 的列。VARCHAR 是一种可存储任意长度文本的数据类型。温度字段使用 INTEGER 类型存储——该类型保存整数(即不带小数点的整数)。FLOAT 列存储单精度浮点数(即带小数点的数)。DATE 存储日期(即年、月、日的组合);DATE 只保存具体的某一天,不保存与该天相关的时间。
DuckDB 支持标准 SQL 数据类型:INTEGER、SMALLINT、FLOAT、DOUBLE、DECIMAL、CHAR(n)、VARCHAR(n)、DATE、TIME 和 TIMESTAMP。
第二个示例将存储城市及其对应的地理位置:
CREATE TABLE cities (
name VARCHAR,
lat DECIMAL,
lon DECIMAL
);
最后需要说明:如果某张表不再需要,或者你想以不同方式重建它,可以使用以下命令将其删除:
DROP TABLE tablename;
4.3 向表中插入行(Populating a Table with Rows)
INSERT 语句用于向表中插入行:
INSERT INTO weather
VALUES ('San Francisco', 46, 50, 0.25, '1994-11-27');
非常量数值(例如文本和日期)必须像示例那样用一对单引号括起来。DATE 类型的输入日期必须按 'YYYY-MM-DD' 格式书写。
可以用同样的方式向 cities 表插入数据:
INSERT INTO cities
VALUES ('San Francisco', -194.0, 53.0);
到目前为止所用的语法要求你记住列的顺序。另一种语法允许显式列出列名:
INSERT INTO weather (city, temp_lo, temp_hi, prcp, date)
VALUES ('San Francisco', 43, 57, 0.0, '1994-11-29');
你也可以按不同顺序列出列名,甚至省略某些列——例如当 prcp 未知时:
INSERT INTO weather (date, city, temp_hi, temp_lo)
VALUES ('1994-11-29', 'Hayward', 54, 37);
提示(Tip):许多开发者认为,显式列出列名比隐式依赖列顺序是更好的书写风格。
请把上面所示的所有命令都输入一遍,以便在后续小节中拥有可供操作的数据。
另一种方式是使用 COPY 语句。对于大批量数据,COPY 速度更快,因为它针对批量加载做了优化;不过其灵活性不如 INSERT。以 weather.csv 为例:
COPY weather
FROM 'weather.csv';
其中源文件名必须在运行该进程的机器上可访问。将数据载入 DuckDB 还有许多其他方式,详见相关文档章节。
4.4 查询表(Querying a Table)
要从表中取回数据,就需要对表进行查询(query)。SQL 的 SELECT 语句用于完成这一任务。一条 SELECT 语句由三部分组成:选择列表(select list,列出要返回的列)、表列表(table list,列出从中取数的表)以及可选的限定条件(qualification,用于指定任何限制)。例如,要取回 weather 表的所有行,输入:
SELECT *
FROM weather;
这里的 * 是「所有列」的简写。因此,下面这条语句会得到完全相同的结果:
SELECT city, temp_lo, temp_hi, prcp, date
FROM weather;
输出应为:
city temp_lo temp_hi prcp date
San Francisco 46 50 0.25 1994-11-27
San Francisco 43 57 0.0 1994-11-29
Hayward 37 54 NULL 1994-11-29
在选择列表中,你不仅可以写简单的列引用,还可以写表达式(expression)。例如:
SELECT city, (temp_hi + temp_lo) / 2 AS temp_avg, date
FROM weather;
结果如下:
city temp_avg date
San Francisco 48.0 1994-11-27
San Francisco 50.0 1994-11-29
Hayward 45.5 1994-11-29
注意这里使用 AS 子句(clause)为输出列重新命名(即起别名 alias)。(AS 子句是可选的。)
4.4.1 用 WHERE 过滤行
查询可以通过附加 WHERE 子句进行「限定」,用以指定需要哪些行。WHERE 子句包含一个布尔(真值)表达式,只有当该布尔表达式为真时,对应的行才会被返回。限定条件中允许使用常见的布尔运算符:AND、OR 和 NOT。例如,下面这条语句取回旧金山下雨那天的天气:
SELECT *
FROM weather
WHERE city = 'San Francisco'
AND prcp > 0.0;
结果:
city temp_lo temp_hi prcp date
San Francisco 46 50 0.25 1994-11-27
4.4.2 用 ORDER BY 排序
你可以要求查询结果按排序后的顺序返回:
SELECT *
FROM weather
ORDER BY city;
city temp_lo temp_hi prcp date
Hayward 37 54 NULL 1994-11-29
San Francisco 43 57 0.0 1994-11-29
San Francisco 46 50 0.25 1994-11-27
在这个示例中,排序规则并未完全指定,因此旧金山的两行可能以任意顺序出现。但如果你这样写,结果就会始终如上所示:
SELECT *
FROM weather
ORDER BY city, temp_lo;
4.4.3 用 DISTINCT 去重
你可以要求从查询结果中去除重复行:
SELECT DISTINCT city
FROM weather;
city
San Francisco
Hayward
同样地,此处结果行的顺序可能变化。你可以将 DISTINCT 与 ORDER BY 一起使用,以确保结果一致:
SELECT DISTINCT city
FROM weather
ORDER BY city;
4.5 表之间的连接(Joins between Tables)
到目前为止,我们的查询一次只访问一张表。查询可以一次访问多张表,也可以以同一表的多行被同时处理的方式访问该表。一次同时访问同一表或不同表的多行的查询,称为连接查询(join query)。举例来说,假设你希望列出所有天气记录,连同相关城市的地理位置。要做到这一点,我们需要把 weather 表每一行的 city 列与 cities 表所有行的 name 列进行比较,选出这两个值相匹配的行对。
可用如下查询完成:
SELECT *
FROM weather, cities
WHERE city = name;
city temp_lo temp_hi prcp date name lat lon
San Francisco 46 50 0.25 1994-11-27 San Francisco -194.000 53.000
San Francisco 43 57 0.0 1994-11-29 San Francisco -194.000 53.000
关于这个结果集(result set),有两点需要注意:
- 没有出现 Hayward 这一行。这是因为
cities表中没有与 Hayward 相匹配的条目,所以连接会忽略weather表中那些未匹配的行。稍后我们将看到如何修正这一问题。 - 有两列都包含城市名。这是正确的,因为
weather表和cities表的列列表被拼接在了一起。但在实际中这并不理想,因此你大概会希望显式列出要输出的列,而不是使用*:
SELECT city, temp_lo, temp_hi, prcp, date, lon, lat
FROM weather, cities
WHERE city = name;
city temp_lo temp_hi prcp date lon lat
San Francisco 46 50 0.25 1994-11-27 53.000 -194.000
San Francisco 43 57 0.0 1994-11-29 53.000 -194.000
由于这些列名各不相同,解析器会自动判断它们分别属于哪张表。如果两张表中存在重名列,你就需要对列名加以限定(即带上表名前缀),以指明你指的是哪一列:
SELECT weather.city, weather.temp_lo, weather.temp_hi,
weather.prcp, weather.date, cities.lon, cities.lat
FROM weather, cities
WHERE cities.name = weather.city;
普遍认为,在连接查询中对所有列名加以限定是一种好风格;这样即使日后其中一张表新增了同名列,查询也不会因此失效。
4.5.1 INNER JOIN
迄今为止所见的连接查询,也可以写成另一种等价形式:
SELECT *
FROM weather
INNER JOIN cities ON weather.city = cities.name;
这种语法不如上面那种常用,但在这里给出它是为了帮助你理解接下来的内容。
4.5.2 LEFT OUTER JOIN
现在我们来看如何把 Hayward 的记录重新取回来。我们希望查询做的事情是:扫描 weather 表,并为每一行找到 cities 表中相匹配的行;如果找不到匹配行,则用一些「空值」来填充 cities 表的列。这种查询称为外连接(outer join)。(我们目前所见的连接都是内连接 inner join。)命令如下:
SELECT *
FROM weather
LEFT OUTER JOIN cities ON weather.city = cities.name;
city temp_lo temp_hi prcp date name lat lon
San Francisco 46 50 0.25 1994-11-27 San Francisco -194.000 53.000
San Francisco 43 57 0.0 1994-11-29 San Francisco -194.000 53.000
Hayward 37 54 NULL 1994-11-29 NULL NULL NULL
这条查询称为左外连接(left outer join),是因为连接运算符左侧的表,其每一行在输出中至少出现一次;而右侧的表则只有那些与左表某行相匹配的行才会被输出。在输出左表某一行、而右表中没有匹配行时,右表的列就用空值(null)填充。
4.6 聚合函数(Aggregate Functions)
与大多数其他关系型数据库产品一样,DuckDB 支持聚合函数(aggregate function)。聚合函数从多行输入中计算出一个单一结果。例如,有一些聚合函数可以在一组行上计算计数(count)、求和(sum)、平均值(avg)、最大值(max)和最小值(min)。
举例来说,我们可以用下面的语句求出任何地方出现过的最低气温的最高值:
SELECT max(temp_lo)
FROM weather;
max(temp_lo)
46
如果我们想知道这个读数出现在哪座(或哪些)城市,可能会尝试:
SELECT city
FROM weather
WHERE temp_lo = max(temp_lo);
但这是行不通的,因为聚合函数 max 不能出现在 WHERE 子句中:
Binder Error:
WHERE clause cannot contain aggregates!
存在这一限制,是因为 WHERE 子句决定了哪些行将被纳入聚合计算;因此它显然必须在聚合函数计算之前被求值。不过,这类查询通常都可以改写以达到预期结果——这里借助一个子查询(subquery)即可:
SELECT city
FROM weather
WHERE temp_lo = (SELECT max(temp_lo) FROM weather);
city
San Francisco
这是可行的,因为该子查询是一个独立的计算,它所做的聚合与外层查询中发生的事情彼此独立。
4.6.1 与 GROUP BY 配合使用
聚合函数与 GROUP BY 子句配合时也非常有用。例如,我们可以用下面的语句得到每座城市观测到的最低气温的最大值:
SELECT city, max(temp_lo)
FROM weather
GROUP BY city;
city max(temp_lo)
San Francisco 46
Hayward 37
这为每座城市输出一行。每个聚合结果都是在该城市所匹配的那些表行之上计算出来的。我们可以使用 HAVING 对这些分组后的行进行过滤(filter):
SELECT city, max(temp_lo)
FROM weather
GROUP BY city
HAVING max(temp_lo) < 40;
city max(temp_lo)
Hayward 37
这就只对那些所有 temp_lo 值都低于 40 的城市返回相同形式的结果。最后,如果我们只关心名字以字母 S 开头的城市,可以使用 LIKE 运算符:
SELECT city, max(temp_lo)
FROM weather
WHERE city LIKE 'S%' -- (1)
GROUP BY city
HAVING max(temp_lo) < 40;
关于 LIKE 运算符的更多信息,请参见模式匹配(pattern matching)相关页面。
4.6.2 WHERE 与 HAVING 的区别
理解聚合函数与 SQL 的 WHERE、HAVING 子句之间的相互作用非常重要。WHERE 与 HAVING 的根本区别在于:WHERE 在分组与聚合计算之前筛选输入行(因此它控制哪些行进入聚合计算),而 HAVING 在分组与聚合计算之后筛选分组行。因此,WHERE 子句中不能包含聚合函数;试图用聚合函数来决定哪些行将成为聚合的输入,是毫无意义的。另一方面,HAVING 子句则总是包含聚合函数。
在上一个示例中,我们把城市名的限制放在 WHERE 中,因为它不需要聚合。这比把该限制放到 HAVING 中效率更高,因为我们可以避免对所有未通过 WHERE 检查的行再去做分组和聚合计算。
4.7 更新数据(Updates)
你可以使用 UPDATE 命令更新已存在的行。假设你发现 11 月 28 日之后的温度读数都偏了 2 度,可以这样修正数据:
UPDATE weather
SET temp_hi = temp_hi - 2, temp_lo = temp_lo - 2
WHERE date > '1994-11-28';
查看数据的新状态:
SELECT *
FROM weather;
city temp_lo temp_hi prcp date
San Francisco 46 50 0.25 1994-11-27
San Francisco 41 55 0.0 1994-11-29
Hayward 35 52 NULL 1994-11-29
4.8 删除数据(Deletions)
可以使用 DELETE 命令从表中移除行。假设你不再关心 Hayward 的天气,那么可以用下面的语句从表中删除这些行:
DELETE FROM weather
WHERE city = 'Hayward';
所有属于 Hayward 的天气记录都被移除。
SELECT *
FROM weather;
city temp_lo temp_hi prcp date
San Francisco 46 50 0.25 1994-11-27
San Francisco 41 55 0.0 1994-11-29
在使用下面这种形式的语句时应当格外小心:
DELETE FROM table_name;
警告(Warning):如果不带任何限定条件,
DELETE将从给定表中移除所有行,使其变为空表。系统在执行此操作前不会请求确认。
五、常见数据类型
原文来源:DuckDB 官方文档「数据类型概览」页面(https://duckdb.org/docs/sql/data_types/overview)。以下为该页正文的完整中文翻译;数据类型名、别名、函数名与 SQL 代码示例均原样保留。
5.1 通用数据类型(General-Purpose Data Types)
下表列出了所有内置的通用数据类型。别名(aliases)列中列出的替代写法同样可用于引用这些类型,但请注意:别名不属于 SQL 标准,因此在其他数据库引擎中可能不被接受。
| 名称(Name) | 别名(Aliases) | 说明(Description) |
|---|---|---|
| BIGINT | INT8, LONG | 有符号八字节整数 |
| BIT | BITSTRING | 由 1 和 0 组成的字符串 |
| BLOB | BYTEA, BINARY, VARBINARY | 变长二进制数据 |
| BOOLEAN | BOOL, LOGICAL | 逻辑布尔值(true / false) |
| DATE | 日历日期(年、月、日) | |
| DECIMAL(prec, scale) | NUMERIC(prec, scale) | 固定精度数值,宽度(精度)与小数位数由参数指定;默认为 prec = 18、scale = 3 |
| DOUBLE | FLOAT8 | 双精度浮点数(8 字节) |
| FLOAT | FLOAT4, REAL | 单精度浮点数(4 字节) |
| HUGEINT | 有符号十六字节整数 | |
| INTEGER | INT4, INT, SIGNED | 有符号四字节整数 |
| INTERVAL | 日期/时间差值 | |
| JSON | JSON 对象(通过 json 扩展提供) | |
| SMALLINT | INT2, SHORT | 有符号二字节整数 |
| TIME | 当日时间(不含时区) | |
| TIMESTAMP WITH TIME ZONE | TIMESTAMPTZ | 使用当前时区的日期与时间组合 |
| TIMESTAMP | DATETIME | 日期与时间的组合 |
| TINYINT | INT1 | 有符号一字节整数 |
| UBIGINT | 无符号八字节整数 | |
| UHUGEINT | 无符号十六字节整数 | |
| UINTEGER | 无符号四字节整数 | |
| USMALLINT | 无符号二字节整数 | |
| UTINYINT | 无符号一字节整数 | |
| UUID | UUID 数据类型 | |
| VARCHAR | CHAR, BPCHAR, TEXT, STRING | 变长字符串 |
多种数据类型之间支持隐式和显式类型转换(typecasting),详见「类型转换」(Typecasting)页面。
5.2 嵌套 / 复合类型(Nested / Composite Types)
DuckDB 支持五种嵌套数据类型:ARRAY、LIST、MAP、STRUCT 和 UNION。每种类型适用于不同场景,且具有不同的结构。
| 名称 | 说明 | 作为列使用时的规则 | 由值构造(Build from values) | 在 DDL / CREATE 中定义 |
|---|---|---|---|---|
| ARRAY | 同类型数据值构成的有序定长序列。每一行在该 ARRAY 实例内的数据类型必须一致,且元素个数相同。 | [1, 2, 3] |
INTEGER[3] |
|
| LIST | 同类型数据值构成的有序序列。每一行在该 LIST 实例内的数据类型必须一致,但元素个数可以任意。 | [1, 2, 3] |
INTEGER[] |
|
| MAP | 由多个具名值构成的字典;每个键的数据类型相同,每个值的数据类型相同。键和值可以是任意类型,且二者可以互不相同。各行可以包含不同的键。 | map([1, 2], ['a', 'b']) |
MAP(INTEGER, VARCHAR) |
|
| STRUCT | 由多个具名值构成的字典;每个键都是字符串,但每个键对应的值可以是不同类型。每一行的键集合必须相同。 | {'i': 42, 'j': 'a'} |
STRUCT(i INTEGER, j VARCHAR) |
|
| UNION | 多种备选数据类型的联合,每个值中同一时刻只存储其中一种。UNION 还包含一个判别符(“tag”)值,用于检查和访问当前设置的成员类型。各行可以被设置为 UNION 的不同成员类型。 | union_value(num := 2) |
UNION(num INTEGER, text VARCHAR) |
5.2.1 更新嵌套类型的值
对嵌套类型的值执行更新时,DuckDB 会先执行删除操作,再执行插入操作。当该表使用了 ART 索引(无论是显式创建的索引,还是通过主键/唯一约束隐式创建的索引)时,这一行为可能导致意外的约束冲突。例如:
CREATE TABLE students (id INTEGER PRIMARY KEY, name VARCHAR);
INSERT INTO students VALUES (1, 'Student 1');
UPDATE tbl
SET j = [2]
WHERE i = 1;
Constraint Error: Duplicate key "i: 1" violates primary key constraint.
如果遇到这种意外的约束冲突,请查阅文档中关于已知索引限制的章节(https://duckdb.org/docs/sql/indexes)。
5.2.2 嵌套(Nesting)
ARRAY、LIST、MAP、STRUCT 和 UNION 类型可以任意深度地相互嵌套,只要遵守各自的类型规则即可。
包含 LIST 的 STRUCT:
SELECT {'birds': ['duck', 'goose', 'heron'], 'aliens': NULL, 'amphibians': ['frog', 'toad']};
包含 MAP 列表的 STRUCT:
SELECT {'test': [MAP([1, 5], [42.1, 45]), MAP([1, 5], [42.1, 45])]};
UNION 构成的列表:
SELECT [union_value(num := 2), union_value(str := 'ABC')::UNION(str VARCHAR, num INTEGER)];
5.3 性能影响(Performance Implications)
数据类型的选择对性能有显著影响。详细说明请参阅《性能指南》(Performance Guide)。
六、窗口函数
原文来源:DuckDB 官方文档「Window Functions」(https://duckdb.org/docs/sql/window_functions)。本节为该页正文的完整中文翻译;其中所有 SQL 示例、函数签名与输出表格均原样保留,未作改动。
DuckDB 支持窗口函数(window function),即可以利用多行数据为每一行计算一个值。窗口函数是阻塞型算子(blocking operator),也就是说它必须把全部输入缓冲下来才能开始计算,因此是 SQL 中内存开销最大的算子之一。
6.1 示例(Examples)
生成一列 row_number(),为每一行给出递增的标识:
SELECT row_number() OVER ()
FROM sales;
按时间顺序生成 row_number():
SELECT row_number() OVER (ORDER BY time)
FROM sales;
按地区分区、再按时间顺序生成 row_number():
SELECT row_number() OVER (PARTITION BY region ORDER BY time)
FROM sales;
按时间顺序,计算当前 amount 与上一行 amount 的差值:
SELECT amount - lag(amount) OVER (ORDER BY time)
FROM sales;
为每一行计算其在本地区销售总额中所占的百分比:
SELECT amount / sum(amount) OVER (PARTITION BY region)
FROM sales;
6.2 语法(Syntax)
function-name
(
expr
,
)
OVER
(
base-window-name
PARTITION BY
expr
,
ORDER BY
expr
ASC
DESC
NULLS
FIRST
LAST
,
ALL
ASC
DESC
NULLS
FIRST
LAST
frame-spec
)
window-name
窗口函数只能出现在 SELECT 子句中。若要在多个函数之间复用同一段 OVER 定义,可以使用语句级的 WINDOW 子句,并通过 OVER ⟨window-name⟩ 语法引用。
6.3 通用窗口函数(General-Purpose Window Functions)
下表列出了 DuckDB 提供的全部通用窗口函数:
| 函数名 | 说明 |
|---|---|
cume_dist() |
累积分布:(分区中排在当前行之前或与当前行并列的行数)/ 分区总行数。 |
dense_rank() |
当前行的排名,不留空号;该函数按并列组计数。 |
first(expr[ IGNORE NULLS]) |
返回窗口帧第一行上求得的 expr(若指定 IGNORE NULLS,则取 expr 第一个非空值所在行)。 |
first_value(expr[ IGNORE NULLS]) |
返回窗口帧第一行上求得的 expr(若指定 IGNORE NULLS,则取 expr 第一个非空值所在行)。 |
lag(expr[, offset[, default]][ IGNORE NULLS]) |
返回窗口帧中当前行向前 offset 行上求得的 expr(若指定 IGNORE NULLS,则在 expr 非空的行中向前数 offset 行);若不存在这样的行,则返回 default(其类型必须与 expr 相同)。offset 与 default 都相对于当前行求值。若省略,offset 默认为 1,default 默认为 NULL。 |
last(expr[ IGNORE NULLS]) |
返回窗口帧最后一行上求得的 expr(若指定 IGNORE NULLS,则在 expr 非空的行中取最后一行)。 |
last_value(expr[ IGNORE NULLS]) |
返回窗口帧最后一行上求得的 expr(若指定 IGNORE NULLS,则在 expr 非空的行中取最后一行)。 |
lead(expr[, offset[, default]][ IGNORE NULLS]) |
返回窗口帧中当前行向后 offset 行上求得的 expr(若指定 IGNORE NULLS,则在 expr 非空的行中向后数 offset 行);若不存在这样的行,则返回 default(其类型必须与 expr 相同)。offset 与 default 都相对于当前行求值。若省略,offset 默认为 1,default 默认为 NULL。 |
nth_value(expr, nth[ IGNORE NULLS]) |
返回窗口帧中第 nth 行上求得的 expr(若指定 IGNORE NULLS,则在 expr 非空的行中数第 nth 行;从 1 开始计数);若不存在这样的行,则返回 NULL。 |
ntile(num_buckets) |
返回一个介于 1 到 num_buckets 之间的整数,把分区尽可能均分。 |
percent_rank() |
当前行的相对排名:(rank() - 1) / (total partition rows - 1)。 |
rank_dense() |
当前行的排名,不留空号;等同于其第一个并列行的 row_number。 |
rank() |
当前行的排名,保留空号;等同于其第一个并列行的 row_number。 |
row_number() |
当前行在分区内的序号,从 1 开始计数。 |
6.3.1 cume_dist()
- 说明:累积分布:(分区中排在当前行之前或与当前行并列的行数)/ 分区总行数。
- 返回类型:
DOUBLE - 示例:
cume_dist()
6.3.2 dense_rank()
- 说明:当前行的排名,不留空号;该函数按并列组计数。
- 返回类型:
BIGINT - 示例:
dense_rank()
6.3.3 first(expr[ IGNORE NULLS])
- 说明:返回窗口帧第一行上求得的
expr(若指定IGNORE NULLS,则取expr第一个非空值所在行)。 - 返回类型:与
expr相同的类型 - 示例:
first(column) - 别名:
first_value(column)
6.3.4 first_value(expr[ IGNORE NULLS])
- 说明:返回窗口帧第一行上求得的
expr(若指定IGNORE NULLS,则取expr第一个非空值所在行)。 - 返回类型:与
expr相同的类型 - 示例:
first_value(column) - 别名:
first(column)
6.3.5 lag(expr[, offset[, default]][ IGNORE NULLS])
- 说明:返回窗口帧中当前行向前
offset行上求得的expr(若指定IGNORE NULLS,则在expr非空的行中向前数offset行);若不存在这样的行,则返回default(其类型必须与expr相同)。offset与default都相对于当前行求值。若省略,offset默认为 1,default默认为NULL。 - 返回类型:与
expr相同的类型 - 别名:
lag(column, 3, 0)
6.3.6 last(expr[ IGNORE NULLS])
- 说明:返回窗口帧最后一行上求得的
expr(若指定IGNORE NULLS,则在expr非空的行中取最后一行)。 - 返回类型:与
expr相同的类型 - 示例:
last(column) - 别名:
last_value(column)
6.3.7 last_value(expr[ IGNORE NULLS])
- 说明:返回窗口帧最后一行上求得的
expr(若指定IGNORE NULLS,则在expr非空的行中取最后一行)。 - 返回类型:与
expr相同的类型 - 示例:
last_value(column) - 别名:
last(column)
6.3.8 lead(expr[, offset[, default]][ IGNORE NULLS])
- 说明:返回窗口帧中当前行向后
offset行上求得的expr(若指定IGNORE NULLS,则在expr非空的行中向后数offset行);若不存在这样的行,则返回default(其类型必须与expr相同)。offset与default都相对于当前行求值。若省略,offset默认为 1,default默认为NULL。 - 返回类型:与
expr相同的类型 - 别名:
lead(column, 3, 0)
6.3.9 nth_value(expr, nth[ IGNORE NULLS])
- 说明:返回窗口帧中第
nth行上求得的expr(若指定IGNORE NULLS,则在expr非空的行中数第nth行;从 1 开始计数);若不存在这样的行,则返回NULL。 - 返回类型:与
expr相同的类型 - 别名:
nth_value(column, 2)
6.3.10 ntile(num_buckets)
- 说明:返回一个介于 1 到
num_buckets之间的整数,把分区尽可能均分。 - 返回类型:
BIGINT - 示例:
ntile(4)
6.3.11 percent_rank()
- 说明:当前行的相对排名:
(rank() - 1) / (total partition rows - 1)。 - 返回类型:
DOUBLE - 示例:
percent_rank()
6.3.12 rank_dense()
- 说明:当前行的排名,不留空号;等同于其第一个并列行的
row_number。 - 返回类型:
BIGINT - 示例:
rank_dense() - 别名:
rank()
6.3.13 rank()
- 说明:当前行的排名,保留空号;等同于其第一个并列行的
row_number。 - 返回类型:
BIGINT - 示例:
rank() - 别名:
rank_dense()
6.3.14 row_number()
- 说明:当前行在分区内的序号,从 1 开始计数。
- 返回类型:
BIGINT - 示例:
row_number()
6.4 聚合窗口函数(Aggregate Window Functions)
所有聚合函数都可以用在窗口语境中,包括可选的 FILTER 子句。其中 first 与 last 两个聚合函数会被对应的通用窗口函数同名遮蔽,由此带来的一个小差异是:这两个函数在窗口语境下不支持 FILTER 子句,但支持 IGNORE NULLS。
6.5 空值处理(Nulls)
所有接受 IGNORE NULLS 的通用窗口函数默认都会尊重空值(respect nulls)。这一默认行为也可以通过显式写出 RESPECT NULLS 来明确声明。
与之相反,所有聚合窗口函数(list 及其别名除外——它们可以通过 FILTER 来忽略空值)都会忽略空值,并且不接受 RESPECT NULLS。例如,sum(column) OVER (ORDER BY time) AS cumulativeColumn 计算的是累计和:当某一行的 column 为 NULL 时,该行的 cumulativeColumn 与它前一行保持相同。
6.6 求值机制(Evaluation)
窗口运算的工作方式是:先把一个关系(relation)切分成若干相互独立的分区(partition),对这些分区分别排序,然后为每一行根据其邻近行的值计算出一个新列。有些窗口函数只依赖分区边界与排序结果,而另一些(包括所有聚合函数)还会用到窗口帧(frame)。窗口帧用「当前行向前/向后多少行」来指定;这个距离既可以按行数给出,也可以借助分区的排序值和一个差值按值区间(range)给出。
完整语法见本页顶部的语法图;下面的示意图直观地展示了求值环境。
6.6.1 分区与排序(Partition and Ordering)
分区把一个关系切分成若干相互独立、互不相干的片段。分区是可选的;若不指定,则整个关系被视为一个分区。窗口函数无法访问当前行所在分区之外的值。
排序同样是可选的;但如果不排序,结果就是未定义的。每个分区都按同一套排序子句排序。
下面是一张发电量数据表,可作为 CSV 文件获取(power-plant-generation-history.csv)。加载数据时执行:
CREATE TABLE "Generation History" AS
FROM 'power-plant-generation-history.csv';
按电厂(Plant)分区、按日期(Date)排序后,数据布局如下:
| Plant | Date | MWh |
|---|---|---|
| Boston | 2019-01-02 | 564337 |
| Boston | 2019-01-03 | 507405 |
| Boston | 2019-01-04 | 528523 |
| Boston | 2019-01-05 | 469538 |
| Boston | 2019-01-06 | 474163 |
| Boston | 2019-01-07 | 507213 |
| Boston | 2019-01-08 | 613040 |
| Boston | 2019-01-09 | 582588 |
| Boston | 2019-01-10 | 499506 |
| Boston | 2019-01-11 | 482014 |
| Boston | 2019-01-12 | 486134 |
| Boston | 2019-01-13 | 531518 |
| Worcester | 2019-01-02 | 118860 |
| Worcester | 2019-01-03 | 101977 |
| Worcester | 2019-01-04 | 106054 |
| Worcester | 2019-01-05 | 92182 |
| Worcester | 2019-01-06 | 94492 |
| Worcester | 2019-01-07 | 99932 |
| Worcester | 2019-01-08 | 118854 |
| Worcester | 2019-01-09 | 113506 |
| Worcester | 2019-01-10 | 96644 |
| Worcester | 2019-01-11 | 93806 |
| Worcester | 2019-01-12 | 98963 |
| Worcester | 2019-01-13 | 107170 |
下文将使用这张表(或它的一小部分)来演示窗口函数求值的各个环节。
最简单的窗口函数是 row_number()。它只是在分区内从 1 开始编号,查询如下:
SELECT
"Plant",
"Date",
row_number() OVER (PARTITION BY "Plant" ORDER BY "Date") AS "Row"
FROM "Generation History"
ORDER BY 1, 2;
结果如下:
| Plant | Date | Row |
|---|---|---|
| Boston | 2019-01-02 | 1 |
| Boston | 2019-01-03 | 2 |
| Boston | 2019-01-04 | 3 |
| … | … | … |
| Worcester | 2019-01-02 | 1 |
| Worcester | 2019-01-03 | 2 |
| Worcester | 2019-01-04 | 3 |
| … | … | … |
注意:即便函数内部用了 ORDER BY 子句,最终结果集也不一定是排序的;如果需要有序输出,SELECT 本身还必须显式排序。
6.6.2 窗口帧(Framing)
窗口帧为每一行待求值的函数圈定了一组相对行。它以当前行为基准,用一个表达式指定向前(PRECEDING)或向后(FOLLOWING)的距离。这个距离既可以按行数(ROWS)给出,也可以用排序值的差值(RANGE delta)给出。使用 RANGE 时,排序表达式只能有一个,并且该排序值必须支持加减运算(即数值或 INTERVAL 类型)。窗口帧的默认范围是从 UNBOUNDED PRECEDING 到 CURRENT ROW。窗口帧的起点不能晚于终点,否则非法。借助 EXCLUDE 子句,可以把当前行周围的某些行排除在窗口帧之外。
6.6.2.1 ROW 窗口帧(ROW Framing)
下面是一个使用聚合函数的简单 ROWS 窗口帧查询:
SELECT points,
sum(points) OVER (
ROWS BETWEEN 1 PRECEDING
AND 1 FOLLOWING) we
FROM results;
该查询计算每一行 points 与其上下各一行 points 之和。
注意:在分区的边缘处,只有两个值被相加。这是因为窗口帧会被裁剪到分区边界内。
6.6.2.2 RANGE 窗口帧(RANGE Framing)
回到发电量数据。假设数据存在噪声,我们希望为每个电厂计算一个 7 天移动平均来平滑噪声。可以使用如下窗口查询:
SELECT "Plant", "Date",
avg("MWh") OVER (
PARTITION BY "Plant"
ORDER BY "Date" ASC
RANGE BETWEEN INTERVAL 3 DAYS PRECEDING
AND INTERVAL 3 DAYS FOLLOWING)
AS "MWh 7-day Moving Average"
FROM "Generation History"
ORDER BY 1, 2;
该查询按电厂(Plant)分区(把不同电厂的数据分开),按日期(Date)对每个电厂的分区排序(让相邻的发电量测量值挨在一起),并在每一天前后各取 3 天作为 RANGE 窗口帧来求 avg(以应对缺失日期)。结果如下:
| Plant | Date | MWh 7-day Moving Average |
|---|---|---|
| Boston | 2019-01-02 | 517450.75 |
| Boston | 2019-01-03 | 508793.20 |
| Boston | 2019-01-04 | 508529.83 |
| … | … | … |
| Boston | 2019-01-13 | 499793.00 |
| Worcester | 2019-01-02 | 104768.25 |
| Worcester | 2019-01-03 | 102713.00 |
| Worcester | 2019-01-04 | 102249.50 |
| … | … | … |
6.6.2.3 EXCLUDE 子句
EXCLUDE 子句允许把当前行周围的某些行排除在窗口帧之外,可选项如下:
EXCLUDE NO OTHERS:不排除任何行(默认)。EXCLUDE CURRENT ROW:把当前行排除在窗口帧之外。EXCLUDE GROUP:把当前行以及它的所有并列行(按ORDER BY指定的列判定)一起排除在窗口帧之外。EXCLUDE TIES:只把当前行的并列行排除在窗口帧之外。
6.7 WINDOW 子句
同一个 SELECT 中可以写多个不同的 OVER 子句,它们会被分别计算。但很多时候,我们希望多个窗口函数共用同一种布局。这时可以用 WINDOW 子句定义一个具名窗口,供多个窗口函数复用:
SELECT "Plant", "Date",
min("MWh") OVER seven AS "MWh 7-day Moving Minimum",
avg("MWh") OVER seven AS "MWh 7-day Moving Average",
max("MWh") OVER seven AS "MWh 7-day Moving Maximum"
FROM "Generation History"
WINDOW seven AS (
PARTITION BY "Plant"
ORDER BY "Date" ASC
RANGE BETWEEN INTERVAL 3 DAYS PRECEDING
AND INTERVAL 3 DAYS FOLLOWING)
ORDER BY 1, 2;
这三个窗口函数还会共享同一份数据布局,从而提升性能。
在同一个 WINDOW 子句中,可以用逗号分隔定义多个窗口:
SELECT "Plant", "Date",
min("MWh") OVER seven AS "MWh 7-day Moving Minimum",
avg("MWh") OVER seven AS "MWh 7-day Moving Average",
max("MWh") OVER seven AS "MWh 7-day Moving Maximum",
min("MWh") OVER three AS "MWh 3-day Moving Minimum",
avg("MWh") OVER three AS "MWh 3-day Moving Average",
max("MWh") OVER three AS "MWh 3-day Moving Maximum"
FROM "Generation History"
WINDOW
seven AS (
PARTITION BY "Plant"
ORDER BY "Date" ASC
RANGE BETWEEN INTERVAL 3 DAYS PRECEDING
AND INTERVAL 3 DAYS FOLLOWING),
three AS (
PARTITION BY "Plant"
ORDER BY "Date" ASC
RANGE BETWEEN INTERVAL 1 DAYS PRECEDING
AND INTERVAL 1 DAYS FOLLOWING)
ORDER BY 1, 2;
上面这些查询都没有用到 WHERE、GROUP BY 等常见子句。对于更复杂的查询,可以参考 SELECT 语句中各子句的标准顺序来确定 WINDOW 子句所处的位置。
6.8 使用 QUALIFY 过滤窗口函数结果
窗口函数是在 WHERE 与 HAVING 子句已经求值之后才执行的,因此无法直接用这两个子句过滤窗口函数的结果。QUALIFY 子句正是为了避免为此再写一个子查询(subquery)或 WITH 子句而设计的。
6.9 箱线图查询(Box and Whisker Queries)
所有聚合函数都可以用作窗口函数,包括复杂的统计函数。这些函数的实现都已针对窗口运算做过优化,我们可以借助窗口语法直接生成绘制移动箱线图(moving box-and-whisker plot)所需的数据:
SELECT "Plant", "Date",
min("MWh") OVER seven AS "MWh 7-day Moving Minimum",
quantile_cont("MWh", [0.25, 0.5, 0.75]) OVER seven
AS "MWh 7-day Moving IQR",
max("MWh") OVER seven AS "MWh 7-day Moving Maximum",
FROM "Generation History"
WINDOW seven AS (
PARTITION BY "Plant"
ORDER BY "Date" ASC
RANGE BETWEEN INTERVAL 3 DAYS PRECEDING
AND INTERVAL 3 DAYS FOLLOWING)
ORDER BY 1, 2;
七、Python 入门
原文来源:DuckDB 官方文档「Python API」概览页(https://duckdb.org/docs/api/python/overview)。以下按页面结构完整翻译;所有 Python 代码块与运行输出原样保留,未作改动。
7.1 安装
DuckDB 的 Python API 可通过 pip 安装:pip install duckdb。详情请参见安装页面。也可以使用 conda 安装 DuckDB:conda install python-duckdb -c conda-forge。
Python 版本:DuckDB 要求 Python 3.7 或更新版本。
7.2 基本 API 用法
使用 DuckDB 运行 SQL 查询最直接的方式是调用 duckdb.sql 命令:
import duckdb
duckdb.sql("SELECT 42").show()
该语句会在一个内存数据库(in-memory database)上运行查询,该数据库全局存储于 Python 模块内部。查询结果以关系(Relation)形式返回。关系是对查询的一种符号化表示——只有在结果被取出或被请求打印到屏幕时,查询才会真正执行。
可以将关系存入变量,并在后续查询中当作表来引用,从而以增量方式构建查询:
import duckdb
r1 = duckdb.sql("SELECT 42 AS i")
duckdb.sql("SELECT i * 2 AS k FROM r1").show()
7.3 数据输入
DuckDB 可从多种格式摄取数据——无论是磁盘上的还是内存中的。详见数据摄取(data ingestion)页面。
import duckdb
duckdb.read_csv("example.csv") # read a CSV file into a Relation
duckdb.read_parquet("example.parquet") # read a Parquet file into a Relation
duckdb.read_json("example.json") # read a JSON file into a Relation
duckdb.sql("SELECT * FROM 'example.csv'") # directly query a CSV file
duckdb.sql("SELECT * FROM 'example.parquet'") # directly query a Parquet file
duckdb.sql("SELECT * FROM 'example.json'") # directly query a JSON file
7.4 DataFrame
DuckDB 可直接查询 Pandas DataFrame、Polars DataFrame 以及 Arrow 表。请注意:这些表是只读的——无法通过 INSERT 或 UPDATE 语句对其进行修改。
7.4.1 Pandas
直接查询 Pandas DataFrame:
import duckdb
import pandas as pd
pandas_df = pd.DataFrame({"a": [42]})
duckdb.sql("SELECT * FROM pandas_df")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
7.4.2 Polars
直接查询 Polars DataFrame:
import duckdb
import polars as pl
polars_df = pl.DataFrame({"a": [42]})
duckdb.sql("SELECT * FROM polars_df")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
7.4.3 PyArrow
直接查询 PyArrow 表:
import duckdb
import pyarrow as pa
arrow_table = pa.Table.from_pydict({"a": [42]})
duckdb.sql("SELECT * FROM arrow_table")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
7.5 结果转换
DuckDB 支持将查询结果高效地转换为多种格式。详见结果转换(result conversion)页面。
import duckdb
duckdb.sql("SELECT 42").fetchall() # Python objects
duckdb.sql("SELECT 42").df() # Pandas DataFrame
duckdb.sql("SELECT 42").pl() # Polars DataFrame
duckdb.sql("SELECT 42").arrow() # Arrow Table
duckdb.sql("SELECT 42").fetchnumpy() # NumPy Arrays
7.6 将数据写入磁盘
DuckDB 支持将关系(Relation)对象直接以多种格式写入磁盘。作为替代方案,也可以使用 COPY 语句通过 SQL 将数据写入磁盘。
import duckdb
duckdb.sql("SELECT 42").write_parquet("out.parquet") # Write to a Parquet file
duckdb.sql("SELECT 42").write_csv("out.csv") # Write to a CSV file
duckdb.sql("COPY (SELECT 42) TO 'out.parquet'") # Copy to a Parquet file
7.7 连接选项
应用程序可通过 duckdb.connect() 方法打开一个新的 DuckDB 连接。
7.7.1 使用内存数据库
通过 duckdb.sql() 使用 DuckDB 时,它操作的是一个内存数据库——即不会有任何表持久化到磁盘。调用 duckdb.connect() 时不带任何参数,会返回一个同样使用内存数据库的连接:
import duckdb
con = duckdb.connect()
con.sql("SELECT 42 AS x").show()
7.7.2 持久化存储
duckdb.connect(dbname) 会创建一个连接到持久化数据库的连接。写入该连接的所有数据都会被持久化,并且可以通过重新连接到同一个文件再次加载——无论是从 Python 还是从其他 DuckDB 客户端。
import duckdb
# create a connection to a file called 'file.db'
con = duckdb.connect("file.db")
# create a table and load data into it
con.sql("CREATE TABLE test (i INTEGER)")
con.sql("INSERT INTO test VALUES (42)")
# query the table
con.table("test").show()
# explicitly close the connection
con.close()
# Note: connections also closed implicitly when they go out of scope
也可以使用上下文管理器来确保连接被关闭:
import duckdb
with duckdb.connect("file.db") as con:
con.sql("CREATE TABLE test (i INTEGER)")
con.sql("INSERT INTO test VALUES (42)")
con.table("test").show()
# the context manager closes the connection automatically
7.7.3 配置
duckdb.connect() 接受一个 config 字典,可在其中指定配置选项。例如:
import duckdb
con = duckdb.connect(config = {'threads': 1})
7.8 连接对象与模块
连接对象与 duckdb 模块可以互换使用——它们支持相同的方法。唯一的区别在于:使用 duckdb 模块时,底层使用的是一个全局的内存数据库。
注意:如果你正在开发一个供他人使用的软件包,并在其中使用了 DuckDB,建议创建连接对象,而不是直接调用
duckdb模块上的方法。这是因为duckdb模块使用的是一个共享的全局数据库——如果在多个不同的软件包中同时使用,可能会产生难以调试的问题。
7.9 在并行 Python 程序中使用连接
DuckDBPyConnection 对象不是线程安全的。如果你希望从多个线程写入同一个数据库,请使用 DuckDBPyConnection.cursor() 方法为每个线程创建一个游标(cursor)。
7.10 加载与安装扩展
DuckDB 的 Python API 提供了安装和加载扩展(extension)的函数,其效果等价于分别运行 INSTALL 和 LOAD 这两条 SQL 命令。下面的示例安装并加载了 spatial 扩展:
import duckdb
con = duckdb.connect()
con.install_extension("spatial")
con.load_extension("spatial")
7.10.1 社区扩展
要加载社区扩展,请在 install_extension 方法中使用 repository="community" 参数。
例如,按如下方式安装并加载 h3 社区扩展:
import duckdb
con = duckdb.connect()
con.install_extension("h3", repository="community")
con.load_extension("h3")
7.10.2 未签名扩展
要加载未签名扩展,请在 duckdb.connect() 方法中使用 config = {"allow_unsigned_extensions": "true"} 参数。
八、附录:官方英文源页面
本文档各章节对应的 DuckDB 官方英文源页面如下(翻译时文档站版本为官网 1.0 (stable)):
| 章节 | 官方英文源页面 URL |
|---|---|
| 一、DuckDB 简介与特性 | https://duckdb.org/ |
| 二、安装 | https://duckdb.org/docs/installation/ |
| 三、快速上手与 CLI 命令行 | https://duckdb.org/docs/api/cli |
| 四、核心 SQL 语法(SQL 教程) | https://duckdb.org/docs/sql/introduction |
| 五、常见数据类型 | https://duckdb.org/docs/sql/data_types/overview |
| 六、窗口函数 | https://duckdb.org/docs/sql/window_functions |
| 七、Python 入门 | https://duckdb.org/docs/api/python/overview |
说明:官网原
/docs/sql/data_types/路径已失效,本节以已验证可用的/docs/sql/data_types/overview为准;官网另提供整站 PDF(https://blobs.duckdb.org/docs/duckdb-docs-0.10.3.pdf)可作为补充参考。