SQL 还是 NoSQL

选数据库不是在 SQL 和 NoSQL 之间二选一,而是按每类数据的主访问路径和不能拆开的约束,给它指定一个 canonical owner。比较关系型、key-value、document、wide-column、graph、time-series 六种模型的状态归属、扩展单位、命名故障和恢复方式,附电商平台的完整落位、算例和面试答法。

这一章回答三个问题:哪条业务不变量不能被拆到两次写里、热路径按什么形状读数据、每份数据的 canonical owner 是谁。答完这三个问题,数据库类型基本就定了;先报数据库品牌,通常会被追问到答不上来。

在 database 世界里,常说的两类方案是 SQL(relational)和 NoSQL(non-relational)。Relational database 结构化且有预定义 schema;「NoSQL」则是一大类模型的统称,key-value、document、wide-column、graph、time-series 的数据结构、扩展方式和故障形态各不相同,不能当成一个选项来比较。

High-level differences

SQL 与 NoSQL 的传统对比维度如下,其中几条已经补上了今天的实际情况。

Storage

SQL 把数据存到 tables:每行是一个 entity,每列是该 entity 的一个 data point。

NoSQL 有多种 storage model,例如 key-value、graph、document 等。

Schema

在 SQL 里,每条记录都必须符合固定 schema,列必须提前设计好。Schema 可以后期改,但需要 migrations。(PostgreSQL 的 jsonb 类型也能在一列里存结构不固定的 JSON,所以「需要灵活字段」本身不足以成为换数据库的理由。)

NoSQL 的 schema 更动态。字段可以随时新增,且每条 record 不一定包含每个 field。

Querying

SQL database 用 SQL(structured query language)做定义与操作,表达能力很强,可以临时写任意 join。

NoSQL 的 query 通常围绕 key 或预先设计好的访问路径,不同 database 有不同语法。Cassandra 这类 wide-column 存储要求先确定查询,再推出主键。

Scalability

关系型数据库的写通常集中在一个 primary 上,先靠垂直扩展和读副本;跨多台 server 分片可以做,但跨分片 join 和事务会变复杂。

许多 NoSQL 系统把分片作为内建能力(Redis Cluster 的 hash slot、MongoDB 的 shard key、Cassandra 的 partition key),加节点更直接。代价是查询必须带上分片键,否则就要扇出到所有分片。

Reliability

多数 relational databases 符合 ACID,因此在 data reliability 和 transactions 方面更有保障。

NoSQL 方案通常为了 performance 与 scalability,在一致性或多记录事务上做取舍,但各家差别很大:MongoDB 支持多文档事务,Cassandra 的一致性按请求可调。要看具体产品的具体配置。

有约束的设计问题

一个全球电商平台,多个区域,合计 50k writes/s、200k reads/s,读 p95 要低于 120 ms(这些数字是面试场景假设,不是基准测试结果)。它有六类数据:

  1. 订单与支付:要事务和引用完整性,要能审计。
  2. Session、幂等记录、功能开关:按精确 key 读写,延迟敏感。
  3. 商品目录:字段随品类变化,通常整条读出。
  4. 活动流:追加为主,按租户加时间范围查询。
  5. 风控:要沿着「账户 → 设备 → 支付卡 → 其他账户」走好几跳。
  6. 遥测:按时间写入,原始数据保留 30 天,汇总数据保留 18 个月。

客户端从不直接连数据库,所有读写都经过鉴权的 API 或采集网关。一个平台用几种数据库可以,但每条数据只能有一个声明过的 canonical owner;其他副本都是可以重建的投影或缓存。

六种模型对比

模型状态归谁扩展单位一致性、延迟与代价适合
关系型事务核心primary 的事务日志和带约束的表分片或租户;读副本分担读事务边界强;join 和索引增加写入成本订单、支付、需要多行事务的业务状态
Key-value 分区环负责某段 hash slot 的 primaryhash slot / 分区单 key 延迟低;多 key 操作和热 key 受限Session、幂等记录、计数器、缓存
Document 聚合集群所在分片上的整个文档shard key 范围或哈希、chunk单文档原子更新;shard key 选错会扇出查询商品、内容这类字段多变、整体读写的聚合
Wide-column 写路径负责该 partition key 的副本partition key / token 范围一致性可调、写吞吐高;compaction 和 repair 有运维成本追加为主、按分区加范围查询的活动流
Graph 关系核心选举出的 writer 加复制的属性图图的业务域;读副本遍历局部性好;写集中在 leader,超级节点会爆多跳关系查询:风控、权限、推荐
Time-series 生命周期按时间分块的数据和派生的汇总时间 chunk按时间追加和查询;保留策略和汇总刷新要协调指标、传感器、可观测性数据

1. 关系型:先守住不能拆开的不变量

一次事务同时写订单、支付意图和库存引用,要么全成功要么全失败。PostgreSQL 的 primary key 和 foreign key 在数据库层面保证身份唯一和引用完整;MVCC 让读不阻塞写。容忍旧数据的查询可以走读副本;「我刚下的单」这类读自己写的请求回 primary,或者等副本追上指定的复制位置。

代价:每加一个索引和约束,写入都要多做一份维护工作;一旦分片,跨分片的事务和 join 就要自己处理。

2. Key-value:只按精确 key 访问的数据

Redis Cluster 把 key 空间分成 16384 个 hash slot,HASH_SLOT = CRC16(key) mod 16384,每个 slot 归一个 primary,primary 有副本用于故障切换。需要在一次操作里原子处理多个 key 时,用 hash tag 让它们落在同一个 slot:

SET session:{u42} ...
SET idem:{u42}:order-7781 ... NX EX 86400

花括号里的 u42 决定 slot,这两个 key 必然在同一个节点上。订单这类要长期保存、要审计的数据不应该以 key-value 为 canonical owner。

3. Document:读写边界就是一个文档

商品的规格、变体、图片列表嵌入在同一个文档里,一次读就拿全,单文档更新是原子的。MongoDB 的应用通过无状态的 mongos 访问分片集群,mongos 按 shard key 把查询路由到对应分片;查询不带 shard key,就要发到所有分片再合并(scatter/gather)。MongoDB 单个文档上限是 16 MiB,这是 MongoDB 的限制,不是所有 document 数据库的通用上限。

4. Wide-column:先写查询,再推主键

Cassandra 的写先进 commit log 和内存里的 memtable,之后刷成不可变的 SSTable,再靠 compaction 合并。数据按 partition key 定位,partition 内按 clustering key 排序。活动流的查询是「某租户某时间段内的事件,按时间倒序」,表就按它设计:

CREATE TABLE activity_by_tenant (
  tenant_id  text,
  hour       timestamp,
  bucket     int,
  event_time timestamp,
  event_id   timeuuid,
  payload    text,
  PRIMARY KEY ((tenant_id, hour, bucket), event_time, event_id)
) WITH CLUSTERING ORDER BY (event_time DESC, event_id DESC);

hour 和 bucket 是为了防止一个大租户把所有数据写进一个无限增长的 partition。没有设计进主键的查询,这张表答不了。

5. Graph:join 是主要工作量时

风控要找「和这张卡共用过设备的账户,再和这些账户共用过收货地址的账户」。在关系库里这是多层自 join;在属性图里是沿着有类型、有方向的关系遍历。Neo4j 集群在 primary 之间选举一个 writer,读副本扩展读能力。图通常是从订单库和账户库派生出来的投影,而不是订单的 canonical owner。

6. Time-series:保留策略和汇总要一起设计

TimescaleDB 的 hypertable 按时间切成 chunk,查询时跳过不相关的 chunk;保留策略删除的是完整的 chunk,不是单行。原始数据保留 30 天、汇总保留 18 个月,要这样配:

SELECT add_retention_policy('telemetry', INTERVAL '30 days');
SELECT add_continuous_aggregate_policy('telemetry_hourly',
  start_offset => INTERVAL '7 days',
  end_offset   => INTERVAL '1 hour',
  schedule_interval => INTERVAL '1 hour');

汇总的刷新窗口(最近 7 天)必须落在原始数据还在的范围(30 天)之内。官方文档举过反例:保留期设成 1 天、刷新窗口是 7 天到 1 天前,刷新时发现原始数据被删了,汇总表里的数据也跟着被删光。

故障与恢复

模型故障用户看到什么恢复
关系型结账后读副本滞后刚下的订单在「我的订单」里看不到读自己写的请求回 primary,或等副本追上复制位置;已提交数据在 primary 上没丢
Key-value热 slot 过载共享这个 slot 的所有 key 都变慢重新设计 key 打散热点;有界的故障切换;限流降级
Document按品类筛选商品时不带 shard key全集群扇出,p99 飙升查询带上有选择性的 shard key 条件,或建受治理的二级投影
Wide-column大租户的 partition 无限增长该租户的读、repair、compaction 都变慢partition key 加时间桶;repair 后回填
Graph遍历经过超级节点(一个设备连了几十万账户)风控查询超时,拖慢共享读资源限制遍历深度和关系类型;预计算投影;查询预算
Time-series保留策略比汇总刷新窗口还激进历史报表的某段时间变成空白停掉保留策略;有外部备份就恢复原始 chunk 再刷新;让两个窗口不重叠

算一遍:活动流的 partition 有多大

假设最大的租户每秒产生 2000 条活动事件,每条约 200 字节。

  • 只按 tenant_id 分区:这个 partition 永远增长,一天就多 2000 × 86400 ≈ 1.73 亿 行,约 35 GB。
  • 按 (tenant_id, day):每个 partition 一天 1.73 亿行,仍然只落在 RF 个副本上,这几台机器承担这个租户的全部写入。
  • 按 (tenant_id, hour, bucket),bucket 取 0–15:每小时 720 万行分到 16 个 partition,每个约 45 万行、约 90 MB,写入分散到更多节点。代价是读「最近一小时」要并行查 16 个 partition 再合并。

分桶数量要按实测的 partition 大小和读放大一起定,这里只演示推算方法。

怎么选,落回开头的六类数据

订单与支付归关系型;session 和幂等记录归 key-value 并设 TTL;商品目录归 document,按品类的复杂筛选交给单独的搜索索引;活动流归 wide-column;风控关系是从订单库和账户库派生的 graph 投影,设遍历深度上限;遥测归 time-series,保留策略绝不能删掉刷新窗口还要读的原始数据。只为明显不同的 workload 加新模型:一个库管所有数据会让 workload 互相拖累,每个服务换一种库则让运维和归属失控。

多种数据库并存时,要维护一张归属表:每份副本的 canonical owner、谁生产投影、新鲜度 SLO、从哪里重建、删除怎么传播、谁有权迁移归属。没有这张表,用户注销时你说不清要删几个地方。

Trade-off 快览

sql-vs-nosql

  • SQL:强 consistency、复杂 query 能力、transaction 友好;但 horizontal scaling 难度高
  • NoSQL:schema 灵活、scale-out 友好、按 key 读写性能好;但 ACID / joins 能力可能受限,查询要预先设计

面试时这样回答

  1. 复述约束和数据分类。 读写量、延迟目标,以及哪几类数据的访问形状不一样。
  2. 先说不变量。 「订单、支付、库存引用必须在一个事务里」,所以它们归关系库。
  3. 再说热路径的形状。 按精确 key、按聚合、按分区加时间范围、按多跳关系、按时间窗口,每种形状对应一个模型,并写出具体的 key 或分片键。
  4. 说清归属。 哪个是 canonical owner,哪些是可重建的投影,投影从哪里重建。
  5. 说一个故障和恢复。 例如读副本滞后导致看不到刚下的订单,读自己写的请求回 primary;或 partition 无限增长,用时间分桶修。

相关章节:Database 概述、SQL databases、NoSQL databases、Indexes、Transactions、Database Replication、Database Sharding、PACELC Theorem。想看分片键怎么选、热点怎么出现,打开互动 Lab:/system-design-lab/database-sharding-architectures。

一手证据

上表的六种分类、场景数字和选型规则是 JR Academy 的归纳;文档支撑的是各自的机制,不是性能排名。