博客 / 詳情

返回

不知道怎麼選型文件型數據庫?快來看看吧

不知道怎麼選型文件型數據庫?快來看看吧

最近正好需要為項目選擇一個初始場景的學習和試用場景的關係型數據庫,對於這種場景,文件型數據庫就是最合適的,因為其幾乎沒有部署成本,並且數據遷移便利,資源消耗低。

SQLite

那既然説到了文件型數據庫,那 SQLite 可就精神了。作為文件型數據庫的中流砥柱,它幾乎佔據了嵌入式場景 70% 以上的江山。你的手機相冊、你的瀏覽器歷史記錄、你正在用的 IDE、你微信的聊天記錄,甚至你那台智能冰箱裏,可能都靜靜地躺着一個 .SQLite 文件。

Small. Fast. Reliable. Choose any three

作為嵌入式數據庫的中流砥柱,SQLite 有諸多過人之處:

  1. 高度可靠,經過嚴苛測試:SQLite 的作者對穩定性近乎偏執,最新統計其測試代碼行數超過 9000 萬行,遠遠高於自身約 15 萬行的源碼。正因如此,SQLite 被視為“非常健壯且耐用”的數據庫,引擎品質媲美工業級產品。實際應用中只要遵循官方建議配置,它幾乎不會丟失數據,哪怕面對各種異常情況也能保證數據完整性。
  2. 零配置易用:SQLite 是自包含的,無需客户端/服務器架構,不用啓動獨立服務進程。應用進程直接以函數庫形式調用它的功能。這意味着部署和使用極其簡單:一個庫文件加上一把 SQL,就能在應用內部實現數據持久化。對開發者來説,SQLite “拿來即用”,非常適合快速開發和小型項目的嵌入式存儲。
  3. 性能優異:別看 SQLite 體積小,單機單文件內它的讀寫速度相當快。在事務型負載(大量點查詢、快速插入更新)場景下,SQLite 表現非常出色。官方經驗表明,SQLite 擅長處理點查詢和單記錄讀寫,能在嵌入式設備上支撐每秒上萬次簡單查詢。例如,對於按主鍵查詢這樣的操作,SQLite 利用索引可以在毫秒級返回結果。很多應用使用 SQLite 代替傳統文件讀寫,性能反而更好,因為它內部對數據組織和檢索做了大量優化。
  4. 資源佔用低:作為面向移動/嵌入式場景設計的引擎,SQLite 對內存和存儲的要求都很低。其二進制庫通常不到1MB大小,而在運行時,SQLite 也以精巧的機制避免浪費內存。在實際測試中,插入百萬級別的數據後,SQLite 進程的內存僅增加區區數兆字節,這一數字遠低於同類 Java 數據庫。這種小內存佔用使它非常適合在內存有限的環境(手機、物聯網設備等)中長期運行。

沒有銀彈

但是,凡事沒有銀彈,如果 SQLite 真的完美無瑕,那麼世界上就肯定只有一種文件型數據庫了。當你試圖用 SQLite 扛起稍微複雜一點的業務時,你可能會遇到以下問題:

  1. 弱類型且 SQL 語法不完備:當你開心地給每一個表創建一個 created_at 作為創建時間時,你突然發現,SQLite 這玩意竟然沒有時間類型!,SQLite 中僅有 5 種類型: NULL, INTEGER, REAL, TEXT, BLOB。此外,SQLite 對 SQL 語法的支持也不完整,比如並不支持 存儲過程,事務隔離級別僅支持串行化以及 RIGHT/OUTER FULL JOIN 等
  2. 併發:你要你用過 SQLite,那幾乎見過這個錯誤:SQLite_BUSY: database is locked, SQLite 在併發讀寫方面有天生侷限:同一時間只允許一個寫事務
  3. 分析慢:老闆讓你統計過去十年的訂單總額,你寫了個 GROUP BY,結果 SQLite 跑得比老牛拉破車還慢。不過 SQLite 是 OLTP 型數據庫,這裏説分析能力有點強人所難了
  4. 單點:服務器硬盤壞了,那麼不好意思了,你的數據庫徹底丟失了。對於要求高可用的關鍵應用來説,僅靠 SQLite 單節點顯然不夠,需要通過操作系統層面的定期備份

巧了,針對 SQLite 這些缺點,在不同領域確實存在相應的數據庫對其補足了。

H2

首先就是 H2。

如果説 SQLite 是 C 語言打造的嵌入式王者,那麼 H2 則是 Java 生態中的後起之秀。

Java 原生,功能完備

H2 Database Engine 是一個純 Java 編寫的輕量級關係數據庫。由於天生和 JVM 環境高度契合,H2 已成為許多 Java 項目的默認嵌入式數據庫選擇(例如 Spring Boot 的默認內存數據庫就是 H2)。

它有如下的優點:

  1. 完整的類型和 SQL 支持:這點直擊 SQLite 弱類型的痛點,它採用靜態模式的強類型系統,提供了 完整的 SQL 類型系統以及完整的事務隔離級別)。總體而言,H2 在功能覆蓋上更接近傳統數據庫
  2. 純Java實現,開箱即用:由於 H2 完全由 Java 編寫,它可以直接嵌入運行於 JVM 的應用,而無需像 SQLite 那樣通過 JNI 調用本地庫。這帶來的好處是部署更加簡單——不再受限於操作系統,Javaer 也能更容易地調試和排查問題(畢竟H2的異常棧都是Java代碼)。同時,H2 提供了方便的嵌入模式和服務器模式。嵌入模式下,它和應用共享同一 JVM 內存,數據文件存本地;而切換到服務器模式,又可以讓多個進程通過TCP/IP協議共同訪問數據庫
  3. 性能和併發:益於 Java 實現,H2 內部採用 多版本併發控制(MVCC)來支持更高的併發度,它允許多個連接同時讀寫同一個數據庫,針對行級別加鎖,從而減少鎖競爭。相較於SQLite的單寫者限制,H2 在併發寫入場景下更具優勢。此外,H2 內置了緩存機制,會將最近和頻繁訪問的數據頁緩存在內存中,合理調優緩存大小可以大幅提升查詢性能。
  4. 易於調試和管理:H2 附帶一個功能完善的Web控制枱,允許開發者在瀏覽器中連接數據庫、執行SQL、查看錶結構,非常方便。(其實一點也不好用,UI 古老而且功能很少,不如直接使用 DB Manager)

H2 的侷限性

儘管 H2 功能強大,但在某些方面卻不如 SQLite 那般穩若磐石,主要體現在數據持久化可靠性和大數據集下的資源消耗:

  1. 持久化安全性:H2 對事務 Durability(持久性)的保證不如 。此外,SQLite 那樣嚴格。在理想情況下,事務提交後數據應當寫入持久介質,即使斷電也不會丟失。但H2官方文檔明確指出:不保證所有已提交事務一定能在掉電後存活,甚至還在後面説一些數據庫聲稱他們能夠保證持久性,但這些聲明是錯誤的(這説的誰我想大家都知道)
  2. 可靠性:由於測試強度和社區使用面不及 SQLite,H2 在大數據量、長時間運行時曝出過一些問題。一些在生產中嘗試使用 H2 的開發者反饋:當數據規模增大、併發請求變多時,H2 容易出現死鎖、性能急劇下降,甚至偶爾會有數據損壞或丟失的情況。這點對於生產環境來説是致命的,沒有產品能接受用户的數據丟失問題。
  3. 內存與資源消耗:相較於高度精簡的 SQLite,H2 對內存的胃口要大得多。一方面,Java 實現本身會佔用一定堆空間;更重要的是,H2 為追求性能會在內存中緩存大量數據頁和索引。一旦數據量上去,H2 進程的內存消耗可能直逼幾百MB甚至更高,這點,在後續的 性能對比 章節會有明確的數據結論。

總得來説,H2 擴展了 SQLite 的功能邊界,在純 Java 生態下提供了一個功能完備的嵌入式數據庫。然而這一切是以更高的資源成本為代價的,並且在數據安全性上存在一定隱憂。如果您的應用對數據可靠和長時間穩定運行要求極高,選擇 H2 需三思;但如果看重其靈活性(內存/文件/服務器模式)和易用性(Java 原生),H2 仍然是值得考慮的方案。

rqlite

從名字中就可以知道,rqlite 是對 SQLite 的補充擴展,rqlite 可以簡單理解為基於 SQLite 的分佈式數據庫:它在多台節點上覆制 SQLite 的數據,通過 Raft 共識算法保證各節點之間的數據一致性

換句話説,rqlite 將原本單機的 SQLite 變身為一個支持高可用冗餘的系統——只要集羣中多數節點存活,你的應用仍然可以訪問到最新的數據。這對需要輕量級分佈式存儲的場景來説非常有吸引力。

特點與適用場景

  1. 易部署,低運維成本:rqlite 非常輕量,每個節點就是一個幾MB大小的可執行程序,無需額外依賴。
  2. 高可用與數據安全:rqlite 基於 Raft 協議保證強一致性,這意味着在任意時刻系統只會有一份一致的數據庫狀態。數據被完整地複製到多個節點上,單點故障不再致命。
  3. SQLite 高度兼容:rqlite 雖然加了分佈式外衣,但底層用的仍是 SQLite 引擎。它對外暴露 HTTP API 供應用寫入查詢,但你在API裏執行的其實還是標準的 SQL 語句,SQLite 支持的複雜查詢、全文檢索、JSON 函數等,在 rqlite 上同樣適用

魚和熊掌不可兼得

既要有要是不可取的,雖然 rqlite 補足了 SQLite 單點缺陷的難題,但是為此,它也不得不捨棄一些 SQLite 的優勢。

  1. 性能開銷:寫入性能較慢是 rqlite 不可避免的弱點。由於每次寫操作都要通過 Raft 在多個節點之間通信確認,rqlite 的單次事務延遲遠高於本地 SQLite。官方明確指出,rqlite 是為高可用而非高性能設計的,其寫吞吐相對於單機 SQLite 會有明顯下降,這點在 性能對比 章節會有明顯體現。
  2. 額外的部署開銷:SQLite 之所以能成為數據庫的中流砥柱,其核心競爭力之一就是嵌入式的單文件型數據庫。然而,rqlite 就不得不啓動一個 Server 對外暴露服務,使用時,感覺它就是一個傳統的 MySQL 數據服務一樣,rqlite 需要獨立部署服務進程。
  3. Java支持弱:rqlite 目前對各語言的支持主要通過 HTTP Client 庫,缺乏像 JDBC 那樣成熟透明的驅動。雖然在 Java 中雖然有社區貢獻的 rqlite-jdbc 驅動,但是經過我的測試,幾乎無法使用,有大量 JDBC 的接口都沒有實現。
  4. 處境尷尬:rqlite 為了支持 SQLite 的單點問題,基於 HTTP Server 的模式部署,使得使用它還需要單獨部署它,這完全拋棄了 SQLite 拿來即用的最大優勢。與其使用 rqlite,我為什麼不使用 MySQL、Postgres 這樣更成熟功能更多的數據庫服務呢?所以它的使用場景非常侷限。

總結來説,rqlite 將 SQLite 帶入了分佈式時代,以極低的複雜度提供了令人驚喜的高可用特性。不過魚與熊掌不可兼得,開發者在享受其簡潔的同時,也要接受性能上的妥協和適用場景的限制。在對可靠性要求高於性能的邊緣計算、物聯網、輕量級服務中,rqlite 尚有用武之地;而在高併發、大數據的核心業務裏,它就不是一個主力選手了。

DuckDB

DuckDB 是近年來數據分析領域中迅速崛起的一款數據庫系統,它的定位和架構非常獨特,是專門為 分析工作負載(OLAP)設計的嵌入式數據庫。

DuckDB 最大的特點在於它結合了兩種對立系統的優勢:SQLite 的簡潔性 和 數據倉庫的分析能力。它同樣也實現了 嵌入式零配置單一文件的優勢,而基於其 列式存儲 的特定,它非常適合做數據分析統計相關的工作。官方還號稱自己為 The SQLite for Analytics

The SQLite for Analytics

近年來,DuckDB 在數據科學圈迅速走紅,Python、R、Julia 等工具鏈都集成了DuckDB,用它來取代笨重的 pandas 或Spark執行本地分析任務。下面讓我們看看DuckDB在文件型數據庫選型中有哪些獨特價值。

  1. 列式存儲,極速分析:DuckDB 最核心的賣點就是列存。它將同一列的數據緊湊地存儲在一起,適合掃描和壓縮,大幅提升了聚合計算的效率。當你對百萬行數據執行 GROUP BYJOINAVG 這類操作時,DuckDB 的列式引擎可以比 SQLite 快出一個數量級以上。DuckDB 還使用向量化執行(一種批量數據處理技術)充分利用 CPU 流水線,提高算子執行效率。這些設計讓 DuckDB 在處理大批量數據分析時如魚得水
  2. 多核並行內存友好:DuckDB 支持多線程並行查詢執行,可以利用機器的多核優勢加速處理,另外,DuckDB 為了分析場景做了大量內存優化,會智能地將中間結果緩存於內存以減少重複計算,並採取樂觀併發控制機制避免不必要的鎖競爭
  3. 即席分析:DuckDB 非常適合作為數據分析的嵌入式引擎融入應用中。它支持標準 SQL,包含窗口函數、複雜 JOIN、CTE 等高級功能,對數據科學家和分析師來説十分友好。同時,DuckDB 可以直接讀取多種數據格式,例如 CSV、Parquet、Arrow 等,這意味着你可以用SQL直接查詢這些文件,而不必先寫腳本轉換導入。這一點對於需要頻繁從數據湖或日誌文件中提取信息的應用來説價值巨大

各有所長

儘管DuckDB在特定領域表現亮眼,但我們也需認識到它並非萬能,同SQLite相比有以下侷限:

  1. 不擅長高併發OLTP:DuckDB 的設計初衷不是替代 OLTP 數據庫來處理海量併發事務。它更關注吞吐而非併發,對於大量小事務的場景支持有限。DuckDB 允許一個進程內開啓多個併發寫線程,但仍然不支持多個進程同時寫入同一庫文件(多進程只能併發只讀)
  2. 內存佔用與啓動開銷:相較 SQLite 極簡的內存足跡,DuckDB 在加載大數據集進行分析時會佔用顯著更多的內存。這是列式數據庫的典型特徵:為了加速計算,DuckDB 常常需要將列數據或中間結果緩存在內存中。對於幾百萬行的數據表,運行一個複雜分析查詢往往會瞬時佔用幾百MB的內存,這一點在嵌入式設備上需要慎重考慮。
  3. 生態成熟度:DuckDB 畢竟是近幾年才出現的新項目,在生態工具和社區積累方面無法與 SQLite 比肩。比如,SQLite 歷經二十年驗證,幾乎沒有重大漏洞且兼容性極佳,而 DuckDB 還在快速迭代中,可能存在隱藏的bug或行為改變。

總的來説,DuckDB 為文件型數據庫開闢了新的可能:在本地完成過去需要數據庫集羣才能完成的大規模分析計算。對於追求實時分析的平台來説,這簡直是福音。然而,正如沒有任何單一數據庫能通吃所有場景一樣,DuckDB 也有其短板。我們應根據具體需求,在性能和資源之間找到平衡。

性能對比

説了這麼多優缺點,實際表現到底如何呢?正好我針對 SQLite、H2、DuckDB、rqlite 四個數據庫做了一系列基準測試,包括增刪改查各類操作。下面我們就結合數據,看看它們在不同場景下的性能差距如何。先上結論:沒有最強,只有最適合場景的。

INSERT 性能 (ms)

數據規模 SQLite H2 DuckDB rqlite
100,000 (新增100K) 780 2,268 12,531 5,481
200,000 (新增100K) 5,921 3,114 7,953 8,566
500,000 (新增300K) 17,345 41,827 16,948 34,989
1,000,000 (新增500K) 28,294 95,202 31,618 超時
  • SQLite 在大規模插入時表現最穩定,插入時間隨數據增長沒有誇張暴漲,穩如老狗。
  • H2 初始插入很快,但隨着數據量增長性能跳崖式下降,50 萬行以後寫入速度直線暴跌。看來多線程插入扛不住太大數據量,後勁不足。
  • DuckDB 插入性能中規中矩,相對穩定,沒有明顯瓶頸,也沒有 SQLite 那麼穩,總體還算不錯。
  • rqlite 因 Raft 共識協議,寫入需要分發給集羣所有節點,大規模寫入相當吃力,插到 100 萬行時直接超時放棄治療。

SELECT 查詢性能 (ms, 1000 次按 ID 查詢)

數據規模 SQLite H2 DuckDB rqlite
100,000 49 690 715 2,298
200,000 39 823 394 664
500,000 35 1,034 328 678
1,000,000 28 1,244 424 -
  • SQLite 點查詢性能最優,100 萬行數據下 1000 次查詢僅耗時 28ms,幾乎可以忽略不計,秒殺其他選手,堪稱單點查詢之王。
  • DuckDB 查詢性能相當穩定,隨着數據量增加變化不大,反而在 50 萬行時最快。這可能得益於它優秀的查詢優化和向量化執行。
  • H2 查詢性能隨着數據量增長明顯下降,大數據量下查詢變慢,估計是緩存命中率降低且 MVCC 開銷顯現。
  • rqlite 因為每次查詢都經過 HTTP 網絡開銷,相對來説要慢不少,在 50 萬行以上基本奔潰(100 萬行我甚至沒測出來)。

UPDATE 性能 (ms, 1000 次更新操作)

數據規模 SQLite H2 DuckDB rqlite
100,000 2,710 478 4,404 85
200,000 2,252 794 3,034 1,396
500,000 2,357 1,149 3,042 88
1,000,000 2,214 967 3,029 -
  • rqlite 在小批量更新上出乎意料地快,只用了 HTTP 接口提供的批處理優化,100k 行時 1000 次更新只要 85ms,堪稱變態。這對需要遠程批量更新的場景是個亮點。
  • H2 更新性能整體優秀且穩定,每增加數據,時間沒有激增,始終保持在毫秒級,得益於其對事務和批量操作的良好支持。
  • SQLite 和 DuckDB 的更新性能相近,基本一個水平線。SQLite 勝在本地無網絡,DuckDB 勝在批量執行優化,最後旗鼓相當。

DateTime 範圍查詢性能 (ms)

> SQLite 本身是沒有 DateTime 相關數據類型的,這裏測試時,採用了普遍的基於 TEXT 類型存儲 ISO-8601 格式的字符串實現

無索引 (1M 行數據)
查詢範圍 查詢次數 SQLite H2 DuckDB rqlite (500K)
1周 300 562 896 2,784 461
1月 200 1,514 1,935 7,218 1,489
6月 100 6,403 5,829 1,554 3,546
1年 50 9,012 3,706 1,013 3,944
有索引 (1M 行數據)
查詢範圍 查詢次數 SQLite H2 DuckDB rqlite (500K)
1周 300 587 1,035 3,799 751
1月 200 1,837 1,309 7,749 1,056
6月 100 5,043 3,303 2,678 3,188
1年 50 6,446 4,804 1,938 4,472
  • 不建索引情況下,查詢範圍越大,DuckDB 越顯優勢:大範圍掃描 DuckDB 最快,比如查詢一年範圍 DuckDB 明顯比其它快得多(列式存儲一次掃描優勢盡顯)。
  • SQLite 在小範圍查詢(比如 1 周的數據)表現優秀,甚至優於 H2,説明少量數據的全表掃描對 SQLite 來説還能 Hold 住。
  • 給 DateTime 列建索引後,大部分數據庫範圍查詢速度都有提升,其中 rqlite 提升最明顯(畢竟不用每次掃完 500K 行了)。不過 DuckDB 在有無索引下對大範圍查詢依然保持強勁,索引對它幫助有限,因為本來全表掃也不慢。
  • H2 和 SQLite 建索引後對中等範圍查詢有提升,但面對特別大的範圍(半年/一年),DuckDB 依舊憑藉列存和並行優勢全面勝出。

備份性能 (ms)

數據庫 備份耗時 備份文件 驗證結果
SQLite 1,166 bench_backup.db ✔ OK (1,000,000 rows)
H2 19,195 h2_backup.zip ✔ OK (1,000,000 rows)
DuckDB 417 duckdb_backup/ (Parquet) ✔ OK (1,000,000 rows)
rqlite - rqlite_backup.db 未測試
  • DuckDB 備份最快,僅耗時 417ms!它備份時直接導出為壓縮的列式格式(Parquet),效率非常高。在大數據備份上 DuckDB 完勝。
  • SQLite 備份性能也相當不錯,1百萬行數據備份文件不到 2 秒搞定,而且只是複製一份 .db 文件的功夫,簡單粗暴但可靠。
  • H2 備份耗時最長,將近19秒,因為它默認備份會壓縮成 zip 包,還原起來麻煩,而且備份期間性能較差。
  • rqlite 因為 100 萬行時基準寫入都沒完成,這裏沒法測試備份,不過 rqlite 本身提供快照功能,速度估計也不會快哪去。

內存使用情況

數據庫 基線內存 最終內存 內存增量 Heap 增量
SQLite 9.5 MB 11.6 MB 2.1 MB 221 KB
H2 18.4 MB 817.2 MB 798.9 MB 782.7 MB
DuckDB 34.3 MB 366.1 MB 331.7 MB 332.3 MB
rqlite 42.6 MB ~120 MB ~77 MB ~70 MB
  • SQLite 內存使用驚人地低!整個基準跑完才增加了2MB 內存,佔用幾乎可以忽略,真正的小而美。
  • H2 內存消耗最高,最終增量將近 800MB,Heap 上暴漲了 782MB,懷疑人生——大概它把數據幾乎全放內存裏了,對內存小的環境很不友好。
  • DuckDB 內存使用適中,增加了約 332MB。考慮到它是列式數據庫,需要緩存列數據,能接受。不過相對 SQLite 還是肉厚了一些。
  • rqlite 客户端這邊內存佔用並不高(因為數據主要在服務端),最終增量大約 77MB,但這並不反映 rqlite 服務器端的內存開銷。總之,如果關注本進程內存,rqlite 表現還行。

綜合評價

綜合來看,根據不同指標,各數據庫各有千秋,下表給出了在不同需求下最推薦的選擇:

指標 推薦數據庫 理由
大規模寫入 SQLite 寫入性能穩定,數據量增大也不掉速
點查詢 SQLite 28ms/1000 次查詢,單點查詢速度最快
批量更新 rqlite / H2 HTTP 批處理加持 (rqlite) 或高效事務 (H2)
大範圍查詢 DuckDB 列式存儲優勢明顯,全表掃描速度最快
小範圍查詢 SQLite 索引配合下性能優異,處理少量數據最快
備份恢復 DuckDB 417ms 完成備份,Parquet 壓縮高效
內存效率 SQLite 內存佔用僅增加 2MB,資源友好
分佈式高可用 rqlite 基於 Raft 共識,多節點冗餘,高可用保障

測試代碼

上面的 Benchmark 測試,我還摘取了一部分核心代碼片段,便於大家理解這些性能差異是怎麼測出來的。例如下面是基準測試中插入操作的實現簡要。可以看到,對於 SQLite,我們使用單線程批量插入,而針對 H2,我們則利用多線程併發插入(writerThreads() 返回的線程數決定了策略)。這段代碼也從一個側面解釋了為何 H2 在小數據量插入上衝得快——畢竟用了多線程——但數據量大時反而失速:線程開多了反而拖累了性能和內存。

int threads = db.writerThreads();
long t0 = System.nanoTime();

if (threads == 1) {
    /* ---- 單線程批量寫(SQLite)---- */
    try (Connection c = db.open()) {
        c.setAutoCommit(false);
        try (PreparedStatement ps = c.prepareStatement("INSERT INTO bench(name, age, salary, employ_date) VALUES (?,?,?,?)")) {
            for (int i = 0; i < need; i++) {
                ps.setString(1, randomString(12));
                ps.setInt(2, RND.nextInt(43) + 18);
                ps.setDouble(3, RND.nextDouble() * 200_000);
                db.bindDate(ps, 4, randomEpoch());
                ps.addBatch();
                if (i % BATCH_SIZE == 0) {
                    ps.executeBatch();
                }
            }
            ps.executeBatch();
        }
        c.commit();
    }
} else {
    /* ---- 多線程批量寫(H2)---- */
    ExecutorService pool = Executors.newFixedThreadPool(threads);
    CountDownLatch latch = new CountDownLatch(need / BATCH_SIZE);
    List<future<?>&gt; futures = new ArrayList&lt;&gt;();

    for (int i = 0; i &lt; need; i += BATCH_SIZE) {
        futures.add(pool.submit(() -&gt; {
            try (Connection c = db.open()) {
                c.setAutoCommit(false);
                try (PreparedStatement ps = c.prepareStatement("INSERT INTO bench(name, age, salary, employ_date) VALUES (?,?,?,?)")) {
                    for (int j = 0; j &lt; BATCH_SIZE; j++) {
                        ps.setString(1, randomString(12));
                        ps.setInt(2, RND.nextInt(43) + 18);
                        ps.setDouble(3, RND.nextDouble() * 200_000);
                        db.bindDate(ps, 4, randomEpoch());
                        ps.addBatch();
                    }
                    ps.executeBatch();
                }
                c.commit();
            } catch (SQLException e) {
                LOG.error("insert task failed", e);
                throw new RuntimeException(e);
            } finally {
                latch.countDown();
            }
        }));
    }

    for (Future<!--?--> f : futures) {
        try {
            f.get();
        } catch (ExecutionException ee) {
            throw ee.getCause();
        }
    }
    latch.await();
    pool.shutdown();
}

long ms = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t0);
System.out.printf("insert %,d rows: %d ms%n", need, ms);

從以上代碼可以看到,SQLite 採用單連接串行批量提交,而 H2 開了多個線程併發插入(每線程每批插入 BATCH_SIZE=1000 行)。這解釋了為什麼在 10萬行這樣的規模下,H2 插入能比 SQLite 快(多線程跑贏單線程),但到 50 萬行、100 萬行時,線程上下文切換和內存開銷反而拖累了 H2,導致性能急轉直下。

選型推薦

聊了這麼多,回到我們最開始的問題:在不同使用場景下,該選哪種文件型關係數據庫? 結合上面的優缺點分析和性能數據,我的選型建議如下:

  • 如果你的場景偏向本地嵌入、移動端、單機應用:比如移動App、本地小工具、單機客户端程序,需要一個零配置、小巧可靠的數據庫,SQLite 永遠是首選。它部署最簡單、穩定可靠,單用户使用時性能也非常好。典型例子:手機應用緩存、本地存儲,IoT 設備的數據落地等等,用 SQLite 十拿九穩
  • 如果你在 Java 服務端開發中需要一個嵌入式數據庫用於單元測試或臨時存儲:優先考慮 H2。它與Java高度兼容,內存模式省去清理麻煩,支持標準SQL特性更全面,拿來當開發測試用的內置庫非常合適。不過要注意,別拿H2當生產庫長期存重要數據——一來持久化可靠性稍差,二來內存消耗較高。如果是小型工具或者對數據丟失不敏感的場景,用 H2 問題不大
  • 如果你的需求是高可用、分佈式部署:比如希望數據庫有容災能力,多機冗餘不會單點故障,那隻能選 rqlite。它基於Raft提供強一致複製,天生容錯。但請謹記:rqlite 適合讀多寫少、對性能要求不高的場景。其實我個人感覺,rqlite尚不成熟,在 Java 相關的項目中,儘量不要使用
  • 如果你的主要任務是本地的大數據分析、OLAP 查詢:毫無疑問,DuckDB 會是你的好夥伴。它簡直就是為這種場景而生,在單機內存允許的範圍內,可以替代龐大的數據倉庫,直接對本地文件或內存數據做分析處理。舉個例子,你要在應用裏分析幾百萬行日誌或做報表彙總,用DuckDB內嵌處理會比把數據塞進SQLite快出一個數量級。不過,同樣的,DuckDB不適合拿來支撐高併發 OLTP 業務,它更像是分析引擎而非通用 OLTP 數據庫
  • 如果你的應用場景介於上述之間:或者仍拿不定主意,那一般優先 SQLite 作為基準方案。然後根據瓶頸再考慮替代:性能瓶頸在分析查詢上就上 DuckDB,瓶頸在併發讀寫上可以考慮 H2(或者直接上更重型的數據庫)。沒有銀彈,另外,大不了先用SQLite 快速驗證業務,真撐不住了再遷移也不遲——反正 SQLite 轉去其它數據庫也不算太痛苦。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.