Day 3 Medium Database SQL vs NoSQL OLTP/OLAP ACID/BASE

数据库选型 — 不是『SQL 还是 NoSQL』那么简单Database Selection: SQL vs NoSQL, OLTP vs OLAP, ACID vs BASE

问题场景

面试官问『你为什么选 MongoDB』,资深工程师会被这种问题问翻车——因为正确答案不是『因为它快』,而是『因为我的访问模式是按 user_id 聚合读、写多读少、schema 经常变』。数据库选型不是技术信仰,是约束推导:数据模型、访问模式、一致性要求、规模、运维成本,五个轴决定一切。

反面案例多得很:Uber 2016 年从 Postgres 迁到 MySQL(因为 Postgres 复制协议的写放大),又在 2020 年自研 Schemaless 在 MySQL 上跑 KV;Pinterest 2012 年从 MongoDB 迁到 MySQL + 自家分片;Discord 2017 年从 MongoDB 迁到 Cassandra(消息存储 1 亿条/天),2023 年又迁到 ScyllaDB;Stripe 至今主库仍是 MongoDB——但他们用一套精密的应用层一致性。没有最优数据库,只有最匹配的

需求与约束(面试必问)

高层架构(典型数据栈)

graph LR
    APP["Application
API / Service"] CACHE["Redis
KV cache · 会话 · 排行榜"] OLTP["OLTP
Postgres / MySQL · DynamoDB"] SEARCH["Search
Elasticsearch · OpenSearch"] QUEUE["CDC / Kafka
binlog → 下游"] DWH["Data Warehouse
Snowflake / BigQuery · OLAP"] LAKE["Data Lake
S3 + Iceberg · 冷数据"] APP --> CACHE APP --> OLTP APP --> SEARCH OLTP -->|binlog| QUEUE QUEUE --> DWH QUEUE --> SEARCH QUEUE --> LAKE classDef app fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef cache fill:#1a1a30,stroke:#ffb450,color:#e8eef5 classDef oltp fill:#0e2030,stroke:#5eead4,color:#e8eef5 classDef olap fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 class APP app class CACHE cache class OLTP,SEARCH oltp class QUEUE,DWH,LAKE olap

现代后端往往是『多数据库共存』:OLTP 跑交易、OLAP 跑分析、Search 跑搜索、Cache 抗读

关键技术点

1. SQL vs NoSQL:不是二元对立,是数据模型选择

原理:『NoSQL』是个糟糕的命名,它其实是 4 类完全不同的数据库的总称:

类型代表数据模型典型场景
Relational (SQL)Postgres, MySQL, Oracle行 + schema + join交易、订单、关系强的业务
Key-ValueRedis, DynamoDB, RiakK → V(V 可能是 JSON)会话、缓存、计数器
DocumentMongoDB, Couchbase嵌套 JSON 文档用户画像、CMS、变 schema
Wide-columnCassandra, HBase, ScyllaDBrow key + 列簇时序、消息、海量写
GraphNeo4j, Neptune, TigerGraph节点 + 边社交关系、反欺诈、知识图谱
SearchElasticsearch, OpenSearch倒排索引全文搜索、日志、分析
Time-seriesInfluxDB, TimescaleDB, Prometheus(time, tags, value)监控、IoT、金融行情
SQL 的优势 NoSQL 的优势
现实案例:

2. OLTP vs OLAP:行存 vs 列存的本质区别

原理:交易系统(OLTP)和分析系统(OLAP)的访问模式完全相反:

这决定了存储格式:OLTP 用行存(一行所有字段在一起,便于点查整行);OLAP 用列存(同一列的值在一起,便于聚合 + 高压缩比 + SIMD)。

graph LR
    subgraph Row["Row Store (Postgres / MySQL)"]
        R1["[1, Alice, 30, US]"]
        R2["[2, Bob, 25, UK]"]
        R3["[3, Carol, 28, JP]"]
    end
    subgraph Col["Columnar Store (BigQuery / Snowflake)"]
        C1["id: [1, 2, 3]"]
        C2["name: [Alice, Bob, Carol]"]
        C3["age: [30, 25, 28]"]
        C4["country: [US, UK, JP]"]
    end
    Row -->|"SELECT * WHERE id=2 ✅"| Q1["点查快"]
    Col -->|"AVG(age) GROUP BY country ✅"| Q2["聚合快 · 压缩 10x"]
    classDef row fill:#0e2030,stroke:#5eead4,color:#e8eef5
    classDef col fill:#2a1530,stroke:#ff7ab6,color:#e8eef5
    class Row,R1,R2,R3 row
    class Col,C1,C2,C3,C4 col
Trade-off:
# 列存为什么聚合快:只读需要的列
# 一张表 100 列、10 亿行
# OLTP 行存做 SELECT AVG(salary):
#   要扫 10 亿 × 100 列的全部数据 = 海量 IO
# OLAP 列存做 SELECT AVG(salary):
#   只读 salary 这一列(10 亿 × 1 列),压缩后可能 1GB
#   + SIMD 向量化执行 → 秒级返回
现实案例:

3. ACID vs BASE:一致性的两种哲学

ACID(Atomicity, Consistency, Isolation, Durability):传统 RDBMS 的承诺。BASE(Basically Available, Soft state, Eventual consistency):NoSQL 的妥协。

ACIDBASE
原子性事务全成或全失败无;靠 idempotent 设计
一致性事务后所有人看到一致视图最终一致;窗口内可能读到旧值
隔离SI / RR / Serializable通常 read committed 都难保证
持久性commit 落盘(fsync)多副本异步,少数极端情况丢
典型Postgres, Spanner, CockroachDBDynamoDB(旧), Cassandra, Riak

但 2010 年代起,这条界线在模糊

# Postgres SERIALIZABLE 事务(最高隔离级别)
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 如果 balance >= 100, 扣款
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;
-- ⚠️ Serializable 下如果有并发冲突, 会抛 SerializationFailure
-- 应用层要捕获并重试 (退避 + jitter)
Trade-off:
现实案例:

4. 选型决策树:Postgres / DynamoDB / Cassandra / Redis

原理:四个主流选项覆盖 90% 场景。下面是一个实用决策路径。

graph TD
    Start{"需要事务 & Join?"}
    Start -->|"是"| SQL_q{"数据量 > 1TB?"}
    Start -->|"否"| KV_q{"延迟要求 < 1ms?"}

    SQL_q -->|"否"| PG["✅ Postgres
事务 · 复杂查询 · JSONB"] SQL_q -->|"是"| Dist_q{"全球多区?"} Dist_q -->|"是"| Span["✅ Spanner / CockroachDB
跨区 ACID"] Dist_q -->|"否"| TiDB["✅ TiDB / Vitess
分布式 MySQL"] KV_q -->|"是"| Redis["✅ Redis
内存 KV · 缓存 · 排行榜"] KV_q -->|"否"| Scale_q{"QPS > 50k 写 & 海量数据?"} Scale_q -->|"是"| Wide_q{"托管 / 自建?"} Scale_q -->|"否"| PG2["✅ Postgres
通常够用"] Wide_q -->|"托管"| DDB["✅ DynamoDB
无运维 · 按访问付费"] Wide_q -->|"自建"| Cass["✅ Cassandra / Scylla
多区写 · BASE"] classDef sql fill:#0e2030,stroke:#5eead4,color:#e8eef5 classDef kv fill:#1a1a30,stroke:#ffb450,color:#e8eef5 classDef wide fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 classDef q fill:#1a2530,stroke:#64c8ff,color:#e8eef5 class PG,PG2,Span,TiDB sql class Redis kv class DDB,Cass wide class Start,SQL_q,KV_q,Dist_q,Scale_q,Wide_q q
资深工程师的默认:除非你能说出『为什么不用 Postgres』的具体原因(QPS、数据量、特殊访问模式),就用 Postgres。它有事务、JSONB(半结构化)、全文搜索(tsvector)、地理(PostGIS)、时序(TimescaleDB 扩展)、列存(Citus),几乎一站式。Notion、Instagram、Robinhood、Reddit、GitLab、Heroku 全是 Postgres 大用户。
现实案例:

扩展与优化(增长后怎么办)

常见陷阱(资深工程师也常翻车)

1. 用 MongoDB 当关系库用:业务一旦稳定,你会发现『没有 join』是诅咒。要么应用层做 N+1(慢),要么数据冗余(写放大、一致性恶梦)。Stripe 至今为此付代价。
2. 把 Redis 当主存储:Redis 持久化(RDB/AOF)不是设计目标,挂掉 RPO 至少几秒。账户余额这种数据,Redis 是 cache 不是 source of truth。
3. ORM 自动生成 SQL:ActiveRecord/Django ORM 一不小心就 N+1 query 或全表扫描。生产前必须 EXPLAIN。
4. 忽视访问模式优先 schema:NoSQL(尤其 DynamoDB)必须『先想清楚怎么查再设计表』。partition key 选错=重做。AWS 官方推 single-table design,但学习曲线陡。
5. Schema migration 不可逆:大表加列、改类型可能锁表几小时。生产做法:在线 DDL(pt-online-schema-change / gh-ost)、分批 backfill、双写新旧字段。
6. 跨数据库分布式事务:『写 Postgres + 写 Redis』如果一致性敏感,要用 outbox pattern(Day 7 详讲),不要寄希望于 2PC。

面试问题示例

  1. 你设计一个 Twitter,timeline 用什么存?为什么不全用 MySQL?
  2. Postgres 和 MySQL 选哪个?说出 3 个真实差异(不要说『差不多』)。
  3. 什么场景下你会选 Cassandra 而不是 DynamoDB?
  4. 解释 BASE 模型的『S』(soft state)是什么意思,给个例子。
  5. 用户量从 10w 到 1 亿,数据库怎么演进?分几个阶段、每个阶段瓶颈是什么。
  6. HTAP 数据库(TiDB)和『OLTP + CDC 到 OLAP』两套方案,怎么选?

关键资源

English Summary

Database selection is constraint derivation, not technology religion. Five axes determine the choice: data model (relational / KV / document / wide-column / graph / search / time-series), access pattern (point lookup vs range scan vs aggregation vs join), consistency (ACID vs BASE), scale (QPS, data size, geo distribution), and operational cost. Default to Postgres unless you have a specific reason — its versatility (JSONB, full-text, geo, time-series extensions) covers 80% of needs. Reach for DynamoDB / Cassandra when write throughput exceeds ~50k QPS or you need horizontal scale-out; Redis when sub-ms latency is required; columnar warehouses (Snowflake / BigQuery / ClickHouse) when analytical aggregation dominates. Modern stacks combine multiple stores via CDC pipelines: OLTP handles transactions, search handles full-text, warehouse handles analytics.

深入思考(点击展开答案)

1. 同一个业务实体(如 user)在 OLTP(Postgres)和 OLAP(Snowflake)里都存了,访问 OLAP 总是滞后几小时。如果产品经理要『实时数据分析』,至少 3 种方案,分别什么代价?

关键:实时分析需要『分钟级或秒级新鲜度』,传统 ETL(每日批处理)做不到。

  • 方案 A:CDC + 流处理 → OLAP。Debezium 监听 Postgres binlog → Kafka → Flink/Spark Streaming → 写入 ClickHouse/Druid。新鲜度可达秒级。代价:流处理复杂、状态管理难、exactly-once 不容易;架构组件多 4-5 个。
  • 方案 B:HTAP 单一数据库。TiDB / SingleStore,行存 + 列存副本并存,自动同步。SQL 一致,迁移成本低。代价:列存副本异步同步仍有 lag(亚秒~几秒);写性能比纯 OLTP 低;运维复杂;社区生态比 Postgres 小。
  • 方案 C:直接在 OLTP read replica 上跑分析。开个独立 read replica(资源隔离),重 query 走这台。代价:行存做聚合慢(GROUP BY 100M 行可能要分钟);replica 资源大;查询写得不好会拖累 replication。
  • 方案 D:实时物化视图(Materialize, RisingWave)。订阅 CDC 自动维护聚合结果,查询毫秒级。代价:物化视图 SQL 表达力受限,复杂查询写不出来;状态膨胀。
  • 方案 E:Lambda 架构。批处理(每天)+ 实时层(增量),查询时合并。代价:两套代码、数据语义难对齐(已经被多数公司放弃)。

实战推荐:CDC + ClickHouse 是 2024 年最主流(Cloudflare、Uber、字节都用);Materialize 适合『SQL 即可表达』的分析。

2. DynamoDB 的 partition key 选『user_id』,结果有个『superuser』占了 30% 的流量,单 partition 被限流(3000 RCU 上限)。不能改 schema,怎么解?

这是经典的『hot partition』问题。DynamoDB 单 partition 硬上限:3000 RCU + 1000 WCU + 10GB。

  • ① Write sharding(散列尾缀):把 superuser 的写打到多个虚拟 partition。比如 partition key 改为 user_id#{0..15},写时随机选;读时 16 个并发 query 后合并。代价:读放大 16 倍;只适合写热。
  • ② 应用层缓存:superuser 的数据放 DAX(DynamoDB Accelerator,托管 cache)或 ElastiCache。读请求大部分被缓存吸收,只有 miss 才打 DDB。代价:stale 窗口、缓存一致性问题。
  • ③ Adaptive Capacity(AWS 自动):DynamoDB 已经会把 hot partition 自动分裂、给更多容量。但有上限,且分裂期间 throttle 会持续几分钟。被动方案。
  • ④ On-Demand 模式:切到按需计费,AWS 内部会更激进地预分配容量。代价:成本可能 2-7 倍。
  • ⑤ 数据冗余 / 二级表:把 superuser 数据复制一份到独立的小表(不同 partition key),读 superuser 走小表。代价:双写、一致性;适合『极少数极热』的场景。

本质教训:DynamoDB 的『先想访问模式再设计 partition key』不是说说而已。一旦上线发现 hot key,迁移代价巨大。这也是为什么很多团队用了 DDB 几年后又迁回 Postgres + 分片。

3. 资深工程师常说『从 Postgres 开始,不行再换』。给一个具体场景,Postgres 真的不行、必须换的临界点(QPS、数据量、特性)在哪?

Postgres 单机临界点(在 AWS r6i.32xlarge / 128 vCPU / 1TB RAM 配置下):

  • 写 QPS 上限约 50k-80k(含 WAL 落盘)。再高就要分片,单机 WAL 写成为瓶颈。Aurora 通过自家 storage layer 可以推到 ~200k,但 cost 翻几倍。
  • 数据量上限约 5-10TB。再大维护操作(vacuum、index rebuild、备份恢复)窗口太长,autovacuum 跟不上 dead tuple 速度。
  • 连接数上限约 5k(每个连接 ~10MB 内存 + 进程 fork)。所以高并发服务必须用 PgBouncer / RDS Proxy 池化。
  • p99 写延迟敏感(要求 < 5ms):fsync 抖动会让 p99 飙到几十 ms;批量 vacuum / checkpoint 期间更糟。
  • 跨区强一致:Postgres 的同步复制(synchronous_commit)跨区延迟会让写变成 100ms+。这种就要 Spanner / CockroachDB。

具体场景

  • IoT 时序写入 1M QPS → 必须 Cassandra/ScyllaDB/TimescaleDB 分片。
  • 全球多区写入(用户在任何区都要低延迟写)→ CockroachDB / Spanner。
  • 列式聚合扫 100 亿行 → ClickHouse / BigQuery。
  • 毫秒级排行榜 + 实时计数器 → Redis(ZSET / HINCRBY)。
  • 图遍历 5 度好友 → Neo4j / TigerGraph(Postgres recursive CTE 写得出来但性能差几个数量级)。

实战:Instagram 用 Postgres 跑到 IPO(10 亿 DAU),靠分片 + 良好访问模式设计。所以『Postgres 不够用了』往往是『设计不够好』的代名词。

4. Cassandra 的『eventual consistency』,应用层怎么处理 stale read?给两个真实场景的设计方案。

Cassandra 默认 R=1, W=1(最快但最不一致)。生产通常 R=QUORUM, W=QUORUM(R+W > N 保证一致),N=3 时即 R=2, W=2。但即使 quorum 也有 stale read 窗口(写到 2 个、复制到第 3 个之前读那一台)。

场景 1:Discord 消息列表

  • 消息按 channel_id 分区、msg_id 排序。新消息可能短暂看不到。
  • 设计:客户端订阅 WebSocket 推送(gateway 直接推),不依赖 DB 读。DB 读只用于历史拉取,stale 100ms 用户无感。
  • 关键技巧:『写后立即推送』,绕过 DB 一致性窗口。

场景 2:电商订单状态

  • 用户付款后立刻跳订单页,可能读到旧状态(pending 而不是 paid)。
  • 设计:用 read-your-writes 一致性——客户端带上『最后写入的 timestamp 或 version』作为 hint,应用层确保读到 ≥ 这个版本(轮询 / 重试 / 路由到写过的副本)。
  • 或者:付款回调时同步更新一份 Redis cache,用户看到的是 cache(强一致),后台异步对账 Cassandra。

通用模式

  • Read-repair:Cassandra 内置,读时如果发现副本不一致会触发后台修复。
  • Hinted handoff:写时如果某副本挂了,其他节点暂存『提示』,恢复后回放。
  • Lightweight Transaction(LWT / Paxos):Cassandra 提供 CAS 写(IF NOT EXISTS),代价是慢 4-10 倍。
  • 应用层版本控制:写时带 version,读取后客户端做 version-aware 校验。

核心理念:BASE 系统的一致性问题要在『产品层』解决,不要妄想在 DB 层解决。Discord 不读 DB 就拿不到新消息?那就推过去。

5. 容量估算:每天 1 亿条订单,每条 1KB,保留 5 年。OLTP(Postgres)需要多少存储?SSD 还是 HDD?预算多少?

裸数据估算

  • 每天 1 亿 × 1KB = 100 GB/天
  • 5 年 ≈ 1825 天 × 100 GB = 182.5 TB

实际占用(×3-5 放大因子)

  • 索引:主键 + 通常 3-5 个二级索引,每个索引开销约原数据 20-40%。总索引 ≈ 1.5x 数据 = 270TB。
  • WAL(write-ahead log):通常保留 7-30 天,约 10TB+。
  • Vacuum bloat:Postgres MVCC 留 dead tuple,没 vacuum 干净会膨胀 20-50%。
  • 备份:通常保留多个快照,本地 + 异地,再 ×3。
  • 合计估算:原始数据 180TB → 实际生产存储需求 600TB-1PB。

SSD vs HDD

  • OLTP 必须 SSD(NVMe)。HDD 寻道 5-10ms,p99 SLA 没法看;NVMe p99 < 100μs。
  • AWS gp3 SSD 约 $0.08/GB/月,io2 约 $0.125/GB/月。
  • 1PB SSD ≈ $80k-$125k/月。一年 $1-1.5M 仅存储。

结论:单机扛不住

  • 必须分片:按 user_id / shard_key 拆 32-128 个逻辑 shard。
  • 冷热分离:最近 90 天热数据在 Postgres SSD(30TB),90 天-1 年温数据在 Aurora I/O Optimized(150TB),1 年以上冷数据归档到 S3 + Iceberg(HDD-tier,$0.004/GB/月 vs SSD $0.08/GB/月,省 20 倍)。
  • 分层后实际 SSD 需求只剩 100-200TB,成本压到 $10k-20k/月。

这道题面试官真正想听:你会不会算放大因子(索引/WAL/备份/bloat)、知不知道分片、知不知道冷热分层省钱。只算原始 180TB 就拍 SSD 一锅端,是新手做法。