用过 MySQL、PostgreSQL、SQLite、LevelDB、Redis、Memcached 之后我发现一件事:大部分选型争论的根源,不在功能对比表上,在于没搞清楚这三东西根本不是一类产品。

SQLite 是嵌入式 SQL 引擎。LevelDB 是嵌入式 KV 存储库。PostgreSQL 是客户端-服务器 RDBMS。

你拿飞机、跑车、卡车比谁跑得快——快在哪?先弄清赛道。

这篇文章不评测性能数据(网上多得是),不讲安装教程(文档比我写得清楚)。我讲三个东西的底层逻辑——存储引擎差异、并发模型差异、事务语义差异。懂了这些,你自己就能判断哪个是你的事。


一、一张图:三者的本质定位

维度SQLiteLevelDBPostgreSQL
本质嵌入式 SQL 引擎嵌入式 KV 库服务端 RDBMS
架构进程内库(C 库)进程内库(C++ 库)C/S 进程模型
数据模型关系型(表/行)有序 KV(Key→Value)关系型(表/行)
存储结构B+ 树(默认)LSM-Tree堆文件 + B+ 树索引
查询语言SQL无(编程 API)SQL
并发模型文件锁(WAL 模式优化)单写者多读者MVCC + 行级锁
部署方式1 个 .db 文件一个目录 + SST 文件独立进程 + 数据目录
典型场景应用嵌入/桌面/IoT数据管道/块存储/引擎底层高并发 Web/企业应用

这不是一张「谁更好」的表格。看清架构差异比选谁更重要。因为你看到「它没有 SQL」不等于「它不好」——LevelDB 根本不走这条赛道。


二、存储引擎:B+ 树 vs LSM-Tree vs 堆文件

这是三者最本质的区别,决定了它们在不同工作负载下的表现方向。

SQLite:B+ 树,读优化

SQLite 的默认存储引擎是 B+ 树(B-tree variant,实际上 SQLite 用的是 B+ 树的一个变体叫 B*-tree)。B+ 树的核心理念是:数据有序存放,读得快

B+ 树结构(简化):
         [50, 100]
       /     |     \
   [1,30] [55,80] [110,200]
  • 点查询(WHERE id = 42):O(log N),一个索引层跳转
  • 范围查询(WHERE id BETWEEN 10 AND 50):找到起点后顺序扫叶子页——连续读,硬盘友好
  • 写入:写 B+ 树需要频繁的页分裂、页合并,随机写多

B+ 树对机械硬盘友好,对 SSD 也友好。它的痛点是写入放大——每次插入不只是在文件末尾追加,是在树中间找位置,可能导致多个页的拆分和重写。

LevelDB:LSM-Tree,写优化

LevelDB 用的是 LSM-Tree(Log-Structured Merge-Tree)。核心理念是:写入只管往内存写,满了再刷盘,后台做合并。

写入路径:
写请求 → WAL(顺序写磁盘) → MemTable(内存跳表) → 满了 → 冻结 → 刷成 SSTable
                                                                       ↓
                                                              Level 0(L0)SST文件
                                                                       ↓
                                                              后台 Compaction → Level 1 → Level 2 ...

核心优势:

  • 所有写入都是顺序写:WAL 追加 + MemTable 转 SSTable,没有随机写
  • 写入吞吐极高(SSD 上能到几十万 ops/s)
  • 天然压缩友好:SSTable 是块级压缩

核心代价:

  • 读放大:读一个 key,可能要查 MemTable → 若干 L0 SSTable → L1 SSTable → ...,每层都要二分查找
  • 空间放大:旧的重复数据在 compaction 之前不会被清除
  • Compaction 的毛刺:后台合并时 CPU 和 IO 会突然飙升

PostgreSQL:堆文件 + 索引,灵活平衡

PostgreSQL 不走纯树结构。它的表数据存储在堆文件中——无序的页面堆。索引(B+ 树、Hash、GIN、GiST、BRIN)在堆上建立指向元组的指针。

堆文件(无序行存储):
  Page 1: row(42, 'a') | row(7, 'b')  | row(99, 'c')
  Page 2: row(15, 'd') | row(3, 'e')  | ...
        ↑                         ↑
    索引1(主键B树)          索引2(GIN for 文本搜索)
  • 数据写入是堆追加(类似 append-only),但更新和删除需要标记旧元组(MVCC 行版本)
  • 读性能由索引质量决定——好的索引,点查询和范围查询都不输 SQLite
  • 查询计划器是顶级的:JOIN、子查询、CTE、窗口函数,PG 能用索引组合出最佳路径
  • 代价是堆膨胀:VACUUM 必须管,否则表膨胀到不可接受

一个我认为最反直觉的事实:LevelDB 是「写快读慢」,SQLite 是「读写均衡偏读」,PostgreSQL 是「读写都强但运维重」。大多数人的第一直觉却是反的——以为 LevelDB 快是因为「小」,以为 PostgreSQL 慢是因为「重」。


三、并发模型:从「文件锁」到「MVCC」

并发能力是三个东西分岔最大的方向。

SQLite:单写者,多读者

SQLite 的经典模式是数据库级锁。一个连接写,其他所有连接等。

-- 连接 A 开始写
BEGIN IMMEDIATE;
UPDATE users SET name = 'new' WHERE id = 1;
-- 此时连接 B 的 SELECT 阻塞?
-- 默认模式下:连接 B 的 SELECT 会被 BLOCKED

WAL(Write-Ahead Logging)模式改变了这个:

PRAGMA journal_mode=WAL;
-- 现在写者和读者不互斥了
-- 读者读 WAL 文件的快照
-- 写者写 WAL 文件
-- 但 WAL 的 checkpoint 时仍会锁库

关键数据:

  • WAL 模式下的并发读:无限制(实际受文件系统限制)
  • 并发写:永远只有一个。SQLite 的 WAL 模式允许多个读者和单个写者同时进行,但不允许并发写
  • 写阻塞时间:微秒级(WAL 模式下的写只锁很短时间)
  • 没有行级锁,没有 MVCC(虽然 WAL 实现了类似快照读的行为)

什么时候这个模型会崩?

当你有的不止是一个进程的 N 个连接,而是 N 个进程竞争同一个数据库文件时。Web 服务器多进程模型(如 PHP-FPM 的多 worker)就是典型。这种情况下,SQLite 的并发写是硬瓶颈——通常建议单机并发写不超过 10-50 QPS(写入)。

LevelDB:显式控制,单写者

LevelDB 更粗暴:一次只允许一个写操作,其他的排队。没有行锁,没有表锁——全局锁

leveldb::DB* db;
leveldb::DB::Open(options, "/path/to/db", &db);

// 写
leveldb::Status s = db->Put(write_options, "key", "value");

// 读
std::string value;
leveldb::Status s = db->Get(read_options, "key", &value);

LevelDB 的并发模型是:多个读者可以同时读,但写者只能一个。这不是 LevelDB 的缺陷——它设计来是作为 存储引擎底层 使用的,而不是作为独立数据库的。RocksDB(LevelDB 的改进版)在此基础上加了更多并发优化,但本质仍然是 MMAP 基础上的单写模型。

LevelDB 在设计上就没打算让你在多进程/多线程环境下直接用它——它是给上层引擎(如 TiKV、Apache Cassandra 的底层的 LevelDB 其实已被 RocksDB 取代)当构建块的。

PostgreSQL:MVCC + 行级锁,真并发

PostgreSQL 是这三者里唯一一个设计来扛并发的。

它的 MVCC(Multi-Version Concurrency Control)实现是这样的:

-- 事务 A(未提交)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 此时 balance 从 1000 变为 900,但事务未提交
-- 旧版本 (id=1, balance=1000) 仍在堆中,被标记为 xmax

-- 事务 B(另一个连接)
SELECT balance FROM accounts WHERE id = 1;
-- 返回 1000!— 基于 MVCC 快照,看不到未提交的修改
-- 不阻塞,不需要等事务 A 提交

MVCC 的关键特性:

  • 读不阻塞写:SELECT 不会锁行,写不会锁读
  • 写不阻塞读:事务 B 读到的是一致性快照
  • 写写会冲突:两个事务同时更新同一行,第二个会被阻塞或回滚

再加上行级锁、表级锁、咨询锁(advisory lock)、SSI(可序列化快照隔离),PostgreSQL 支持业务做复杂的并发控制——乐观锁、悲观锁、可重复读、可序列化隔离级别,都是开箱就有的。

一个容易忽略的点:PostgreSQL 的所有并发模型健全性,建立在它是一个独立进程的前提下。它有自己的内存管理器、连接池、缓冲池。而你跑 SQLite 和 LevelDB 时,这些资源跟你应用的堆是共享的——你的 GC 停顿可能会导致 SQLite 写入超时。


四、事务语义与数据完整性

SQLite:ACID,单核级

SQLite 提供完整的 ACID 语义——在单个连接内。通过回滚日志或 WAL 实现原子提交和回滚。

BEGIN;
UPDATE inventory SET quantity = quantity - 1 WHERE id = 10;
INSERT INTO orders (user_id, item_id) VALUES (1, 10);
COMMIT;
-- 以上要么全部生效,要么全部不生效

SQLite 的事务性能取决于 journal 模式:

  • DELETE(默认):写入回滚日志 → 修改数据 → 提交时删除回滚日志。安全但慢
  • WAL:写入 WAL → checkpoint 时刷入主库。读写并发好,但 checkpoint 会停一下
  • MEMORY:不回滚日志 → 快但 crash 不安全

SQLite 的 ACID 有一层局限:它保护的是数据库本身,而不是数据域。没有外键约束的默认配置下,你能插入一个 order.user_id 到不存在的用户——除非你手动开启 PRAGMA foreign_keys=ON

LevelDB:写原子但无事务

LevelDB 的原子性边界是单次 PutWriteBatch

leveldb::WriteBatch batch;
batch.Put("key1", "value1");
batch.Put("key2", "value2");
db->Write(write_options, &batch);
// 以上:要么 key1 和 key2 都写成功,要么都不写

但是:

  • 没有回滚:没有 BEGIN/COMMIT/ROLLBACK 语义
  • 没有隔离性:没有 MVCC 快照隔离
  • 没有一致性约束:没有 Schema、没有类型检查、没有外键

LevelDB 是给上层引擎当存储后端的,不是给应用层直接写业务逻辑的。

PostgreSQL:企业级 ACID

SQLite 能做的事,PostgreSQL 全做。SQLite 不能做的事,PostgreSQL 也能做。

-- 可序列化隔离级别 + SAVEPOINT
BEGIN ISOLATION LEVEL SERIALIZABLE;
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 检查是否发生串行化冲突(冲突会被自动回滚)
-- 没有就继续
INSERT INTO transactions (from_id, to_id, amount) VALUES (1, 2, 100);
COMMIT;

PostgreSQL 的 ACID 是企业级的——事务隔离(读已提交、可重复读、可序列化)、外键约束、CHECK 约束、排除约束、触发器事件一致性。

代价?就是所有这些机制都需要一个独立进程来管理


五、谁该用什么:实战建议

推荐 SQLite 的场景

  • 桌面应用/移动端 APP 的本地存储
  • IoT 设备/嵌入式系统的持久化
  • 单人/小团队 SaaS 工具(日活 < 1k)
  • 开发/测试环境的数据库(你不想装 PG)
  • 数据分析的中间格式(.import .mode csv 太方便了)
  • 原型验证阶段(上线才需要换 PG)

直接给个能跑的配置,不是默认值:

-- SQLite 生产常用配置
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=-64000;        -- 64 MB cache
PRAGMA busy_timeout=5000;        -- 5秒等待锁
PRAGMA foreign_keys=ON;          -- 手动开启外键
PRAGMA mmap_size=268435456;      -- 256 MB mmap
PRAGMA temp_store=MEMORY;

推荐 LevelDB 的场景

  • 写密集的 KV 存储底层(日志系统、时序数据初始化写入、爬虫数据缓冲)
  • 有序 KV 存储需求(范围扫描前缀)
  • 嵌入一个比 SQLite 写性能更好的本地 KV 层
  • 作为上层系统(RocksDB/TiKV/CockroachDB 等)的认知基础——你不必直接用 LevelDB,但它的 LSM-Tree 理念是一切现代 KV 引擎的底子

LevelDB 不适合的场景:

  • 需要 SQL 查询的东西(别折腾)
  • 多进程/多线程直接竞争(用 RocksDB 或弃用)
  • 需要事务或约束的业务逻辑(换 SQLite 或 PG)

推荐 PostgreSQL 的场景

  • 高并发 Web 应用(100+ 并发连接)
  • 需要复杂查询(多表 JOIN、CTE、窗口函数、全文搜索、GIS)
  • 数据完整性要求严格(外键约束、触发器等)
  • 数据需要多人管理、备份、监控、容灾
  • 需要流复制做主从

六、一个反常识的结论:你可能不需要「选」

我见过最聪明的项目是用三个:

桌面客户端:SQLite 存标准配置和会话数据
后端服务:PostgreSQL 存业务核心数据
中间管道:LevelDB(或 RocksDB)存时间序列日志

没有冲突。你在不同层用不同工具。

如果你只能选一个:

  • 你在写桌面 App → SQLite
  • 你在搭高并发后端 → PostgreSQL
  • 你在造一个新的数据库引擎底层 → LevelDB 或 RocksDB
  • 你在做小型 Web 项目,还没拿到第一笔融资 → SQLite,够用。拿到钱了再换不迟

最后一句是真心话:大部分项目到死都只有几百个用户。为千万用户设计架构,死在没有千万用户的路上——这才是最常见的死法。


对了,写这篇文章的时候没查任何选型表。不是记性好,是用够了。你也一样——用够三个,就不需要看了。


SQLite. LevelDB. PostgreSQL.

Three databases. Three fundamentally different architectures. And most debates about "which one is better" are useless, because people aren't arguing about the same thing.

SQLite is an embedded SQL engine. LevelDB is an embedded key-value library. PostgreSQL is a client-server RDBMS.

Comparing them by feature count is like comparing a bicycle, a motorcycle, and a pickup truck by top speed. Different machines. Different tracks.

This article is about architectural fundamentals — storage engine, concurrency model, transaction semantics. Understand those, and you won't need a feature checklist to pick the right one.


One Table: The Architectural Identity

DimensionSQLiteLevelDBPostgreSQL
NatureEmbedded SQL engineEmbedded KV libraryServer RDBMS
ArchitectureIn-process C libraryIn-process C++ libraryClient-server process
Data modelRelational (tables/rows)Ordered KV (Key→Value)Relational (tables/rows)
StorageB+ tree (default)LSM-TreeHeap files + B+ tree indexes
Query languageSQLNone (library API)SQL
ConcurrencyFile locks (WAL improves)Single writer, multiple readersMVCC + row-level locks
DeploymentOne .db fileA directory of SST filesServer process + data directory
Typical casesDesktop/IoT/embeddedData pipelines/storage engine internalsHigh-concurrency web/enterprise

This isn't a "who's better" table. It's a "what are we even comparing" table.


Storage Engine: B-Tree vs LSM-Tree vs Heap

SQLite: B+ Tree, Read-Optimized

SQLite's default storage engine is a B+ tree variant (B*-tree). The core idea: keep data sorted, read fast.

Point query: O(log N). Range scan: find the leaf start and walk forward — sequential reads, disk-friendly. Write: random insert into an ordered tree means page splits and rewrites. Write amplification from the tree structure itself.

B+ tree does well on both HDDs and SSDs. Its pain point is write amplification — each insert may cause multiple page splits and rewrites.

LevelDB: LSM-Tree, Write-Optimized

LevelDB uses Log-Structured Merge-Tree. Core idea: write to memory first, flush to disk when full, merge in the background.

Write path:
Request → WAL (sequential write) → MemTable (in-memory skip list) → flush → SSTable
                                                                       ↓
                                                               Background Compaction

Key strength: all writes are sequential — WAL append + SSTable flush. No random writes. Write throughput: hundreds of thousands of ops/s on SSD. Natural compression friendliness: SSTable pages are block-compressed.

Key cost: read amplification — one Get() might check MemTable → L0 SSTables → L1 → L2... each level needs a binary search. Space amplification: stale data lingers until compaction. Compaction spikes: background merges cause sudden CPU/IO bursts.

PostgreSQL: Heap + Indexes, Flexible Balance

PostgreSQL stores table data in heap files — unordered page sets. Indexes (B-tree, Hash, GIN, GiST, BRIN) point to tuples in the heap.

Heap (unordered row store):
  Page 1: row(42, 'a') | row(7, 'b') | row(99, 'c')
  Page 2: row(15, 'd') | row(3, 'e') | ...
        ↑                        ↑
    PKey B-tree             GIN index (text search)
  • Data writes are heap-append (append-ish), but updates and deletes leave old tuples (MVCC row versions)
  • Read performance depends on index quality — good indexes make point and range queries competitive with SQLite
  • Query planner is world-class: JOINs, CTEs, window functions, subquery optimizations
  • Cost: heap bloat — VACUUM is not optional

The most counter-intuitive fact: LevelDB is "write-fast, read-slow", SQLite is "balanced, read-leaning", PostgreSQL is "strong at both, heavy ops". Most people intuit the exact opposite.


Concurrency: From File Locks to MVCC

SQLite: Single Writer, Multiple Readers

SQLite in default mode uses database-level locking. One connection writes, everyone else waits.

PRAGMA journal_mode=WAL;
-- WAL mode lets readers and writers coexist
-- But only one writer at a time

Key numbers:

  • Concurrent readers in WAL mode: unlimited
  • Concurrent writers: always one
  • Write blocking time: microseconds (WAL lock is brief)
  • No row-level locks, no MVCC (though WAL provides snapshot-like reads)

When this model breaks: N processes competing for the same .db file. PHP-FPM multi-worker is the classic case. Practical ceiling for writes: ~10-50 QPS in multi-process setups.

LevelDB: Explicit Control, Single Writer

LevelDB goes simpler: one write at a time, everything else queues. No row locks, no table locks — global lock.

Multiple readers can read simultaneously, but only one writer. This isn't a defect — LevelDB was designed as a storage engine building block, not a standalone database. RocksDB (LevelDB's evolved fork) adds more concurrent optimizations, but the single-writer-gated model is inherent to the LSM design.

LevelDB was never meant to be used directly by multi-threaded/multi-process applications. It's the layer under systems like TiKV, Cassandra (RocksDB replaced LevelDB there), or your own storage engine.

PostgreSQL: MVCC + Row-Level Locks, True Concurrency

PostgreSQL is the only one of the three designed for concurrency from day one.

MVCC in action:

-- Transaction A (uncommitted)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- Transaction B (another connection)
SELECT balance FROM accounts WHERE id = 1;
-- Returns 1000! — MVCC snapshot, doesn't block

Key properties:

  • Readers don't block writers: SELECT doesn't lock rows
  • Writers don't block readers: snapshot isolation
  • Write-write conflicts: serializable through row locks or SSI

Plus row-level locks, table-level locks, advisory locks, SSI (Serializable Snapshot Isolation). PostgreSQL ships with every concurrency control tool you'd need.

One easily overlooked point: PostgreSQL's concurrency model works because it's an independent process. It manages its own memory, connection pool, buffer pool. When you embed SQLite or LevelDB, these resources share your application's heap — your GC pause can cause SQLite write timeouts.


Transaction Semantics and Data Integrity

SQLite: ACID at Process Level

Full ACID semantics per connection. Atomic commits via rollback journal or WAL.

BEGIN;
UPDATE inventory SET quantity = quantity - 1 WHERE id = 10;
INSERT INTO orders (user_id, item_id) VALUES (1, 10);
COMMIT;

Performance by journal mode:

  • DELETE (default): safest, slowest
  • WAL: good read/write concurrency, checkpoint pause
  • MEMORY: fast, crash-unsafe

One limitation: SQLite's ACID protects the database file, not domain integrity. No foreign key enforcement by default — you can insert order.user_id pointing to a non-existent user unless you PRAGMA foreign_keys=ON.

LevelDB: Atomic Writes, No Transactions

Atomicity boundary is a single Put or WriteBatch:

leveldb::WriteBatch batch;
batch.Put("key1", "value1");
batch.Put("key2", "value2");
db->Write(write_options, &batch);

What's missing:

  • No rollback
  • No isolation
  • No schema, no type checking, no constraints

LevelDB is a storage backend for upper-layer systems, not a database for application business logic.

PostgreSQL: Enterprise ACID

Everything SQLite can do, PG can do. Everything SQLite can't, PG probably can too.

BEGIN ISOLATION LEVEL SERIALIZABLE;
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
INSERT INTO transactions (from_id, to_id, amount) VALUES (1, 2, 100);
COMMIT;

Enterprise ACID: transaction isolation (Read Committed, Repeatable Read, Serializable), foreign key constraints, CHECK constraints, exclusion constraints, trigger event consistency.

The price: you need a dedicated process to manage all this machinery.


Practical Recommendations

Go SQLite when

  • Desktop/mobile app local storage
  • IoT/embedded device persistence
  • Solo/small team SaaS (< 1k DAU)
  • Dev/test databases
  • Data analysis intermediate format
  • Prototyping (migrate to PG later if needed)

Production-ready config (not defaults):

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=-64000;
PRAGMA busy_timeout=5000;
PRAGMA foreign_keys=ON;
PRAGMA mmap_size=268435456;
PRAGMA temp_store=MEMORY;

Go LevelDB when

  • Write-heavy KV storage (log collection, time-series ingest, crawler buffers)
  • Ordered KV needs (prefix scan, range queries)
  • Embedding a local KV layer with better write throughput than SQLite
  • Learning the LSM-Tree concept that powers RocksDB, TiKV, CockroachDB

LevelDB is not for:

  • SQL queries
  • Multi-process contention
  • Transactional business logic

Go PostgreSQL when

  • High-concurrency web (100+ concurrent connections)
  • Complex queries (multi-table JOIN, CTEs, window functions, full-text search, GIS)
  • Strict data integrity requirements
  • Data that needs multi-person management, monitoring, backup
  • Streaming replication

The Counter-Intuitive Conclusion: You May Not Need to Choose

The smartest project I've seen used all three:

Desktop client: SQLite for configs and session data
Backend service: PostgreSQL for business data
Middleware pipeline: LevelDB/RocksDB for time-series logs

No conflict. Different layers, different tools.

If you can only pick one:

  • Writing a desktop app → SQLite
  • Building a high-concurrency backend → PostgreSQL
  • Building a new storage engine → LevelDB or RocksDB
  • Building a small web project pre-funding → SQLite. It's enough. Migrate when you have real traffic.

Most projects will die with a few hundred users. Designing for ten million and dying before ten is the most common death. Pick the tool that lets you ship today.


I didn't consult any comparison table while writing this. Not because I have a good memory. Because I've used all three enough. You will too.