SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-07

服务器上 DuckDB 和 SQLite 怎么选?

SQLite 保存订单和会话等事务状态,DuckDB 分析 Parquet 与 CSV。本文解释两者为何适合在同一台 VPS 上共存,并提供可运行示例。

服务器上 DuckDB 与 SQLite 的区别:一句话答案

SQLite 是 OLTP 引擎(在线事务处理):它按行存储数据,适合安全、快速地同时读写少量数据。DuckDB 是 OLAP 引擎(在线分析处理):它按列存储数据,适合扫描数百万行并返回一个聚合结果。两者都是嵌入式库,都可以打开普通文件,也都不需要运行必须专门维护的服务器进程。

因此,对于“应该选哪个”这个问题,最实际的答案通常是“在同一台 VPS 上同时使用两者”。应用程序将实时状态保存在 SQLite 中。报表使用 DuckDB 读取 Parquet 和 CSV 文件。两者并不冲突,因为它们负责的任务不同。

行存储与列存储为何会改变结论

SQLite 会将一行作为页面中的一个连续数据块写入。按主键获取一条订单时,需要访问一个索引页和一个数据页,也就是执行两次读取。这正是应用每秒执行数千次的操作:读取某个用户、更新某个会话、插入某个订单。

DuckDB 会分别写入每一列,并对其进行压缩。对 500 万行的 amount_cents 求和时,只需读取 amount_cents 列,跳过文件中的其他所有字节,再通过向量化代码批量处理这些值并完成求和。其他列完全不需要从磁盘读取,速度优势就来自这里。

现在让每个引擎处理对方擅长的工作负载。SQLite 对一列求和时,必须遍历每一行,并从页面中读取整行才能访问其中一个字段,因此读取的磁盘数据远多于实际需要的数据。DuckDB 插入一条订单时,必须为一个值访问每一列的存储区域,还要为此获取整个数据库文件的写锁。两个引擎都没有问题。只是它们各自处理的任务并不是自身设计的目标。

SQLite 的优势场景:事务性应用状态

如果写入数据量小、频率高且不能丢失,请选择 SQLite。会话、订单、队列行、设置,以及 Web 请求创建的任何数据都适用。

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

在同一步中创建表并启用预写式日志记录。

sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
  id           INTEGER PRIMARY KEY,
  customer     TEXT    NOT NULL,
  placed_at    TEXT    NOT NULL,
  amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
  VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQL

输出的第一行是 wal。这是 PRAGMA,用于报告它切换到的模式,也是服务器上最有用的设置。在默认的回滚日志模式下,写入操作会阻塞所有读取操作。在 WAL 模式下,一个写入操作追加数据时,读取操作仍可读取最近一次提交的状态。因此,缓慢的报表查询不再阻塞后续的 Web 请求。

检查该行是否已返回:

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

输出为 1|ana|2026-07-30T09:14:00Z|4200。数据库旁边还出现了两个文件:app.db-walapp.db-shm,这两个文件都属于该数据库。应用运行时单独复制 app.db 会生成不完整的备份,本文稍后会详细介绍。

SQLite 仍然一次只允许一个写入操作。这个限制是锁,而不是队列,因此第二个写入操作等待过久后会失败并返回 database is locked,而不是无限期阻塞。对应用打开的每个连接使用 PRAGMA busy_timeout = 5000; 来延长等待时间。在正常的 Web 工作负载下,等待五秒通常可以消除大多数此类错误。

DuckDB 的优势:分析现有文件中的数据

当问题以“多少”“有多少”或“排名前十的是哪些”开头,输入又是一批 CSV 或 Parquet 文件时,应选择 DuckDB。安装命令行客户端。截至 2026 年 7 月,版本为 1.5.5:

curl https://install.duckdb.org | sh

该脚本会将二进制文件安装到 ~/.duckdb/cli/latest/duckdb,并输出将其加入 PATH 的命令。确认它可以运行:

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

创建一个可用于查询的真实文件。以下命令会将五百万条订单记录写入 Parquet,并使用 zstd 压缩:

mkdir -p /srv/data
duckdb :memory: "
COPY (
  SELECT i                                                     AS id,
         'cust_' || (i % 5000)                                 AS customer,
         TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
         (i * 37) % 20000                                      AS amount_cents
  FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"

现在提出分析问题。打开 shell,启用计时器,然后直接查询文件,无需导入步骤:

.timer on
SELECT customer,
       count(*)                AS orders,
       sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;

.timer 读取实际耗时,不要直接相信已发布的数值,因为结果取决于磁盘和 CPU 核心数。重要的是性能表现的趋势。这里没有 CREATE TABLE、没有 INSERT,也没有加载步骤:DuckDB 读取 Parquet 文件尾部,确定查询所需的列块,然后只读取这些列块。使用 glob FROM '/srv/data/orders-*.parquet' 后,整个目录的处理方式也相同;因此,一个月的每日导出文件可以通过一次查询完成分析。

磁盘速度决定了整体性能下限,而列扫描属于长距离顺序读取。因此,在这里,VPS 上的 NVMe 存储与较旧 SATA 存储之间的差距比 SQLite 的小规模随机读取场景更加明显。

从 DuckDB 读取 SQLite 数据库

两个引擎通过 DuckDB 的 sqlite 扩展连接。以只读方式附加应用数据库,确保分析查询不会写入实时数据:

INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;

该操作会在查询时直接从 SQLite 文件读取行数据,不会创建副本。使用方便,但速度不快,因为磁盘上的数据仍采用行式存储,DuckDB 必须遍历这些数据。将它用于导出,不要用于每三十秒重新加载一次的仪表板:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

整个方案就是这一条语句。SQLite 负责保存最近的实时数据。定时导出会将已关闭的时间段转换为 Parquet。DuckDB 负责回答跨越数月的数据查询,同时应用数据库保持较小,从而维持较快的写入速度。

应按计划运行导出,而不是手动执行。使用一组 systemd 服务和计时器 正合适:一个单元运行 COPY,一个计时器每晚触发它。

在同一台 VPS 上运行两者

这里不需要容器,也不需要端口。两个引擎都是库,因此安装内容只是软件包和文件路径。如果堆栈的其余部分已经通过同一台 VPS 上的 Docker Compose运行,请将数据目录挂载到需要使用它的容器中,而不是添加数据库服务,因为这里没有需要添加的服务。

以下两条规则可以避免这种部署方式出现问题。

为每个引擎使用独立目录:/srv/app存放应用写入的 SQLite 文件,/srv/data存放分析程序读取的 Parquet 文件。如果它们共用一个目录,备份任务为其中一个创建快照时,可能会与另一个引擎发生竞争。

不要让两个进程以读写模式打开同一个 DuckDB 数据库文件。一个 DuckDB 文件同时只能由一个进程写入,第二个进程会直接打开失败。多个读取进程可以同时访问,但每个进程都必须设置access_mode = 'READ_ONLY'。这与 SQLite 的行为不同,因此从 SQLite 迁移过来的用户可能会感到意外:SQLite 通常允许多个进程共享同一个文件。如果分析程序只读取 Parquet 文件,就不会遇到这个问题。这也是将持久化状态保存在 SQLite 中的另一个原因。

备份存在差异,而差异可能造成严重问题

正在运行的 SQLite 数据库由 3 个文件组成。在写入过程中使用 cp 复制这些文件,得到的文件可能可以打开,但内容不正确。请使用数据库引擎自带的备份命令。该命令会在应用继续写入时创建一致的快照:

sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"

integrity_check 在备份正确时输出 ok。输出任何其他内容,都表示应删除该快照并重新执行备份。

Parquet 文件写入后不会改变,因此无需特殊处理:直接备份目录即可。使用 从 VPS 执行 restic 备份 将这两个路径发送到服务器外部。这样,整个数据层只需在一个备份任务中备份两个目录。

故障模式与将看到的确切字符串

Error: database is locked 表示另一个连接持有写锁的时间超过了您设置的超时。这不表示数据损坏。为每个连接设置 PRAGMA busy_timeout,然后查找本应拆分为多个短事务、却持续时间过长的事务。

权限变更后出现 Error: unable to open database file,通常表示进程可以写入文件,但无法写入其所在目录。SQLite 会在数据库旁创建 app.db-walapp.db-shm,因此必须确保目录本身可写,而不仅是 .db 文件可写。

DuckDB 出现 IO Error: Could not set lock on file 表示已有另一个进程以写入模式打开该数据库。关闭另一个 shell,或以只读模式打开当前连接。

在小型 VPS 上,DuckDB 出现 Out of Memory Error 表示查询所需的工作内存超过了可用内存。DuckDB 在条件允许时会将数据溢出到磁盘,因此应将数据库文件放在磁盘上,而不是使用 :memory:,并通过 SET memory_limit = '2GB'; 限制内存使用量。在运行其他服务的服务器上,该限制可以防止临时查询耗尽应用的 RAM。

查询 Parquet 时出现 Binder Error: Referenced column "amount" not found,几乎总是表示文件的 schema 与您记忆中的不一致。运行 DESCRIBE SELECT * FROM '/srv/data/orders.parquet';,读取实际的列名。

实际如何选择

先确认写入模式。大量必须在断电后保留的小规模写入,适合 SQLite。再确认读取模式。需要对长期历史数据执行聚合的全表扫描,适合 DuckDB。大多数实际系统对这两个问题的答案都是“是”。正确做法是让每个引擎负责自己擅长的部分,而不是强行让其中一个引擎替代另一个。

应避免的迁移方式,是因为报表运行缓慢,就把实时应用状态迁移到 DuckDB。报表运行缓慢的原因是存储布局,因此解决方案是导出数据,而不是重写写入路径。

FAQ

DuckDB 可以替代应用数据库中的 SQLite 吗?

如果应用频繁写入,则不能。DuckDB 会锁定整个数据库文件,同时只允许一个读写进程,并且针对批量变更而非单行插入进行了优化。请将事务状态保存在 SQLite 中,需要生成报告时,再通过 ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); 让 DuckDB 读取这些数据。

DuckDB 在分析方面真的比 SQLite 快吗?

对于大型表的扫描和聚合,是的。原因在于存储布局,而不是某种调优技巧。DuckDB 只读取查询指定的列,并按批次处理数据;SQLite 则必须遍历完整行才能访问某个字段。按主键获取单行时,情况正好相反,因为 SQLite 只需访问两个页面,而 DuckDB 会访问每一列的存储区域。

在 VPS 上运行 DuckDB 需要很多 RAM 吗?

不需要,但应为它设置限制并提供磁盘空间。打开数据库文件,而不是 :memory:,这样 DuckDB 可以将中间结果溢写到磁盘;然后将 SET memory_limit = '2GB'; 设置为 VPS 可以提供的值。如果不设置限制,一次大型 GROUP BY 可能会导致 Out of Memory Error,或将其他服务挤出 RAM。

如何将 SQLite 数据导入 Parquet?

从 DuckDB 附加 SQLite 文件,然后使用 COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); 直接导出查询结果。可以按计划对已关闭的时间段执行此操作,例如导出上个月的行;近期行仍保留在 SQLite 中,因为应用仍会向其中写入数据。

应该备份哪一个,如何备份?

两者都应备份,但方式不同。使用 sqlite3 app.db ".backup '/srv/backup/app.db'" 创建 SQLite 快照,而不是使用 cp,因为运行中的数据库同时也是一个 -wal-shm 文件,直接复制可能得到不完整的文件。Parquet 文件写入后不会改变,因此只需复制所在目录即可。