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,用來回報已切換的模式,也是伺服器上最實用的設定。在預設的 rollback journal 模式中,寫入者會阻擋所有讀取者。在 WAL 模式中,讀取者可繼續讀取最近提交的狀態,同時由單一寫入者附加資料,因此緩慢的報表不會再讓後方的 Web 請求停滯。
確認資料列已返回:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"輸出會是 1|ana|2026-07-30T09:14:00Z|4200。資料庫旁會多出兩個檔案: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();"建立一個可供查詢的實際檔案。以下命令會將五百萬筆訂單寫入 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 footer,判斷查詢需要哪些 column chunk,然後只讀取這些資料。整個目錄也能用 glob FROM '/srv/data/orders-*.parquet' 以相同方式處理,因此一個月的每日匯出檔可以合併成一次查詢。
磁碟速度是這一切的基礎,而 column scan 屬於長距離的循序讀取,因此在這裡,NVMe 與 VPS 上較舊的 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 服務與計時器配對 很適合這項工作:一個 unit 執行 COPY,另一個 timer 每晚觸發它。
在同一台 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 個目錄。
失敗模式與實際顯示的字串
Error: database is locked(SQLite)表示其他連線持有寫入鎖的時間超過允許的逾時時間。這不是資料損毀。請在每個連線上設定 PRAGMA busy_timeout,然後找出原本應拆成數個短交易的長交易。
權限變更後出現 Error: unable to open database file,通常表示程序可以寫入檔案,但無法寫入其所在目錄。SQLite 會在資料庫旁建立 app.db-wal 與 app.db-shm,因此目錄本身必須可寫入,不能只讓 .db 檔案具備寫入權限。
IO Error: Could not set lock on file(DuckDB)表示已有其他程序以寫入模式開啟該資料庫。請關閉其他 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 只需存取 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 檔案寫入後不會變更,因此複製整個目錄即可。