NoSQL databases

NoSQL 建模从访问模式出发:先列出要回答的查询,再推出 partition key、sort key 和二级索引。用 DynamoDB 单表设计走一遍订单系统,讲清 400 KB item 上限、每分区 3,000 RCU / 1,000 WCU、Query 1 MB 分页、filter 不省容量;再逐类说明 document、key-value、graph、time-series、wide-column、multi-model 各自答不了什么查询,附面试答法。

这一章回答三个问题:NoSQL 的表为什么要先写查询再建、一个 key 设计错了会在哪里爆、每一类 NoSQL 答不了什么查询。 六种数据模型之间怎么选、一个平台里怎么给每份数据指定 canonical owner,见 SQL 还是 NoSQL,这里不重复选型,只讲选定之后怎么建模。

NoSQL 是一个大类,指不以 SQL 作为主要数据访问语言的 database,也常被称为 non-relational database。与 relational database 不同,NoSQL 数据不一定遵循预定义 schema。很多 NoSQL 系统默认提供的是 BASE 式的最终一致,但这不是一条规则:MongoDB 支持多文档事务,DynamoDB 支持强一致读和 TransactWriteItems,要看具体产品和具体配置。

有约束的设计问题

一个订单系统决定用 DynamoDB,需要回答五个查询:

  1. 按订单号取订单详情和它的所有明细行。
  2. 某个客户最近的 20 单,按时间倒序。
  3. 客服按状态查「今天所有待发货的订单」。
  4. 按客户 ID 取客户资料。
  5. 大促时,一个爆款商品的库存每秒被扣几千次。

DynamoDB 的建模文档写得很直白:关系库可以先规范化建模、不考虑访问模式;DynamoDB 在你知道它要回答哪些问题之前,不应该开始设计 schema。

用单表设计回答五个查询

把订单、明细、客户放在同一张表里,用通用的 PK、SK 两个属性:

实体PKSK其他属性
客户资料CUSTOMER#c42PROFILEname、tier
客户的订单索引CUSTOMER#c42ORDER#2026-09-22T10:31:00#o789total、status
订单头ORDER#o789HEADERcustomer_id、status
订单明细ORDER#o789ITEM#001、ITEM#002…sku、qty
  • 查询 1:Query PK = ORDER#o789,一次取回订单头和全部明细,它们在同一个 partition 里。
  • 查询 2:Query PK = CUSTOMER#c42 AND begins_with(SK, 'ORDER#'),ScanIndexForward=false、Limit=20。时间放在 SK 前缀里,排序就免费了。
  • 查询 4:GetItem PK = CUSTOMER#c42, SK = PROFILE。
  • 查询 3:给订单头加属性 GSI1PK = STATUS#pending#2026-09-22、GSI1SK = created_at(下单时间),建一个 GSI。注意 GSI 只支持最终一致读,刚改成待发货的订单可能晚一会儿才出现在客服列表里。如果一天的待发货量很大,这个 GSI 分区本身会变热,要在 GSI1PK 后面加分桶后缀。
  • 查询 5:见下面的热分区。

没有设计进 key 的查询,这张表答不了。比如「金额大于 1,000 的所有订单」:只能 Scan 全表,或者再加一个 GSI,或者把数据导到分析系统去查。

四个硬限制和它们的后果

限制(DynamoDB 文档)后果设计上的应对
单个 item 最大 400 KB把订单的所有明细塞进一个 item 的 list 里,大订单写不进去明细拆成同一 PK 下的多个 item;大字段放 S3,item 里只存对象 key
每个分区最多 3,000 读单位/秒、1,000 写单位/秒单个热 key 的写超过 1,000 WCU 会被限流,加表容量也没用写分片:key 加后缀分散写,读时合并
一次 Query 最多返回 1 MB客户有几万单时一次拿不完用 LastEvaluatedKey 分页,接口返回游标
filter expression 在读取之后才过滤文档写明带不带 filter,Query 消耗的读容量相同能用 key 条件缩小范围的,不要靠 filter

算一遍热分区。 假设爆款商品的库存 item 小于 1 KB,每次扣减是一个条件写,消耗 1 个 WCU。大促峰值 3,000 次/秒,是单分区上限的 3 倍。把库存拆成 SKU#hot#0 到 SKU#hot#9 十个 item、每个放总量的十分之一,每次随机选一个扣,峰值时每个约 300 次/秒;代价是某个分片扣完了要换一个重试,查总库存要读 10 个 item 再相加。拆多少份要按实测峰值定,这里只演示推算;写分片的完整拓扑见 Database Sharding。

常见类型:各自擅长和答不了什么

Document

Document database(文档型)以 documents 存储数据,一个文档通常对应一个业务聚合(一个商品连同它的规格、图片列表)。MongoDB 的建模文档默认推荐嵌入,并列出改用引用的情况:嵌入会造成重复、而读性能收益抵不过重复的代价(比如被嵌入的数据经常变);要表达复杂的多对多关系或很大的层级数据;需要经常单独查询被关联的实体。单个 BSON 文档上限 16 MiB,是 MongoDB 的限制,一个只增不减的评论数组迟早会撞上它。

  • 擅长:整个聚合一次读写,单文档更新是原子的;字段随品类变化也不用 migration。
  • 答不了:跨很多文档的临时 join;分片集群里不带 shard key 的查询会发到所有分片。
  • Examples:MongoDB、Amazon DocumentDB、CouchDB

Key-value

Key-value database 用 key-value pairs 存数据,也叫 key-value store,是最简单的 NoSQL 类型之一。

  • 擅长:按完整 key 的读写,延迟低,适合 session、幂等记录、计数器、缓存。
  • 答不了:按 value 里的字段查询。Redis 有 SCAN 可以遍历 key,但那是运维工具,不是查询路径;DynamoDB 可以用 GSI 和 filter,但如上所述 filter 不省容量。
  • Examples:Redis、Memcached、Amazon DynamoDB、Aerospike。DynamoDB 通常被归为 key-value 和 document 两类都支持。

Graph

Graph database 使用 graph 结构(nodes、edges、properties)表达与存储数据,edges 表示关系,多跳关联可以沿着边遍历,而不是一层层自 join。

  • 擅长:多跳关系查询。Use cases:fraud detection、recommendation engines、social networks、network mapping。
  • 答不了(或代价高):全图聚合统计;遍历经过连着几十万条边的超级节点时查询会爆。
  • 查询语言过去各家不同(Cypher、Gremlin、SPARQL 等);2024 年 4 月 ISO/IEC 39075:2024 GQL 作为图查询语言的国际标准发布。
  • Examples:Neo4j、ArangoDB、Amazon Neptune、JanusGraph

Time series

Time-series database 针对 time-stamped 数据做优化:按时间追加写入、按时间窗口查询、按保留策略整块删除旧数据。

  • 擅长:IoT data、metrics、application monitoring、金融行情的时间窗口聚合。
  • 答不了:频繁更新历史点、按非时间维度的随机查找。
  • Examples:InfluxDB、TimescaleDB。Apache Druid 更准确的定位是实时分析数据库,常用于时间序列的聚合查询。

Wide column

Wide column database 按 partition key 定位一组行,partition 内按 clustering key 排序,每行可以有不同的列。Cassandra 的写先进 commit log 和内存 memtable,再刷成不可变的 SSTable,写路径是追加,写吞吐是它的强项。

  • 擅长:高写入、按「分区 + 范围」读,例如某设备某天的事件。
  • 答不了:不带 partition key 的查询;partition 无限增长后读、修复、compaction 都变慢,要在 key 里加时间桶。
  • 代价:compaction 和 repair 的运维成本;一致性按请求可调,要自己选 ONE、QUORUM 等级别。
  • Examples:Bigtable、Apache Cassandra、ScyllaDB

Multi-model

Multi-model database 在一个后端里支持多种模型(document、graph、key-value 等),用同一套运维管理多种数据。

  • 擅长:一个团队需要两三种模型但不想维护两三套集群。
  • 代价:每种模型的能力和调优深度不一定比得上专门的数据库;「一套库装所有数据」也会让不同 workload 互相拖累。
  • Examples:ArangoDB、Azure Cosmos DB、Couchbase

常见翻车

翻车用户看到什么修法
先照关系库的表结构建,再想查询上线后每个新需求都要 Scan 或加 GSI,成本和延迟一起涨先写访问模式清单,每个查询对应一个 key 条件
热 key 超过单分区上限大促时爆款商品扣库存大量报限流错误写分片;或把计数放到专门的计数服务里
数组字段无限增长大客户的文档写入失败(超过 400 KB 或 16 MiB)子项拆成独立 item / 文档,按父 key 聚在一起
用 filter 代替 key 条件账单上的读容量远超返回的数据量把过滤条件设计进 SK 或 GSI
GSI 最终一致没被考虑刚改的状态在列表里短暂查不到详情页用基表强一致读;列表页接受延迟并说明
以为 NoSQL 没有事务所以在应用里拼并发时两个 item 一个改了一个没改用产品自带的事务(TransactWriteItems、MongoDB 多文档事务),或重新划定聚合边界

面试时这样回答

  1. 先列访问模式。 把要回答的查询逐条写出来,标出读写频率和一致性要求。
  2. 推出 key。 每个查询对应 PK + SK 条件或一个 GSI,说出 key 的具体格式,例如 CUSTOMER#c42 / ORDER#{created_at}#{order_id}。
  3. 说硬限制。 item 大小、单分区吞吐、分页大小,以及哪个实体会先撞上。
  4. 说热点和修法。 算一下最热 key 的峰值写入与单分区上限的比例,给出分片数量和读时合并的代价。
  5. 说它答不了什么。 主动说出一个没设计进 key 的查询,以及准备怎么处理(加 GSI、导到搜索或分析系统)。

相关章节:SQL 还是 NoSQL、SQL databases、ACID vs BASE、Database Sharding、Consistent Hashing、Indexes、设计 Key-Value Store。

一手证据

单表设计的 key 格式和库存分片数是本章的示例;文档支撑的是各项限制和机制。