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,需要回答五个查询:
- 按订单号取订单详情和它的所有明细行。
- 某个客户最近的 20 单,按时间倒序。
- 客服按状态查「今天所有待发货的订单」。
- 按客户 ID 取客户资料。
- 大促时,一个爆款商品的库存每秒被扣几千次。
DynamoDB 的建模文档写得很直白:关系库可以先规范化建模、不考虑访问模式;DynamoDB 在你知道它要回答哪些问题之前,不应该开始设计 schema。
用单表设计回答五个查询
把订单、明细、客户放在同一张表里,用通用的 PK、SK 两个属性:
| 实体 | PK | SK | 其他属性 |
|---|---|---|---|
| 客户资料 | CUSTOMER#c42 | PROFILE | name、tier |
| 客户的订单索引 | CUSTOMER#c42 | ORDER#2026-09-22T10:31:00#o789 | total、status |
| 订单头 | ORDER#o789 | HEADER | customer_id、status |
| 订单明细 | ORDER#o789 | ITEM#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 多文档事务),或重新划定聚合边界 |
面试时这样回答
- 先列访问模式。 把要回答的查询逐条写出来,标出读写频率和一致性要求。
- 推出 key。 每个查询对应 PK + SK 条件或一个 GSI,说出 key 的具体格式,例如
CUSTOMER#c42/ORDER#{created_at}#{order_id}。 - 说硬限制。 item 大小、单分区吞吐、分页大小,以及哪个实体会先撞上。
- 说热点和修法。 算一下最热 key 的峰值写入与单分区上限的比例,给出分片数量和读时合并的代价。
- 说它答不了什么。 主动说出一个没设计进 key 的查询,以及准备怎么处理(加 GSI、导到搜索或分析系统)。
相关章节:SQL 还是 NoSQL、SQL databases、ACID vs BASE、Database Sharding、Consistent Hashing、Indexes、设计 Key-Value Store。
一手证据
- Amazon DynamoDB:NoSQL design(先知道访问模式再设计)
- Amazon DynamoDB:Partition key design(每分区 3,000 RCU / 1,000 WCU)
- Amazon DynamoDB:Constraints(400 KB item)
- Amazon DynamoDB:Query pagination(1 MB)
- Amazon DynamoDB:Filter expressions for Query
- Amazon DynamoDB:Global secondary indexes
- Amazon DynamoDB:Overloading GSIs
- MongoDB:Embedded data
- MongoDB:Referenced data
- MongoDB:Limits and Thresholds
- Apache Cassandra:Storage engine
- GQL Standards(ISO/IEC 39075:2024 发布说明)
单表设计的 key 格式和库存分片数是本章的示例;文档支撑的是各项限制和机制。