DuckDB 與 SQLite 用於伺服器:為何兩者都用
SQLite 儲存交易式應用程式狀態,DuckDB 則分析 Parquet 與 CSV。了解兩者如何在同一台 VPS 共存,並查看各自的實作範例。
DuckDB 與 SQLite 用於伺服器:一句話回答
SQLite 是 OLTP 引擎(線上交易處理):它以資料列儲存資料,設計用來安全且快速地一次讀寫少量資料列。DuckDB 是 OLAP 引擎(線上分析處理):它以資料欄儲存資料,設計用來掃描數百萬筆資料列並回傳一個彙總結果。兩者都是嵌入式函式庫,都能開啟一般檔案,而且都不會執行需要您持續管理的伺服器程序。
因此,對於「該選哪一個」這個問題,最誠實的答案幾乎總是「兩者都用,部署在同一台 VPS 上」。您的應用程式將即時狀態保留在 SQLite 中。您的報表則使用 DuckDB 讀取 Parquet 和 CSV 檔案。兩者並不衝突,因為它們處理的工作不同。
為什麼列式儲存與欄式儲存會改變答案
SQLite 會將一列資料以連續區塊寫入頁面。依主索引鍵擷取一筆訂單時,會存取一個索引頁面和一個資料頁面,也就是 2 次讀取。這正是應用程式每秒執行數千次的操作:讀取這位使用者、更新這個工作階段、插入這筆訂單。
DuckDB 會分別寫入各個欄位並進行壓縮。對 5000000 列的 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。資料庫旁又出現了另外 2 個檔案:app.db-wal 和 app.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 讀取實際數值,不要直接相信已發布的數值,因為結果取決於磁碟及核心數量。重要的是結果的整體形態。這裡沒有 CREATE TABLE、沒有 INSERT,也沒有載入步驟:DuckDB 讀取 Parquet footer,判斷查詢需要哪些欄位區塊,然後只讀取那些區塊。使用 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 通常允許多個程序共用同一個檔案。如果您的分析程序只讀取 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 將這兩個路徑傳送到伺服器外。如此一來,整個資料層就是一個備份工作中的 2 個目錄。
失敗模式與您將看到的確切字串
SQLite 傳回的 Error: database is locked 表示另一個連線持有寫入鎖定的時間超過您設定的逾時時間。這不是損毀。請在每個連線上設定 PRAGMA busy_timeout,然後找出原本應分成數個短交易的長時間交易。
權限變更後出現 Error: unable to open database file,通常表示程序可以寫入檔案,但無法寫入其所在目錄。SQLite 會在資料庫旁建立 app.db-wal 和 app.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,報表需要時再讓 DuckDB 使用 ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); 讀取。
DuckDB 的分析效能真的比 SQLite 快嗎?
對大型資料表的掃描和彙總而言,是的。原因在於儲存配置,而不是調校技巧。DuckDB 只讀取查詢指定的欄位,並以批次處理值;SQLite 則必須逐列走訪,才能取得其中一個欄位。若依主鍵擷取單一資料列,結果則相反,因為 SQLite 只需存取 2 個頁面,而 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 檔案寫入後不會變更,因此只要複製目錄即可。