本文档由 DuckDB 官方网站「核心入门」范围内的文档页面翻译整理而成,共七章:DuckDB 简介与特性、安装、快速上手与 CLI、核心 SQL 语法、常见数据类型、窗口函数、Python 入门。

翻译原则:正文逐句完整译为简体中文,语气正式、书面、客观;所有代码示例、SQL 语句、运行输出表格、命令行与校验值均原样保留(含英文注释,未作改动);SQL 关键词、数据类型名与函数名保留英文原文,首次出现处括注中文含义;全篇术语译法统一(如 relation=关系、schema=模式、aggregate function=聚合函数、window function=窗口函数、in-memory database=内存数据库、persistent database=持久化数据库、extension=扩展、DataFrame=DataFrame、CLI=命令行界面)。

文末附各章节对应的官方英文源页面地址(第八、附录)。

–虑得先生整理

目录

  1. DuckDB 简介与特性
  2. 安装
  3. 快速上手与 CLI 命令行
  4. 核心 SQL 语法(SQL 教程)
  5. 常见数据类型
  6. 窗口函数
  7. Python 入门
  8. 附录:官方英文源页面

一、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
  • Linux amd64:duckdb_cli-linux-amd64.zip
    • SHA256:82c75b22b8035204122da84528f6954fb80c8749f0c9eeb9709e33d0b7933fe5
  • Linux arm64:duckdb_cli-linux-arm64.zip
    • SHA256:3e29952507ebd202e8d0e2678df4490689bfc9c86534e240b541791b969df0e1

Windows

通过 winget 包管理器安装:

winget install DuckDB.cli

或直接下载压缩包:

  • Windows amd64:duckdb_cli-windows-amd64.zip
    • SHA256:3fab5174658bf16189c80eaa047b3e7737847491c9e4433a985adf014618cbd9
  • Windows arm64:duckdb_cli-windows-arm64.zip
    • SHA256:b753187a285f491bf7e2dfbb0b893f551390806f6944d9f1764a7aa7e69d9dbb

注意: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

通过 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
  • Linux arm64:duckdb_odbc-linux-arm64.zip
    • SHA256:1a50bae51ee15290fccd9813620e4803e9c55d082538032aaff192d4e2e978de
  • macOS(universal):duckdb_odbc-osx-universal.zip
    • SHA256:6235d7fd73e956d60e4cf03cd2a5461026edb9ec1fb92962fd92ca4954b64ed6
  • Windows amd64:duckdb_odbc-windows-amd64.zip
    • SHA256:9ce35378ef7e6a5c6a47e37d0d2af0b87599aa4238ed513e21abf4a262d605f8

注意:ODBC 客户端目前尚不提供 Windows / arm64 平台的二进制包。

2.7 C / C++

按平台下载库文件(libduckdb):

  • Linux amd64:libduckdb-linux-amd64.zip
    • SHA256:ce859962fe96ca952d53571964dafa0168644283a76a2c669b3f73120d710edb
  • Linux arm64:libduckdb-linux-arm64.zip
    • SHA256:4aa05a74956b1d57f05a139623943231caeee36286c9f42ff10dc278b6df0b6e
  • macOS(universal):libduckdb-osx-universal.zip
    • SHA256:a82bde325dcbb6d1011e1014673eb288e8c1fd5f5b755d47fd245e8b37194817
  • Windows amd64:libduckdb-windows-amd64.zip
    • SHA256:704877e85b556640b0f20a0f07a1c1f404320210b245f51b6da0087a1c92bb76
  • Windows arm64:libduckdb-windows-arm64.zip
    • SHA256:851f0b89659f9ebfdecc99a9371942117fe4bc9fd9521af82fbe52be073534b6

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)可作为补充参考。