不知道怎么选型文件型数据库?快来看看吧
最近正好需要为项目选择一个初始场景的学习和试用场景的关系型数据库,对于这种场景,文件型数据库就是最合适的,因为其几乎没有部署成本,并且数据迁移便利,资源消耗低。
SQLite
那既然说到了文件型数据库,那 SQLite 可就精神了。作为文件型数据库的中流砥柱,它几乎占据了嵌入式场景 70% 以上的江山。你的手机相册、你的浏览器历史记录、你正在用的 IDE、你微信的聊天记录,甚至你那台智能冰箱里,可能都静静地躺着一个 .SQLite 文件。
Small. Fast. Reliable. Choose any three
作为嵌入式数据库的中流砥柱,SQLite 有诸多过人之处:
- 高度可靠,经过严苛测试 :SQLite 的作者对稳定性近乎偏执,最新统计其测试代码行数超过 9000 万行,远远高于自身约 15 万行的源码。正因如此,SQLite 被视为"非常健壮且耐用"的数据库,引擎品质媲美工业级产品。实际应用中只要遵循官方建议配置,它几乎不会丢失数据,哪怕面对各种异常情况也能保证数据完整性。
- 零配置易用 :SQLite 是自包含的,无需客户端/服务器架构,不用启动独立服务进程。[应用进程直接以函数库形式调用它的功能](SQLite.org/whentouse.h... SQL database engines strive,efficiency%2C reliability%2C independence%2C and simplicity)。这意味着部署和使用极其简单:一个库文件加上一把 SQL,就能在应用内部实现数据持久化。对开发者来说,SQLite "拿来即用",非常适合快速开发和小型项目的嵌入式存储。
- 性能优异:别看 SQLite 体积小,单机单文件内它的读写速度相当快。在事务型负载(大量点查询、快速插入更新)场景下,SQLite 表现非常出色。官方经验表明,SQLite 擅长处理点查询和单记录读写,能在嵌入式设备上支撑每秒上万次简单查询。例如,对于按主键查询这样的操作,SQLite 利用索引可以在毫秒级返回结果。很多应用使用 SQLite 代替传统文件读写,性能反而更好,因为它内部对数据组织和检索做了大量优化。
- 资源占用低:作为面向移动/嵌入式场景设计的引擎,SQLite 对内存和存储的要求都很低。其二进制库通常不到1MB大小,而在运行时,SQLite 也以精巧的机制避免浪费内存。在实际测试中,插入百万级别的数据后,SQLite 进程的内存仅增加区区数兆字节,这一数字远低于同类 Java 数据库。这种小内存占用使它非常适合在内存有限的环境(手机、物联网设备等)中长期运行。
没有银弹
但是,凡事没有银弹,如果 SQLite 真的完美无瑕,那么世界上就肯定只有一种文件型数据库了。当你试图用 SQLite 扛起稍微复杂一点的业务时,你可能会遇到以下问题:
- 弱类型且 SQL 语法不完备 :当你开心地给每一个表创建一个
created_at作为创建时间时,你突然发现,SQLite 这玩意竟然没有时间类型! ,SQLite 中仅有 5 种类型: NULL, INTEGER, REAL, TEXT, BLOB。此外,SQLite 对 SQL 语法的支持也不完整,比如并不支持 存储过程,事务隔离级别仅支持串行化以及 RIGHT/OUTER FULL JOIN 等 - 并发 :你要你用过 SQLite,那几乎见过这个错误:
SQLite_BUSY: database is locked, SQLite 在并发读写方面有天生局限:同一时间只允许一个写事务 - 分析慢 :老板让你统计过去十年的订单总额,你写了个
GROUP BY,结果 SQLite 跑得比老牛拉破车还慢。不过 SQLite 是 OLTP 型数据库,这里说分析能力有点强人所难了 - 单点:服务器硬盘坏了,那么不好意思了,你的数据库彻底丢失了。对于要求高可用的关键应用来说,仅靠 SQLite 单节点显然不够,需要通过操作系统层面的定期备份
巧了,针对 SQLite 这些缺点,在不同领域确实存在相应的数据库对其补足了。
H2
首先就是 H2。
如果说 SQLite 是 C 语言打造的嵌入式王者,那么 H2 则是 Java 生态中的后起之秀。
Java 原生,功能完备
H2 Database Engine 是一个纯 Java 编写的轻量级关系数据库。由于天生和 JVM 环境高度契合,H2 已成为许多 Java 项目的默认嵌入式数据库选择(例如 Spring Boot 的默认内存数据库就是 H2)。
它有如下的优点:
- 完整的类型和 SQL 支持 :这点直击 SQLite 弱类型的痛点,它采用静态模式的强类型系统,提供了 完整的 SQL 类型系统以及完整的事务隔离级别。总体而言,H2 在功能覆盖上更接近传统数据库
- 纯Java实现,开箱即用:由于 H2 完全由 Java 编写,它可以直接嵌入运行于 JVM 的应用,而无需像 SQLite 那样通过 JNI 调用本地库。这带来的好处是部署更加简单------不再受限于操作系统,Javaer 也能更容易地调试和排查问题(毕竟H2的异常栈都是Java代码)。同时,H2 提供了方便的嵌入模式和服务器模式。嵌入模式下,它和应用共享同一 JVM 内存,数据文件存本地;而切换到服务器模式,又可以让多个进程通过TCP/IP协议共同访问数据库
- 性能和并发 :益于 Java 实现,H2 内部采用 多版本并发控制(MVCC)来支持更高的并发度,它允许多个连接同时读写同一个数据库,针对行级别加锁,从而减少锁竞争。相较于SQLite的单写者限制,H2 在并发写入场景下更具优势。此外,H2 内置了缓存机制,会将最近和频繁访问的数据页缓存在内存中,合理调优缓存大小可以大幅提升查询性能。
- 易于调试和管理:H2 附带一个功能完善的Web控制台,允许开发者在浏览器中连接数据库、执行SQL、查看表结构,非常方便。(其实一点也不好用,UI 古老而且功能很少,不如直接使用 DB Manager)
H2 的局限性
尽管 H2 功能强大,但在某些方面却不如 SQLite 那般稳若磐石,主要体现在数据持久化可靠性和大数据集下的资源消耗:
- 持久化安全性 :H2 对事务 Durability(持久性)的保证不如 。此外,SQLite 那样严格。在理想情况下,事务提交后数据应当写入持久介质,即使断电也不会丢失。但H2官方文档明确指出:不保证所有已提交事务一定能在掉电后存活,甚至还在后面说一些数据库声称他们能够保证持久性,但这些声明是错误的(这说的谁我想大家都知道)
- 可靠性 :由于测试强度和社区使用面不及 SQLite,H2 在大数据量、长时间运行时曝出过一些问题。一些在生产中尝试使用 H2 的开发者反馈:当数据规模增大、并发请求变多时,[H2 容易出现死锁、性能急剧下降,甚至偶尔会有数据损坏或丢失的情况](www.baeldung.com/h2-producti... reports and articles from,performance when the data grows)。这点对于生产环境来说是致命的,没有产品能接受用户的数据丢失问题。
- 内存****与资源消耗 :相较于高度精简的 SQLite,H2 对内存的胃口要大得多。一方面,Java 实现本身会占用一定堆空间;更重要的是,H2 为追求性能会在内存中缓存大量数据页和索引。一旦数据量上去,H2 进程的内存消耗可能直逼几百MB甚至更高,这点,在后续的
性能对比章节会有明确的数据结论。
总得来说,H2 扩展了 SQLite 的功能边界,在纯 Java 生态下提供了一个功能完备的嵌入式数据库。然而这一切是以更高的资源成本为代价的,并且在数据安全性上存在一定隐忧。如果您的应用对数据可靠和长时间稳定运行要求极高,选择 H2 需三思;但如果看重其灵活性(内存/文件/服务器模式)和易用性(Java 原生),H2 仍然是值得考虑的方案。
rqlite
从名字中就可以知道,rqlite 是对 SQLite 的补充扩展,rqlite 可以简单理解为基于 SQLite 的分布式数据库:它在多台节点上复制 SQLite 的数据,[通过 Raft 共识算法保证各节点之间的数据一致性](rqlite.io/docs/faq/#:... top of that%2C rqlite,that data at all times)
换句话说,rqlite 将原本单机的 SQLite 变身为一个支持高可用冗余的系统------只要集群中多数节点存活,你的应用仍然可以访问到最新的数据。这对需要轻量级分布式存储的场景来说非常有吸引力。
特点与适用场景
- 易部署,低运维成本:rqlite 非常轻量,每个节点就是一个几MB大小的可执行程序,无需额外依赖。
- 高可用与数据安全:rqlite 基于 Raft 协议保证强一致性,这意味着在任意时刻系统只会有一份一致的数据库状态。数据被完整地复制到多个节点上,单点故障不再致命。
- SQLite 高度兼容 :rqlite 虽然加了分布式外衣,但底层用的仍是 SQLite 引擎。它对外暴露 HTTP API 供应用写入查询,但你在API里执行的其实还是标准的 SQL 语句,[SQLite 支持的复杂查询、全文检索、JSON 函数等,在 rqlite 上同样适用](rqlite.io/docs/featur... functionality)
鱼和熊掌不可兼得
既要有要是不可取的,虽然 rqlite 补足了 SQLite 单点缺陷的难题,但是为此,它也不得不舍弃一些 SQLite 的优势。
- 性能开销 :写入性能较慢是 rqlite 不可避免的弱点。由于每次写操作都要通过 Raft 在多个节点之间通信确认,rqlite 的单次事务延迟远高于本地 SQLite。官方明确指出,rqlite 是为高可用而非高性能设计的,[其写吞吐相对于单机 SQLite 会有明显下降](rqlite.io/docs/faq/#:... but only for reads,write to the Raft log),这点在
性能对比章节会有明显体现。 - 额外的部署开销:SQLite 之所以能成为数据库的中流砥柱,其核心竞争力之一就是嵌入式的单文件型数据库。然而,rqlite 就不得不启动一个 Server 对外暴露服务,使用时,感觉它就是一个传统的 MySQL 数据服务一样,rqlite 需要独立部署服务进程。
- Java支持弱 :rqlite 目前对各语言的支持主要通过 HTTP Client 库,缺乏像 JDBC 那样成熟透明的驱动。虽然在 Java 中虽然有社区贡献的 rqlite-jdbc 驱动,但是经过我的测试,几乎无法使用,有大量 JDBC 的接口都没有实现。
- 处境尴尬: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在文件型数据库选型中有哪些独特价值。
- 列式存储,极速分析 :DuckDB 最核心的卖点就是列存。它将同一列的数据紧凑地存储在一起,适合扫描和压缩,大幅提升了聚合计算的效率。当你对百万行数据执行
GROUP BY、JOIN、AVG这类操作时,DuckDB 的列式引擎可以比 SQLite 快出一个数量级以上。DuckDB 还使用向量化执行(一种批量数据处理技术)充分利用 CPU 流水线,提高算子执行效率。这些设计让 DuckDB 在处理大批量数据分析时如鱼得水 - 多核并行 ,内存友好 :DuckDB 支持多线程并行查询执行,可以利用机器的多核优势加速处理,另外,DuckDB 为了分析场景做了大量内存优化,会智能地[将中间结果缓存于内存以减少重复计算,并采取乐观并发控制机制避免不必要的锁竞争](duckdb.org/docs/stable... using option 1%2C DuckDB,the same connection are faster)
- 即席分析:DuckDB 非常适合作为数据分析的嵌入式引擎融入应用中。它支持标准 SQL,包含窗口函数、复杂 JOIN、CTE 等高级功能,对数据科学家和分析师来说十分友好。同时,DuckDB 可以直接读取多种数据格式,例如 CSV、Parquet、Arrow 等,这意味着你可以用SQL直接查询这些文件,而不必先写脚本转换导入。这一点对于需要频繁从数据湖或日志文件中提取信息的应用来说价值巨大
各有所长
尽管DuckDB在特定领域表现亮眼,但我们也需认识到它并非万能,同SQLite相比有以下局限:
- 不擅长高并发****OLTP:DuckDB 的设计初衷不是替代 OLTP 数据库来处理海量并发事务。它更关注吞吐而非并发,对于大量小事务的场景支持有限。DuckDB 允许一个进程内开启多个并发写线程,但仍然不支持多个进程同时写入同一库文件(多进程只能并发只读)
- 内存****占用与启动开销:相较 SQLite 极简的内存足迹,DuckDB 在加载大数据集进行分析时会占用显著更多的内存。这是列式数据库的典型特征:为了加速计算,DuckDB 常常需要将列数据或中间结果缓存在内存中。对于几百万行的数据表,运行一个复杂分析查询往往会瞬时占用几百MB的内存,这一点在嵌入式设备上需要慎重考虑。
- 生态成熟度: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 在小数据量插入上冲得快------毕竟用了多线程------但数据量大时反而失速:线程开多了反而拖累了性能和内存。
Java
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<?>> futures = new ArrayList<>();
for (int i = 0; i < need; i += BATCH_SIZE) {
futures.add(pool.submit(() -> {
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 < 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 转去其它数据库也不算太痛苦。