# DuckDB

> 以 pgcli 插件方式安装嵌入式 OLAP 分析数据库（宿主二进制）。

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

[DuckDB](https://duckdb.org/) 是嵌入式分析数据库——常被称作"SQLite for analytics"。
C++ 编写的列式引擎，向量化执行，可以直接查询 Parquet、CSV、JSON 和 Arrow 文件，
无需服务器进程。把它当成一个单二进制的 OLAP 引擎，指向你的数据即可使用：
加载 `httpfs` 扩展后，`SELECT * FROM read_parquet('s3://bucket/data.parquet')`
开箱即用。

作为 pgcli 插件，DuckDB 是**宿主二进制**形态：没有容器，没有守护进程。
`pg addon install duckdb` 从 GitHub releases 下载官方 CLI zip（sha256 校验；
`PATH` 上已有 `duckdb` 则直接复用），安装 `httpfs` 扩展（parquet 和 json
已内置于二进制中），并在 `pg.yaml` 中记录条目。

> **平台**：仅 Linux，仅 amd64（macOS 及其他架构明确拒绝并给出错误提示）。
> DuckDB 是一个独立的 CLI 工具——不作为后台服务运行、不监听端口、不会传播进
> PG 实例容器。

## 为什么在 pgcli 旁边装 DuckDB

DuckDB 填补了 PostgreSQL 行存引擎不擅长的分析场景：对大量 Parquet/CSV 数据集的
即席查询、数据湖探索、加载进 PostgreSQL 之前的 ETL 预处理，或者对已经躺在 S3 里
的文件做快速聚合。pgcli 已经管理了多种 S3 存储（MinIO、silo、rustfs、garage、
seaweedfs）——DuckDB 通过 `httpfs` 直接查询其中的内容。

## 用法

```bash
# 默认：v1.5.6 LTS，安装 httpfs 扩展（parquet/json 已内置）
pg addon install duckdb

# 指定版本
pg addon install duckdb --version 2.0.0

# 自定义名称和数据目录
pg addon install duckdb --name analytics --data-dir /srv/duckdb

# 跳过扩展安装
pg addon install duckdb --no-extensions

# 重新安装并更换参数（条目已存在时）
pg addon install duckdb --version 2.0.0 --force
```

安装完成后，启动交互式 shell 或执行一次性查询：

```bash
# 交互式 shell（数据目录作为工作目录）
pg addon duckdb shell

# 对本地 Parquet 文件的一次性查询
pg addon duckdb shell -- -c "SELECT * FROM read_parquet('/tmp/data.parquet')"

# 直接查询 S3（需要 httpfs 扩展，默认已安装）
duckdb -c "SELECT count(*) FROM read_parquet('s3://bucket/data/*.parquet')"

# 打开持久化数据库文件
duckdb ~/.pgcli/addon/duckdb/duckdb/analytics.duckdb
```

## 参数

| 参数 | 默认 | 说明 |
|---|---|---|
| `--name` | `duckdb` | 实例名（配置 map 的键） |
| `--version` | `1.5.6` | 要下载的版本（sha256 已 pin 在 pgcli 源码里；支持 `1.5.6` LTS 和 `2.0.0`） |
| `--data-dir` | `<base>/addon/duckdb/<name>` | `.duckdb` 数据库文件的默认目录 |
| `--install-dir` | `/usr/local/bin` | 宿主二进制落点（不可写时退到 `~/.local/bin`——注意保持其在 `PATH` 上） |
| `--no-extensions` | 关 | 跳过安装 `httpfs` 扩展（parquet 和 json 已内置于二进制中） |
| `--force` | 关 | 按命令行参数重建配置条目 |

## 日常管理

```bash
pg addon duckdb status --name duckdb        # 二进制版本、路径、数据目录
pg addon duckdb shell --name duckdb         # 交互式 DuckDB 会话
pg addon duckdb shell -- -c "SELECT 1"      # 一次性查询（-- 后面的参数传给 duckdb）
pg addon list                               # duckdb 显示在"Analytics add-ons"下
pg addon start duckdb                       # 无操作（CLI 工具，非守护进程）
pg addon stop  duckdb                       # 无操作（CLI 工具，非守护进程）
pg addon remove duckdb --name duckdb        # 忘掉配置；二进制和数据目录保留
pg addon remove duckdb --name duckdb --clean-data   # 同时删除数据目录
```

`remove` 从不删除二进制——它可能被多个命名实例共享。加 `--clean-data` 删除
数据目录（数据库文件）；不加则保留目录并打印提示。

## Server/Client 模式（v2.0.0+）

DuckDB v2.0.0 引入了 **Quack 协议**，支持原生 server/client 模式。
DuckDB 可以通过 HTTP 对外提供数据库服务，多个客户端可以同时查询同一个实例。

### 启动服务端

```bash
# 交互式 server（stdin 必须保持打开）
duckdb /path/to/db.duckdb -serve

# 后台 server（重定向 stdin 以保持存活）
nohup duckdb /path/to/db.duckdb -serve < /dev/zero > /tmp/duckdb-serve.log 2>&1 &

# 或在 SQL 中使用 quack_serve()
duckdb -c "INSTALL quack; LOAD quack; CALL quack_serve(token='mytoken')"
```

服务端默认监听 `localhost:9494`，启动时会打印自动生成的 auth token。
默认只接受本地连接——如需远程访问，需在前面放一个 TLS 终止反向代理
（参见 [DuckDB 文档](https://duckdb.org/docs/2.0/quack/setup/reverse_proxy.html)）。

#### 跨主机访问（绑定 0.0.0.0）

要接受来自任意网络接口的连接，使用 `quack_serve()` 并指定参数（而非 `-serve`）：

```sql
-- 服务端：绑定到所有接口的 9494 端口
CALL quack_serve(
  listen_uri='quack:0.0.0.0:9494',
  allow_other_hostname=true,  -- 必须，允许非 localhost 绑定
  disable_ssl=true,            -- 可选，默认是 HTTPS
  token='mytoken'
);
```

需要三个参数：
- `listen_uri='quack:0.0.0.0:9494'` — 绑定到所有接口（不只是 localhost）
- `allow_other_hostname=true` — 绕过仅限 localhost 的限制
- `disable_ssl=true` — 使用纯 HTTP（默认是 HTTPS 并自动生成证书）

客户端使用服务端 IP 地址连接：

```bash
# 客户端连接远程服务端
duckdb -c "ATTACH 'quack:192.0.2.10:9494' AS qk (TOKEN 'mytoken'); CONNECT qk; SELECT * FROM t"
```

> **安全警告**：绑定 `0.0.0.0` 并使用 `disable_ssl=true` 会将数据库以明文形式暴露到网络。
> 仅在可信网络或防火墙后面使用。生产环境请使用带 TLS 的反向代理。

### 客户端连接

```bash
# 使用 -connect 参数（v2.0.0+）
duckdb -connect -c "SELECT * FROM t"

# 或通过 SQL（简写形式，默认 localhost）
duckdb -c "ATTACH 'quack:localhost' AS qk (TOKEN 'BAED...'); CONNECT qk; SELECT * FROM t"

# 显式 ip:port 形式（远程连接或非默认端口时使用）
duckdb -c "ATTACH 'quack:127.0.0.1:9494' AS qk (TOKEN 'BAED...'); CONNECT qk; SELECT * FROM t"
```

`-connect` 参数会自动发现服务端输出的 token。手动连接时，从服务端启动
信息中复制 token 即可。

### 注意事项

- **stdin 必须保持打开**：服务端在 stdin 关闭后会退出（后台运行用 `< /dev/zero` 或 `tail -f /dev/null |`）
- **仅限本地**：远程访问需要带 TLS 终止的反向代理
- **Auth token**：自动生成，启动时打印，客户端连接时必须提供
- **pgcli 不管理 Quack server**：`pg addon start/stop duckdb` 仍然是无操作——Quack server 是一个独立进程，需要自行管理

## 扩展

DuckDB 的扩展系统在首次使用时安装共享库。pgcli 在安装时预装 `httpfs`——
它启用通过 HTTP/S3 读取文件的能力。`parquet` 和 `json` 是静态链接在 DuckDB CLI
二进制里的，始终可用，无需网络下载。

- **`httpfs`** —— 通过 HTTP/S3 读取文件（`read_parquet('s3://...')`）
- **`parquet`** —— Parquet 文件读写器（内置）
- **`json`** —— JSON 文件读取器（`read_json_auto(...)`）（内置）

后续可以从 DuckDB shell 里安装更多扩展：

```sql
INSTALL 'spatial';       -- 地理空间函数
INSTALL 'sqlite';        -- 读写 SQLite 数据库
INSTALL 'mysql';         -- 直接查询 MySQL
LOAD 'httpfs';           -- 在当前会话中加载已安装的扩展
```

如果希望手动管理扩展，或安装时主机无法上网，可在安装时加 `--no-extensions`。

## 访问 S3、Parquet、JSON 和 CSV 数据

DuckDB 天生为文件分析查询而生——无需服务器、无需 `COPY`、无需导入步骤。
直接指向你的数据即可。

### Parquet 文件

```sql
-- 单个文件
SELECT * FROM read_parquet('/tmp/events.parquet');

-- 通配符：目录下所有 parquet 文件
SELECT * FROM read_parquet('/data/logs/*.parquet');

-- schema 推断 + 计数
SELECT count(*) FROM read_parquet('s3://bucket/year=2026/*.parquet');

-- 指定列 + 过滤
SELECT user_id, event, ts
FROM read_parquet('/data/events.parquet')
WHERE ts > '2026-01-01'
LIMIT 100;
```

### JSON 文件

```sql
-- 自动推断 schema（支持 JSON lines 和嵌套 JSON）
SELECT * FROM read_json_auto('/tmp/users.json');

-- 显式 schema
SELECT * FROM read_json('/tmp/users.json',
    columns = {'id': 'INTEGER', 'name': 'VARCHAR', 'email': 'VARCHAR'},
    format = 'newline_delimited');

-- 嵌套对象：用 ->> 提取字段
SELECT
    data->>'user_id' AS user_id,
    data->>'event'   AS event
FROM read_json_auto('/tmp/events.jsonl');
```

### CSV 文件

```sql
-- 自动推断表头和类型
SELECT * FROM read_csv_auto('/tmp/sales.csv');

-- 显式列类型（自动推断不准时）
SELECT * FROM read_csv('/tmp/sales.csv',
    columns = {'id': 'INTEGER', 'amount': 'DOUBLE', 'date': 'DATE'},
    header = true);

-- TSV 或自定义分隔符
SELECT * FROM read_csv_auto('/tmp/data.tsv', delim = '\t');
```

### S3 访问（httpfs 扩展）

`httpfs` 扩展启用 S3 访问。凭据和端点通过 `CREATE SECRET`（v2.0.0+）或传统
`SET` 命令（v1.5.x）配置。

#### DuckDB v2.0.0+ — `CREATE SECRET`

```sql
-- 配置 S3 凭据（v2.0.0+ 语法）
CREATE SECRET (
  TYPE S3,
  ENDPOINT 'minio.example.com:9000',
  KEY_ID 'admin',
  SECRET 'your-secret-key',
  USE_SSL false,
  URL_STYLE 'path'   -- MinIO 及兼容存储必须设置
);

-- 从 S3 直接查询 Parquet
SELECT count(*) FROM read_parquet('s3://my-bucket/events/2026/*.parquet');

-- 从 S3 查询 CSV
SELECT * FROM read_csv_auto('s3://my-bucket/users.csv') LIMIT 10;

-- 从 S3 查询 JSON
SELECT * FROM read_json_auto('s3://my-bucket/config.json');

-- 写回 Parquet 到 S3（需要桶的写权限）
COPY (SELECT * FROM read_parquet('/tmp/*.parquet') WHERE year = 2026)
  TO 's3://output-bucket/filtered/2026.parquet' (FORMAT parquet);
```

#### DuckDB v1.5.x — 传统 `SET` 命令

```sql
-- 在 DuckDB 会话中配置 S3 凭据（仅 v1.5.x）
SET s3_endpoint = 'minio.example.com:9000';
SET s3_access_key_id = 'admin';
SET s3_secret_access_key = 'your-secret-key';
SET s3_use_ssl = false;   -- https 端点用 true
SET s3_region = 'us-east-1';   -- MinIO 写 'us-east-1' 即可（region 被忽略）
SET s3_url_style = 'path';   -- MinIO 及兼容存储必须设置
```

#### 连接 pgcli 管理的 S3 存储

pgcli 管理了多种 S3 兼容存储（MinIO、silo、rustfs、garage、seaweedfs）。
DuckDB 直接查询它们——`s3_endpoint` 就是存储的 HTTP 地址，凭据来自
`pg addon password`。

先创建 S3 存储实例和桶。MinIO、silo（Pigsty 的 MinIO 分支）和 rustfs
（Rust 重新实现）的设置方式完全相同——它们共用 `pg mc` 包装命令，DuckDB
端也需要相同的 `s3_url_style = 'path'`：

```bash
# MinIO
pg addon install minio --name ms3
S3_SECRET=$(pg addon password minio --name ms3)

# 或 silo（功能面一致，与 minio 共用端口池）
pg addon install silo --name ms3
S3_SECRET=$(pg addon password silo --name ms3)

# 或 rustfs（Rust S3 兼容存储，仅 Linux）
pg addon install rustfs --name ms3
S3_SECRET=$(pg addon password rustfs --name ms3)

# 通过 pg mc 包装命令创建桶
pg mc alias set ms3 http://127.0.0.1:9000 admin "${S3_SECRET}"
pg mc mb ms3/my-bucket

# 上传测试数据（可选）
pg mc cp /tmp/data.parquet ms3/my-bucket/data.parquet
```

然后从 DuckDB 查询——MinIO、silo、rustfs 及兼容存储需要
`URL_STYLE 'path'`（不支持 virtual-host style）：

```bash
# DuckDB v2.0.0+
duckdb -c "
  CREATE SECRET (
    TYPE S3,
    ENDPOINT '127.0.0.1:9000',
    KEY_ID 'admin',
    SECRET '${S3_SECRET}',
    USE_SSL false,
    URL_STYLE 'path'
  );
  SELECT count(*) FROM read_parquet('s3://my-bucket/data/*.parquet');
"

# DuckDB v1.5.x（传统 SET 语法）
duckdb -c "
  SET s3_endpoint = '127.0.0.1:9000';
  SET s3_access_key_id = 'admin';
  SET s3_secret_access_key = '${S3_SECRET}';
  SET s3_use_ssl = false;
  SET s3_url_style = 'path';
  SELECT count(*) FROM read_parquet('s3://my-bucket/data/*.parquet');
"
```

如需更持久的配置，创建 DuckDB 启动文件（`~/.duckdbrc`），其中的命令
会在每次会话启动时执行：

```sql
-- ~/.duckdbrc (DuckDB v2.0.0+)
CREATE SECRET (
  TYPE S3,
  ENDPOINT 'minio.example.com:9000',
  KEY_ID 'admin',
  SECRET 'your-secret-key',
  USE_SSL false,
  URL_STYLE 'path'
);
```

### S3 环境变量

DuckDB 从 `DUCKDB_S3_*` 环境变量读取端点和 SSL 设置，但凭据必须使用标准 AWS
变量（`AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY`）——DuckDB 的
`enable_global_s3_configuration` 会自动检测这些变量。

> **v2.0.0 限制**：`URL_STYLE` 无法通过环境变量设置。它默认为 virtual-host，
> 会导致 MinIO/silo/rustfs（需要 path-style）无法访问。对于这些存储，
> 应使用 `CREATE SECRET` 并指定 `URL_STYLE 'path'`。

```bash
# 端点和 SSL 通过 DUCKDB_S3_*
export DUCKDB_S3_ENDPOINT='127.0.0.1:9000'
export DUCKDB_S3_USE_SSL='false'

# 凭据通过 AWS_*（标准变量，DuckDB 自动检测）
export AWS_ACCESS_KEY_ID='admin'
export AWS_SECRET_ACCESS_KEY='your-secret-key'

# URL 样式仍需在会话或启动文件中设置（v1.5.x）
duckdb -c "SET s3_url_style = 'path'; SELECT * FROM read_parquet('s3://my-bucket/data.parquet');"

# v2.0.0 中 URL_STYLE 必须通过 CREATE SECRET 设置——环境变量无法覆盖
# MinIO/silo/rustfs 不支持 virtual-host style，必须显式指定 path
duckdb -c "
  CREATE SECRET (TYPE S3, ENDPOINT '127.0.0.1:9000', KEY_ID 'admin',
    SECRET 'your-secret-key', USE_SSL false, URL_STYLE 'path');
  SELECT * FROM read_parquet('s3://my-bucket/data.parquet');
"
```

对于 MinIO/silo/rustfs，仍需设置 `s3_url_style = 'path'`，因为这些存储
不支持虚拟主机样式 URL。

---

反链：

- [插件 (Addons)](/zh/docs/addon/)
