同一个「存数据」的需求,为什么长出了三种完全不同的数据库?

PostgreSQL、Redis、DuckDB——三者都管自己叫「数据库」,但架构几乎没有一处相同。本文从一个问题出发:为什么「存数据」这一件事,需要三种截然不同的答案?

先说个真实感受:新人问我「该用什么数据库」时,我最怕听到的是「就……存点数据」。因为「存数据」这个需求背后,至少藏着三个互相矛盾的目标:要快(延迟)、要对(一致性)、要能算(分析吞吐)。这三个目标在物理层面就是打架的,所以业界长出了三种完全不同形态的数据库,各自押注一个目标。

这篇拿 PostgreSQL(75K+ Star 里的常青树)、Redis(75K Star)、DuckDB(40K Star)当样本——它们恰好是三条路线的教科书级代表。

三种数据库,三种「世界观」

PostgreSQLRedisDuckDB
一句话定位数据的基础设施远程数据结构接口SQLite for Analytics
押注的目标正确性 > 性能延迟 > 一切分析吞吐 + 零部署
数据在哪磁盘(缓冲进内存)内存就在文件里(CSV/Parquet/S3)
并发模型多进程、MVCC 多版本单线程事件循环单写者、Morsel 并行读
典型场景金融、ERP、订单缓存、会话、排行榜、限流数据科学家本地跑 SQL 分析

注意:这三行的每一列差异都不是「实现细节」,全是世界观分歧。往下看。

PostgreSQL:把「正确」放在所有选项的前面

PostgreSQL 从 1986 年 Berkeley 的学术项目走到今天,38 年没死,靠的不是快——它从来不追求最快。它的护城河是正确性和可信度的复利

几个能看出价值观的设计:

1. Process-per-connection:每个连接一个独立进程。 MySQL(InnoDB)用线程池,PG 用进程。进程更重、启动更慢,但一个连接崩了不会带走别人——内存隔离是操作系统保证的,不是代码纪律保证的。这是「可靠性高于便利」的字面体现。

2. MVCC 的代价是 VACUUM。 读写互不阻塞的代价是旧版本数据堆在表里,得靠 VACUUM 回收。很多人骂 VACUUM 烦,但这笔交易是明码标价的:读永远不阻塞写、写永远不阻塞读,换来的运维成本。对比 MySQL undo log 的回收方式,各有取舍,但 PG 把旧版本直接放主表的设计,让「时间旅行」「闪回查询」这类能力成为可能。

3. 默认值永远站在安全侧。 full_page_writes 默认开(断电防撕裂页)、WAL 预写日志保证持久性、SSI 串行化隔离提供最高级别的正确性保证。每一个默认值都在说:宁可你嫌慢,不能让你丢数据。

4. 插件化是真正的「乐高」。 自定义类型、运算符、索引方法、存储引擎(Table AM)、外部数据包装器(FDW)——PostGIS 是插件、pgvector(向量检索)也是插件。PG 不追风口,它提供一个能让你自己造风口底座。GIS 火了有 PostGIS,AI 火了有 pgvector,都是社区在它的扩展点上长出来的。

30 多年每年一个大版本的发布节奏,本身就是产品哲学:向后兼容是铁律,功能慢慢加,但加进去的就永远在。金融、ERP 这种「数据错了要出人命」的场景选它,图的就是这个确定性。

Redis:单线程是深思熟虑,不是没想到多线程

Redis 是三个里面最容易被误解的。「单线程还能快?」——能,而且正因为单线程才快。

先破除一个迷信:Redis 不是「缓存数据库」,它是「远程数据结构接口」。 它把 String/Hash/List/Set/ZSet/Stream 这些数据结构放在内存里,用网络协议暴露出去。你操作的从来不是表和行,是数据结构本身。

为什么单线程反而快?因为它的瓶颈从来不在 CPU,在网络 I/O。想清楚这一点,后面所有设计都顺了:

graph LR A[瓶颈在网络 I/O] --> B[epoll 事件循环<br/>一个线程管所有连接] B --> C[命令执行无锁] C --> D[无上下文切换] C --> E[CPU 缓存友好] D --> F[亚毫秒延迟] E --> F

多线程共享数据要加锁,锁带来上下文切换和缓存失效——在亚毫秒级的世界里,这些开销就是灾难。单线程事件循环直接把「锁」这个概念从代码里删掉了。命令执行天然原子,INCR 永远不会竞争,这不是特性,是架构赠品。

几个教科书级的工程细节,值得每个后端工程师抄作业:

  • 渐进式 Rehash:百万级 key 的哈希表扩容,拆成每次操作迁移一个桶,永不阻塞。大表 rehash 这种「必然卡顿」的操作被摊薄到不可感知。
  • 编码多态:同一个 List,小的时候用 listpack(紧凑连续内存),大了自动切 dict/skiplist。按数据规模自动选最省的表示,官方说法能省 80% 内存。你以为你在用一种数据结构,其实底下是四种。
  • fork + COW 做 RDB 快照:fork 出子进程,靠操作系统的写时复制,主进程继续服务请求,快照在子进程里慢慢写。不阻塞业务的持久化,白嫖操作系统能力。
  • 跳表(SkipList)代替平衡树:ZSet 的有序结构用跳表——实现比红黑树简单 10 倍,性能差不多,范围查询还更顺手。「简单」本身就是可维护性。

顺带一提:2024 年 Redis 许可证从 BSD 换成 AGPLv3/RSALv2/SSPLv1 三选一,直接催生 Linux 基金会下的 Valkey 分叉。这是开源可持续性问题的经典案例——协议也是架构的一部分,选型时别只看性能跑分。

DuckDB:把 20 年的学术成果做成「pip install 即用」

DuckDB 是我近年最喜欢的项目之一,因为它精准地找到了一个无人区

  • SQLite 是嵌入式 OLTP——单行读写很强,跑全表聚合分析很痛苦;
  • ClickHouse 是服务器 OLAP——分析飞快,但要部署、要运维、要集群;
  • 中间的空档:数据科学家想在笔记本上对几个 GB 的 CSV/Parquet 跑 SQL 分析,谁来管?

没人管,直到 DuckDB。它的答案是「三零」:零部署、零运维、零配置——pip install duckdb,默认就用 80% 内存和全部核心,数据在哪就在哪查(CSV、Parquet、S3 直接 SQL 引用,不强迫导入)。

技术上它是个「学术成果工程化」的集大成者:

  • 解析器直接复用 PostgreSQL 的 libpg_query——不重造轮子,SQL 兼容性白嫖 PG 三十年积累。
  • 向量化执行:数据按 2048 行一批(DataChunk)处理,贴 CPU 缓存、喂饱 SIMD。对比 PG 逐行拉取(Volcano 模型),分析查询快 10-100 倍。
  • Morsel-Driven 并行:把数据切成小任务块动态分配给线程,自动负载均衡,不用你手动分区。
  • 列式存储 + 自动压缩:RLE、Dictionary、FSST、BitPacking 自动挑,分析场景只读相关列,I/O 直接砍到零头。
  • 单写者模型:明确声明只支持一个写者,于是 MVCC 可以大幅简化,用 Undo Buffer 替代 VACUUM——又是「砍功能换简单」的范例。

还有个细节我很喜欢:它在 0.x 版本赖了很多年,就是不肯发 1.0,因为存储格式还想改。直到 2024 年才发 1.0 并承诺格式稳定。用版本号管理用户的预期,这也是工程纪律。

放一起看:三条路线的取舍矩阵

问题PG 的答案Redis 的答案DuckDB 的答案
断电了怎么办?WAL + full page writes,绝不丢承认内存会丢,RDB/AOF 尽力而为没这问题——只读分析,数据还在原文件
并发写怎么办?MVCC 多版本,读写互不阻塞单线程串行,根本没有并发写只允许一个写者
内存不够怎么办?缓冲池 + 磁盘兜底淘汰策略(LRU 等),数据本来就该有热度分层流式分批处理,不要求全量进内存
数据量暴涨怎么办?分区、FDW、扩展Cluster 16384 槽分片不解决——坚持单机,复杂性留给上层

看出来了吗?每个数据库的「缺点」,都是它核心取舍的另一面。 PG 慢,因为它把正确性排第一;Redis 数据可能丢,因为它把延迟排第一;DuckDB 不能多人并发写,因为它把零部署排第一。

选型实战:别问「哪个好」,问「瓶颈在哪」

我这些年的选型习惯,浓缩成几个判断:

  1. 交易、订单、账务——PG 起步。 需要强一致性和复杂 SQL 的场景,不要用别的东西赌运气。
  2. 热点数据读写、计数器、限流、会话——Redis(或 Valkey)。 但记住它是缓存语义:数据要能从别处重建,或者你能接受丢失。
  3. 分析师在本地跑数、Jupyter 里探索数据——DuckDB。 别再 pd.read_csv 然后内存爆炸了,SELECT * FROM 'data.parquet' WHERE ... 直接跑。
  4. 组合拳才是生产常态:PG 做真相源(source of truth)+ Redis 做热点加速 + DuckDB/ClickHouse 做分析,三者互补而非竞争。

一个反例提醒:不要用 Redis 当主存储(丢了补不回来),不要用 PG 跑大规模分析(行存 + 逐行执行,分析师会骂街),不要用 DuckDB 做在线服务(单写者模型扛不住并发)。每个工具的边界,就是它设计哲学的边界。

番外篇:Presto 与存算分离的取舍

三条路线讲完,其实还有第四种形态绕不开——分布式查询引擎,代表是 Presto(prestodb/presto,16.7K Star)。它值得单独说,不是因为功能,而是因为它把「存算分离」这个架构命题做到了极致。

Presto 2012 年诞生于 Facebook,解决一个当时很痛的问题:数据仓库里的海量数据,跑个交互式查询要等几十分钟。它的架构是典型的 MPP:

graph TD A[SQL 请求] --> B[Coordinator<br/>解析/计划/调度] B --> C[Worker 1] B --> D[Worker 2] B --> E[Worker N] C & D & E -->|纯内存流水线执行<br/>中间结果不落盘| F[结果汇聚返回] C & D & E -.->|Connector 拉数据| G[(HDFS / S3 / Hive / MySQL...)]

注意最底下那层:Presto 自己不存任何数据。它通过 Connector 去 Hive、HDFS、S3、甚至 MySQL 里现拉数据,纯内存流水线执行,中间结果不落盘。查询完即走,片甲不留——这是存算分离最纯粹的形态。

顺带一段八卦:2019 年三位原始作者离开 Facebook,把社区版 fork 出去创立了 Trino(原 PrestoSQL);Facebook 则把 Presto 捐给了 Linux 基金会(即 prestodb)。一个项目长出两个顶级分支,本身就说明这个生态位有多重要。

分离还是一体?本质是一道成本题

存算分离 vs 存算一体,不是技术先进性的问题,是在存储、算力、网络三种资源的价格结构下做取舍

维度存算一体(ClickHouse / DuckDB 本地文件)存算分离(Presto / Trino / Spark SQL)
成本结构扩容必须存储算力一起买,闲时浪费数据躺廉价存储,算力按需付费
弹性差——搬数据才能扩好——加 Worker 节点即扩容
查询效率本地 I/O,扫描快受网络带宽约束,靠缓存和谓词下推补
数据共享多引擎 = 数据复制 N 份一份数据喂多个引擎(Presto/Spark/Hive 同吃一个湖)
运维复杂度数据本地性调度是难题存储托管给 S3/HDFS,自己只管计算

把这张表读透,你会发现分离架构的每一分收益都标好了价格

  • 省了存储冗余,就要付网络传输的延迟;
  • 换来弹性伸缩,就要忍受冷启动和缓存预热的不确定性;
  • 一份数据多引擎共享很美,但数据格式(Parquet/ORC)、分区设计、元数据管理成了新的生死线——湖仓(lakehouse)这个词火起来,就是在补这门课。

反过来看一体化阵营也没有躺平:ClickHouse 出了云服务往分离走,Snowflake 用商业成功证明了分离可行,而各家分离架构又在拼命加本地磁盘缓存——钟摆在分离与一体之间来回摆,摆的动力就是存储/算力/网络三者的实时价格比。

落到选型上其实很朴素:数据已经躺在对象存储/数据湖里、查询是交互式分析、团队不想维护存储集群——Presto/Trino 是对的答案;对延迟极敏感、查询模式固定、吞吐优先——ClickHouse 这类一体化引擎仍是王者。别问哪个先进,问你的数据在哪、钱花在哪。

写在最后

三个数据库,三种世界观,但有一个共同的启示:顶级开源项目的竞争力,不在于功能多,而在于敢于不做。

PG 明确不做最快的数据库,Redis 明确不做通用数据库,DuckDB 明确不做分布式,Presto 明确不做存储。它们都在文档和架构里把「不做什么」写得清清楚楚——用户因此能精确地知道边界在哪,这比任何跑分都值钱。

下次做选型,先别打开 benchmark 网站。先问自己四个问题:我的数据丢了会死吗?我的延迟预算是多少?谁来运维?数据现在躺在哪?答案有了,数据库自己会跳出来。