SSD Nodes Learn 8GB 内存 — 每年 $66
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-01

服务器上该用 DuckDB 还是 SQLite?为何通常两者都用

SQLite 保存会话、订单等事务状态,DuckDB 查询 Parquet 和 CSV 分析数据。本文解释两者如何在同一台 VPS 共存,并分别提供可运行示例。

DuckDB 与 SQLite 在服务器上的对比:一句话答案

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

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

行存储和列存储为何会改变答案

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

DuckDB 分别写入各列并对其进行压缩。对五百万行的 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 工作负载下,等待 5 秒可以消除大多数此类错误。

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();"

创建一个用于查询的真实文件。以下命令会将 5000000 条订单记录写入 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 用于存放 analytics 读取的 Parquet 文件。如果两者共用一个目录,备份作业对其中一个执行快照时,可能会与另一个发生竞争。

不要让两个进程以读写模式指向同一个 DuckDB 数据库文件。一个 DuckDB 文件只能由一个进程持有写入权限,第二个进程会完全无法打开该文件。如果每个进程都设置了 access_mode = 'READ_ONLY',则可以有多个读取进程。这一点会让习惯 SQLite 的用户感到意外,因为 SQLite 通常允许多个进程共享一个文件。如果您的 analytics 只读取 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 备份 将这两个路径发送到服务器外部。这样,整个数据层就是一个备份作业中的两个目录。

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

SQLite 中的 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,几乎总是表示文件的架构与您记忆中的不一致。运行 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 文件写入后不会改变,因此复制目录即可。

#duckdb#sqlite#database#analytics#parquet#自托管