# 数据库是系统的记忆

关于数据库与数据存储的速成课。20 个概念——Relational、SQL、Transactions、Document、Key-value、Wide-column、Graph、Time-series、Search、Vector、Object storage、ACID、BASE、CAP、Index、Replication、Sharding、Normalization、Polyglot persistence——每个都配有英文名称、一幅图示,以及一张带例子和代价的卡片。结尾附一页纸的速查表,收齐所有概念。

- **Author:** Fedor Chishchin
- **Level:** intro
- **Reading time:** ~35 分钟阅读
- **Published:** 2026-06-02
- **Source:** https://fedorthinks.com/zh/learn/databases-express

---

## 1. 数据库是用来做什么的

数据库是你系统的长期记忆。代码会被重写;而数据、以及你为它选定的那个形状,往往比周围的一切都活得更久。

### 数据比代码活得久

> 一栋房子几十年间会重新粉刷、换家具、甚至翻修屋顶——但第一天浇下的地基会留着,因为换掉它意味着要把整栋房子抬起来。

语言会变,框架会被换掉,UI 会被重画十遍。**数据和它的结构通常留着**,因为在一个装满真实记录的运行中系统上去改它们,既慢又险。所以数据库是第一件要认真想清楚的事,也是最不能仓促的事。

### 数据库回答两个问题

> 挑一个存东西的地方,要问两件事:这些东西是什么形状——是书架上的书,还是挂钩上奇形怪状的工具?——以及它们必须多安全——是一个保险库,还是一个顺手的抽屉?

每一种存储都由两件事定义。第一,它的**数据模型**:它如何塑造数据——刚性的表、灵活的文档、一张由链接构成的图。第二,它的**保证**:严格的正确性,还是用更松的承诺换来速度与可用性。几乎每一次数据库选型,都是对这两点的一个不同回答——而这门课其余的部分,就是那份菜单。

### Access pattern——按你怎么读和写来挑

> 一个文件柜、一个卡片目录、一条传送带,全都是「存储」——但你按自己将怎么取东西来挑,而不是按哪个看上去最新。

没有最好的数据库。一个对快速键查找堪称完美的存储,用在深层关系查询上就错了;一个为巨量写入而生的存储,用在严格的银行事务上就错了。对的问题是你的**访问模式**(access pattern):你读什么,你写什么,多频繁,以及当事情出错时它必须怎么表现。

### 地图:六组共 20 个概念

> 一张地铁线路图:先看到整条线路,再一站一站地坐过去。

这门课的概念分成六组:relational 核心、NoSQL 家族、专用存储、保证、扩展技术,以及如何把它们全部组合起来。

_Diagram: 六组概念——第 2–7 章依次讲它们。_

下面每个概念都得到同样的对待:它的**英文名称**(你去查文档时要搜的那个词)、一幅生活中的画面、一幅**图示**,以及一张**卡片**,字段有:_问题_、_思路_、_例子_、_代价_、_何时使用_ 和 _关联_。结尾有一页纸的速查表在等着你。

> **在选一个存储之前:** "我的数据是什么形状,我将怎样查询它,以及那些保证必须有多严格?"

> 代码容易改。数据不容易。挑这个存储,要像挑一个你将与之相处多年的决定——因为你真的会。

## 2. 默认之选

对大多数系统而言,默认就是 relational 数据库——而它配得上这个默认。从这里起步,只在有理由时才离开。

### Relational Database——表、行与关系

> 一张做对了的电子表格:每个工作表是一类东西(客户、订单),每一行是一条记录,而订单指回它所属的那个客户。

这就是**关系型数据库**(relational database)。它把数据存在带固定 **schema** 的**表**里,并通过引用把表连起来——一条订单行存着它所属客户的 id。这个引用叫做**外键**(foreign key)。schema 是一份关于形状的承诺:每条订单都有它该有的列,在进来的路上就被校验。**先付出结构,换取日后的安稳。**

_Diagram: 两张表:订单里存着 customer_id——一个指向它所属客户的外键。_

#### Relational Database (RDBMS)

- **问题:** 数据有结构、有关系,而你会对它提出很多问题。
- **思路:** 带固定 schema 的表;表与表通过外键相连。
- **例子:** Postgres、MySQL:表 `customers` 和 `orders`,每个订单都有一个 `customer_id`。
- **代价:** schema 要事先想清楚;在运行中的数据上改它很费事。
- **何时使用:** **几乎总是,作为起点。** 只在有理由时才离开。
- **关联:** 说 **SQL**,提供 **Transactions** 和 **ACID**。

### SQL——说出你要什么,而不是怎么拿到

> 你对图书管理员说「1950 年以后出版的、法国作家的每一本小说,按年份排序」——他自己想办法去找。你描述的是结果,而不是搜索过程。

**SQL(Structured Query Language,结构化查询语言)**是一种**声明式查询语言**:你说出想要的答案,数据库自己规划怎么取到它,包括把表连接起来(**JOIN**)。这个规划由**查询规划器**(query planner)制定。`SELECT ... JOIN ... WHERE ...` 能回答你在设计时从没预料到的问题——这是关系模型一项悄无声息的超能力。

_Diagram: 你写下要什么(WHAT);数据库的规划器决定怎么取(HOW)。_

#### SQL (查询语言)

- **问题:** 你需要向数据提问,而不必为每次搜索手写代码。
- **思路:** 描述结果;由数据库挑选执行计划(索引、连接、排序)。
- **例子:** `SELECT title FROM books JOIN authors ... WHERE country = 'FR' AND year > 1950 ORDER BY year`。
- **代价:** 复杂的查询要仔细读,还要检查数据库实际是怎么执行它们的。
- **何时使用:** 凡是有关系型数据库的地方——它是这里的母语。
- **关联:** **Relational Database** 的语言;靠 **Index** 变快。

### Transactions——要么全做,要么全不做

> 一笔转账是两次写入——这边减、那边加——但只有一个意图。要么两个都发生,要么都不发生;转到一半是一场灾难。

这就是**事务**(transaction):一组更改,要么完整地**提交**(commit),要么完整地**回滚**(rollback),与其他事务相互隔离,一旦确认就能扛过崩溃。这一整套承诺合称 **ACID**(第 5 章会详讲)。这就是为什么银行、订单、库存都放在 SQL 里——**当正确性比原始速度更重要时,你要的就是这份承诺。**

_Diagram: 一笔转账:扣款和入账是同一个事务。要么都 COMMIT,要么都 ROLLBACK。_

#### Transaction (全有或全无的单元)

- **问题:** 一个动作是好几次写入;中途崩溃就会留下一堆垃圾。
- **思路:** 把这些写入包在 `BEGIN … COMMIT` 里:全部保存,或者一个都不保存。
- **例子:** 一笔转账:`A − 100` 和 `B + 100` 在同一个事务里。
- **代价:** 损失一些速度,并且跨机器扩展更难。
- **何时使用:** 钱、订单、库存——任何容不得出错数字的地方。
- **关联:** 事务的各项属性就是 **ACID**。

### 为什么它是正确的默认选择

> 一件好用的通用工具:在任何单项活儿上都不是最快的,但在某个活儿明确要求别的工具之前,它就是你伸手去拿的那一件。

有结构的数据、真实的关系、临时起意的查询、强保证——关系型数据库这些都做得好,而且现代 Postgres 还能处理 JSON、全文搜索,甚至向量。**每个项目都从这里开始;** 只有当一个具体的痛点——规模、形状或访问模式——把你推出去时,才去找别的。

> **关系型的问题:** "我的数据是否有清晰的结构和关系,而我将以一些现在还无法完全预料的方式去查询它?"

> 从 Postgres 起步。问题不是「为什么要用 SQL」——而是你有没有一个真正的理由不用它。

## 3. 当一张表不是那个形状时

有时,刚性的表就是错误的形状。「NoSQL」其实是一个存储家族,每一个都朝不同的方向松动规则,去干不同的活儿。

### Document Store——每样东西一个 JSON 文档

> 一夹填好的表格,每张表格的字段都可以略有不同——没有一个中央模板强迫它们全都一致。

这就是**文档存储**(document store)。它把记录保存为灵活的、类 JSON 的**文档**,嵌套、**无 schema**(schema-less),所以同一个**集合**(collection)里的两条记录可以不一样。当形状各异或演变很快时非常合适,而一整件东西——一个商品、一份用户资料——就是一个文档,一次读取就能取回。

_Diagram: 同一个集合里的两个商品,字段不同;整个商品一次读取就回来了。_

#### Document Store (文档数据库)

- **问题:** 记录的形状各异或变化很快;表格碍事。
- **思路:** 每样东西是一个嵌套的 JSON 文档,没有共享的刚性 schema。
- **例子:** MongoDB:商品目录、用户资料。
- **代价:** 保证更少,跨文档查询比 SQL 更难。
- **何时使用:** 一样东西整个读取,而且不同记录有不同字段。
- **关联:** Postgres 也能处理文档(**JSONB**);常常遵循 **BASE** 规则。

### Key-Value Store——一张巨大的哈希表

> 一个衣帽寄存处——你递上一个号码牌,拿回的正是你的外套。不用找,不用问,立刻就有。

这就是**键值存储**(key-value store)——最简单的形状:**键进,值出**,快得惊人。非常适合缓存、会话、**rate-limit**(限流)计数器,以及任何凭已知 id 来取的东西。

_Diagram: 知道键,就能立刻拿到值。按值去查是做不到的。_

#### Key-Value Store (键 → 值)

- **问题:** 你需要凭一个已知的 id 非常快地取回数据。
- **思路:** 键 → 值,像一张巨大的哈希表。
- **例子:** Redis、DynamoDB:`session:42`、页面缓存、限流计数器。
- **代价:** 只能按键查找——无法对值做丰富的查询。
- **何时使用:** 缓存、会话、计数器——任何事先就知道 id 的场合。
- **关联:** 在 **Polyglot persistence** 里,主数据库旁边那个典型的「快速帮手」。

### Wide-Column Store——为海量写入而造

> 一座巨大的仓库,有无尽的相同过道,布局让一千辆叉车可以同时上货而不会互相碰撞。

这就是 **wide-column 存储**。它按**分区键**(partition key)把数据分散到许多台机器上,换来**海量的写入吞吐和规模**,代价是放弃丰富的查询和严格的一致性,好去吞下一股洪流般的数据。强大——但要运维好也需要更多工作。

_Diagram: 分区键把写入分散到许多台机器;行很宽,没有 join。_

#### Wide-Column Store (列族存储 (column-family store))

- **问题:** 写入多到一台 SQL 机器会被压垮。
- **思路:** 按分区键把数据分散到许多节点;并行写入。
- **例子:** Cassandra:事件日志、遥测数据、大规模信息流。
- **代价:** 没有丰富的查询,也没有严格的一致性;更难运维。
- **何时使用:** 写入如洪流,而查询事先已知且简单。
- **关联:** 本质上自带 **Sharding**;通常遵循 **BASE**。

### Graph Database——当链接才是重点

> 侦探的线索板——用线连起来的照片。价值不在任何一张照片里,而在谁和谁相连的那张网里。

这就是**图数据库**(graph database)。它把数据建模成**节点**和它们之间的**边**(edge),让关系查询——「喜欢这个的朋友的朋友」「这两个账号之间的路径」——变得自然而快速,而用 SQL 则需要痛苦的多层 join。**当连接比记录本身更重要时,就用它。**

_Diagram: “喜欢 X 的朋友的朋友”:只需顺着边走 Ann → Bob → Cara → Jazz club。_

#### Graph Database (图存储 (graph store))

- **问题:** 主要的问题都关于链接,而在 SQL 里它们会变成好几层的 JOIN。
- **思路:** 节点和边是一等数据;查询就是沿着边走。
- **例子:** Neo4j:社交图谱、推荐、欺诈检测。
- **代价:** 适用面窄;处理普通记录和报表很别扭。
- **何时使用:** 连接比记录更重要。
- **关联:** 当 JOIN 成了痛点时,可替代 **Relational Database**。

> **NoSQL 的问题:** "关系型的形状是否真的在和我作对——文档各不相同、纯粹的键查找、一股写入洪流,还是一张关系网?"

> NoSQL 不是「更新版的 SQL」。每种存储都放弃一项保证,去赢得某个具体的东西——要清楚你放弃的是哪一项。

## 4. 专用存储

在通用存储之外,还有为某一项工作而造的数据库,专门打造的引擎在那里远远胜过通用的。

### Time-Series Database——被时钟盖了戳的数据

> 心率监护仪的纸带——一条没有尽头的读数长带,每个读数都系着一个时刻,而你几乎总是在问「这两个时间之间发生了什么?」

这就是**时序数据库**(time-series database)。它为**按时间顺序到来、并按时间来查询的数据**而调优——指标、传感器读数、价格。它能摄入巨大的数据流,并快速回答「过去一周每分钟的平均值」,还内置对旧数据的过期清理(**retention**,保留策略)。

_Diagram: 数据点按时间顺序到来;一次查询就是从 t1 到 t2 的一个窗口;旧数据按保留策略过期。_

#### Time-Series Database (TSDB)

- **问题:** 海量的读数流,而每个问题都是「这段时间里发生了什么」。
- **思路:** 按时间存储并建立索引;快速聚合时间窗口。
- **例子:** InfluxDB、TimescaleDB:服务器指标、传感器、价格。
- **代价:** 多一个要运维的存储;不适合普通的业务数据。
- **何时使用:** 所有东西都带时间戳,而时间是主轴。
- **关联:** TimescaleDB 是 Postgres 的扩展——见 **Postgres first**。

### Search Engine——全文与相关性

> 一个能容忍拼写错误的搜索框,把最佳匹配排在最前,你输入「run」它也能找到「running」——像一个懂你意思的图书管理员。

这就是**搜索引擎**(search engine)。它建立一个**倒排索引**(inverted index)——「词 → 文档」——来做**快速的全文搜索,带排序、模糊匹配和过滤**:这些是 SQL 里的 `LIKE '%word%'` 做得又慢又差的事。它通常和你的主数据库并排运行,保持同步,支撑搜索栏和日志探查。

_Diagram: 数据从 Postgres 复制进搜索索引;一个带拼写错误的查询返回一份排好序的列表。_

#### Search Engine (全文搜索)

- **问题:** `LIKE '%word%'` 很慢,不容忍拼写错误,也不排序。
- **思路:** 词的倒排索引,外加一个相关性分数。
- **例子:** Elasticsearch、OpenSearch:网站的搜索栏、日志探查。
- **代价:** 必须与主数据库保持同步。
- **何时使用:** 用户按文字搜索,并期待「聪明」的搜索。
- **关联:** **不是事实来源**——只是它上面一面快速的镜片;属于 **Polyglot persistence** 的一部分。

### Vector Database——按含义搜索

> 不再匹配确切的词,而是问「给我找些感觉和这个一样的东西」——它按相似度返回邻居,而不是按关键词。

这就是**向量数据库**(vector database)。它存储 **embedding**——含义的数字指纹——并找出**最近邻**(nearest neighbours),所以你可以按相似度而不是确切文字来搜索。这是**语义搜索**和 **RAG** 背后的引擎,AI 应用靠它取回最相关的片段来作答。

_Diagram: 文本变成向量;最近的点就是含义上最接近的文本。_

#### Vector Database (向量存储 (vector store))

- **问题:** 你需要含义相近的东西,但字面上可能对不上。
- **思路:** 存储 embedding,搜索最近邻。
- **例子:** pgvector、Pinecone:查询「refund policy」能找到「returns」和「money back」。
- **代价:** 需要一个 embedding 模型,还要与源数据同步。
- **何时使用:** 语义搜索和 RAG——AI 时代新的必备存储。
- **关联:** pgvector 是 Postgres 的扩展;是数据的一份副本,和 **Search Engine** 一样。

### Object Storage——大文件去的地方

> 一间自助仓储间——放的不是索引卡片,而是家具:体积庞大的东西,凭标签保管,整件搬出来。

这就是**对象存储**(object storage)。它不是数据库,但**大块的二进制数据**——图片、视频、备份、文档——就放在这里,通过 **bucket**(存储桶)里的**键**来寻址,并能大规模地廉价提供服务。这个模式几乎是通用的:文件放进对象存储,在数据库里留一行指向它。**记录在数据库里,载荷在 bucket 里。**

_Diagram: Postgres 的那一行只存键;文件本身放在一个 S3 bucket 里。_

#### Object Storage (S3、blob storage)

- **问题:** 大文件放进数据库会让它臃肿,而且提供服务成本高。
- **思路:** 文件按键放进 bucket;数据库只留一行带这个键的记录。
- **例子:** S3 及其同类:照片、视频、备份、文档。
- **代价:** 不是数据库:不能对内容做查询,只有「按键 put/get」。
- **何时使用:** 任何大文件。
- **关联:** 形状像 **Key-Value Store**,只不过装的是庞大的东西。

> **专用存储的问题:** "这是不是一项活儿——时间、文本、相似度、大文件——专门打造的存储在这里能碾压通用存储?"

> 别把你的主数据库硬掰成搜索引擎或文件服务器。把专门的活儿交给为它而造的存储。

## 5. 那些保证

数据库最深的选择不是它的形状——而是出事时它承诺什么。一小碗字母汤,就给这个取舍命了名。

### ACID——悲观、正确、谨慎

> 一位银行柜员,每笔记录都复核两遍,账不平就不往下走——更慢,但从不出错。

**ACID** 是关系型数据库对事务做出的**严格承诺**:每个事务要么让数据正确而完整,要么根本不发生。四个字母:**Atomicity**(原子性,全有或全无)、**Consistency**(一致性,数据的规则始终成立)、**Isolation**(隔离性,并发的事务互不干扰)、**Durability**(持久性,已确认的内容能扛过崩溃)。**对钱、订单,以及任何容不得出错数字的地方,这就是你要的取舍。**

_Diagram: ACID 的四个字母,以及每一个承诺什么。_

#### ACID (Atomicity, Consistency, Isolation, Durability)

- **承诺:** 每个事务要么完全正确,要么根本没发生。
- **例子:** 银行转账、下订单、从库存里扣减货物。
- **代价:** 损失一些速度,并且跨机器扩展更难。
- **何时使用:** 出错的数字不可接受。
- **关联:** **Transaction** 的各项属性;相反的立场是 **BASE**。

### BASE——乐观、可用、最终正确

> 一家繁忙的商店,即使在更新价签的同时也让大家继续买——数字过一会儿就会跟上,这没关系。

许多 NoSQL 系统采取相反的立场:**BASE**(Basically Available、Soft state、Eventually consistent)。它们保持**快速和可用**,并接受数据的各个副本在趋于一致之前可能短暂地不一致。这叫做**最终一致性**(eventual consistency)。

_Diagram: 写入立刻落在节点 A 上,稍后才到 B 和 C。短暂的不一致,然后大家达成一致。_

#### BASE (Basically Available, Soft state, Eventually consistent)

- **承诺:** 系统总有响应;各副本「最终」会收敛。
- **例子:** 点赞数、信息流、浏览计数器。
- **代价:** 你可能会短暂地看到一个过时的值。
- **何时使用:** 可用性比即时的精确更重要,而且过时一秒钟没有任何损失。
- **关联:** **ACID** 的对立面;**CAP** 里选「A」的那一边。

### CAP Theorem——网络断开时选哪边

> 一支被断掉的电话线隔开的团队:每一半都可以各自继续工作(但会渐渐分歧),或者放下工具等重新连上(但就停工了)。两样不可兼得。

**CAP 定理**说:当一次**网络分区**(network partition)把你数据库的机器隔开时,你必须在 **Consistency**(拒绝写入,保持正确)和 **Availability**(继续服务,冒着不一致的风险)之间选一个——分区期间你不能两者都要。每个分布式数据库都要做这个决定。**知道你的存储选了哪个,就知道它在最糟糕的日子里会怎么表现。**

_Diagram: 网络断了:要么 CP(拒绝,但保持正确),要么 AP(继续工作,但可能分歧)。_

#### CAP Theorem (Consistency, Availability, Partition tolerance)

- **内容:** 网络分区期间,你不能既正确又可用。
- **例子:** 两个节点失去联系:拒绝这次写入(CP),或者在两边都接受、事后再协调(AP)。
- **代价:** CP——停机;AP——数据过时或冲突。
- **何时考虑:** 一旦你的数据存在于不止一台机器上。
- **关联:** 由 **Replication** 和 **Sharding** 引出;AP 那一侧是 **BASE**。

> **保证的问题:** "当两个副本可能不一致时,我需要的是正确性——哪怕意味着停机;还是在线——哪怕意味着数据过时?"

> 钱用 ACID,点赞用 BASE。你需要哪种保证,取决于短暂出错的代价有多大。

## 6. 在规模之下又快又可靠

一小撮技术,能把数据库从一只脆弱的单箱,变成又快又能扛的东西。你会把它们全都遇上。

### Index——数据的书末索引

> 要在一本 900 页的书里找一个词,你不会逐页去读——你翻到书末的索引,直接跳过去。

**索引**(index)是一个旁路结构,把**全表扫描**(full table scan)变成直接查找——它是**提升读取速度最大的杠杆。** 代价是:每个索引都占存储空间,并拖慢写入(每次插入也得更新它)。

_Diagram: 没有索引时,数据库读每一行;有了索引,它沿着一个有序列表直接跳到正确的那一行。_

#### Index (B-tree index)

- **问题:** 一次查询要读整张表,数据越多就越慢。
- **思路:** 在某一列上建一个有序的旁路结构 → 直接查到那一行。
- **例子:** 在 `email` 上建索引:不用扫描整张表就能找到一个用户。
- **代价:** 磁盘空间,而且每次写入都变慢。
- **何时使用:** 建在你真正用来过滤和排序的列上——不要为了「以防万一」给每一列都建。
- **关联:** 加速 **SQL**;搜索引擎是针对文本的一种特殊索引。

### Replication——留下副本,扛住故障

> 一份文件放在三栋楼里的三个保险柜中——一个烧毁了,其他的还留着它,而且三个人可以同时读。

**复制**(replication)把**数据的副本保存在多台机器上**。所有写入都进入**主节点**(leader),**从节点(副本)**(follower, replica)接收一份拷贝并提供读取。如果主节点挂了,一个从节点接管(**failover**,故障切换)。麻烦在于**复制延迟**(replication lag):从节点可能落后片刻,所以你刚写入的值也许还没出现在某个副本上。

_Diagram: 主节点接收写入并发出副本;从节点提供读取,并在主节点倒下时接管。_

#### Replication (主从 (leader–follower))

- **问题:** 一台机器既是单点故障,也是读取能力的唯一上限。
- **思路:** 同样的数据放在多台机器上:主节点写,从节点读。
- **例子:** 带两个只读副本的 Postgres;主节点倒下后,一个副本成为主节点。
- **代价:** 延迟:刚写入的数据可能还没到达副本。
- **何时使用:** 你需要可靠性,或者读取量超过一台机器能承受的。
- **关联:** 复制的是**相同的**数据;**Sharding** 拆分的是**不同的**数据。两者都会引出 **CAP**。

### Sharding——一只箱子装不下时就拆分

> 一个塞爆了的文件柜,变成一个柜子放 A–M、另一个柜子放 N–Z。空间多了——但现在你得知道某个名字放在哪个柜子里。

当数据或写入负载超出单台机器时,**分片**(sharding)把**数据拆分到许多台机器上**——用户 A–M 放这边,N–Z 放那边。每一部分是一个 **shard**,你用来拆分的那个字段是**分片键**(shard key)。它带来近乎无限的规模,但**分片键是一个艰难、近乎永久的选择:** 选错了,你就会遇到**热点**(hotspot),或者必须访问每一个 shard 的查询。

_Diagram: 路由器读取键:“Ann”去 shard 1,“Zoe”去 shard 2。每个 shard 存的是不同的数据。_

#### Sharding (水平分区 (horizontal partitioning))

- **问题:** 数据或写入流一台机器装不下。
- **思路:** 按一个键把数据拆分到许多台机器上。
- **例子:** 用户 A–M 在 shard 1,N–Z 在 shard 2。
- **代价:** 这个键近乎永久;键选得不好 → 热点,或者要跨每个 shard 的查询。
- **何时使用:** 最后一步:在索引和副本之后。
- **关联:** 内置于 **Wide-Column Store**;与 **Replication** 一起会引出 **CAP**。

### Normalization / Denormalization——一个事实放一处,还是为了速度留副本

> 一份人人都引用的地址总表(没有矛盾),对比把地址的一份拷贝订在每一张订单上(读起来更快,但现在有一堆副本要保持诚实)。

**规范化**(normalization)让每个事实只存在于恰好一个地方——干净,没有矛盾,是关系型的默认做法。**反规范化**(denormalization)则故意复制数据,让读取更快、需要的 join 更少。两者都成立;规矩是要**有意识地**去做,并且始终清楚哪一份是**事实来源**,哪一份只是快速的副本。(和缓存是同一个教训。)

_Diagram: 左:每张订单都指向同一个地址。右:地址被复制进每张订单。_

#### Normalization / Denormalization (一处存放 vs 快速副本)

- **问题:** 同一个事实放在多处会渐渐对不上;但 join 会拖慢读取。
- **思路:** 默认规范化;在读取更重要的地方有意识地反规范化。
- **例子:** 地址放在一张 `addresses` 表里,对比每张订单里各存一份拷贝。
- **代价:** 规范化——更多 JOIN;反规范化——要保持同步的副本。
- **何时使用:** 当可测量的慢读取要求时,再反规范化。
- **关联:** 和 **Polyglot persistence** 是同一个「事实来源 + 快速副本」的思想。

> **规模的问题:** "我真正的问题是读取速度(索引、副本)、存活(复制),还是单纯的体量(分片)——而我解决的是不是我真正有的那一个?"

> 先加索引,再加服务器;先加副本,再分片。最便宜的扩展,是你不必去运维的那一种。

## 7. 选得好

你不会只挑一个数据库,然后把一切都硬塞进去。真实的系统会用上几个,每个干它最擅长的活儿。

### Polyglot Persistence——有意地使用多个存储

> 厨房里有冰箱、冷冻柜和储物柜——而不是一只箱子被逼着同时干这三件事。各自存放适合它的东西。

这就是**多语言持久化**(polyglot persistence)模式。成熟的系统通常会用好几个存储:Postgres 作事实来源,Redis 做缓存和会话,一个搜索索引服务搜索栏,也许再加一个向量存储给 AI 功能用,以及对象存储放文件。每个存储做它擅长的事。要守的纪律是始终清楚**哪一个握有真相**,其余的都是保持同步的快速副本。

_Diagram: 五个存储,各管各的活儿。真相存在 Postgres 里;其余都是副本。_

#### Polyglot Persistence (一个系统里的多个存储)

- **问题:** 没有哪个数据库能样样第一。
- **思路:** 每项活儿有自己的存储;有一个明确的事实来源。
- **例子:** Postgres + Redis + 一个搜索索引 + 一个向量存储 + S3。
- **代价:** 每个存储都要运维、备份,并保持一致。
- **何时使用:** 当一个真实的痛点需要另一个存储时——而不是「以防万一」。
- **关联:** 从 **Postgres first** 出发;把这门课里几乎所有存储串在一起。

### Postgres First——从 Postgres 起步,有意地逐步超越它

> 一把瑞士军刀,在你需要整套工具箱之前,能应付惊人多的事情。

这就是 **Postgres first** 的做法。现代 Postgres 的能力令人惊讶——relational 核心、JSON 文档、全文搜索、向量,甚至一个够用的任务队列。**大多数产品应该只从 Postgres 开始**,只有当一个真实的、可度量的痛点需要时,才加一个专用存储。每多一个数据库,就多一样要运维、备份和保持一致的东西。最便宜的存储,就是你没加的那个。

_Diagram: 一个 Postgres 就涵盖表、文档、搜索、向量和队列。只有出现真实痛点才加新存储。_

#### Postgres First (从 Postgres 开始)

- **问题:** 团队在痛点出现之前就加数据库。
- **思路:** 先用一个 Postgres,直到某个可度量的问题要求更多。
- **例子:** 用 JSONB 代替单独的文档存储,用 pgvector 代替单独的向量数据库。
- **代价:** 在非常大的规模下,专用引擎仍然会赢。
- **何时使用:** 几乎任何产品的起步阶段。
- **关联:** 迈向 **Polyglot persistence** 的第一步——等「暂时」结束之后。

### 这一切是怎么连起来的

> 走了很久的路之后的街区地图:现在你看得出哪条街通向哪里。

这门课里的概念不是孤立存在的。关系型数据库是带 **ACID** 保证的**事实来源**;**索引**加速它的读取;搜索引擎和向量数据库是从它喂出来的**副本**;对象存储存放它的行所指向的文件。当一台机器不够用时,**复制和分片**就登场了——随之而来的是 **CAP** 的抉择。NoSQL 家族常常选 **BASE**——ACID 的反面。

_Diagram: 这门课各概念之间的主要联系:事实来源居于正中。_

#### 选数据库之前

- **数据是什么形状**——表、文档、键值,还是图?
- **我将怎样查询它**——按 id、按关系、按文本、按相似度,还是按时间?
- **我需要什么保证**——严格的 ACID,还是最终一致就够了?
- **读写的比例如何**,以及大致的规模?
- **Postgres 是不是已经能做这件事**,在我再加一个存储之前?
- **哪个存储握有事实来源**,哪些只是副本?

#### 你选错了的嗅探信号

- 你用 `LIKE '%...%'` 做搜索,而不是用搜索引擎。
- 一个 **键值存储**,而你却老是需要按值去查询。
- 两个人维护的产品用了**五个数据库**。
- 你选 **MongoDB 是为了避开 schema**,结果还是手工重建了校验。
- **没有明确的事实来源**——两个存储都自称是权威。

#### 你选对了的迹象

- 你**最常跑的那个查询**,正是这个存储最快的。
- **保证与利害相匹配**——钱要严格,点赞可放松。
- 你能说出每个重要事实的**事实来源**。
- 你加每个存储都是为了解决一个**真实、切身的痛点**,而不是假想的。
- 你能在几分钟内向新工程师**讲清楚数据模型**。

## 速查表:全部 20 个概念

### 关系型核心

- **Relational Database** (RDBMS): 带固定 schema 的表,由外键连接。默认之选。
- **SQL** (查询语言): 描述你要什么;由数据库决定怎么取。
- **Transaction** (全有或全无): 多次写入作为一个单元:要么全部 COMMIT,要么全部 ROLLBACK。

### NoSQL 家族

- **Document Store** (文档数据库): 灵活的 JSON 文档;一样东西整个读取。
- **Key-Value Store** (键 → 值): 键 → 值,立刻拿到。缓存、会话、计数器。
- **Wide-Column Store** (列族存储): 把数据分散到许多机器上,吞下写入洪流。
- **Graph Database** (图存储): 节点和边;关系查询,没有痛苦的 JOIN。

### 专用存储

- **Time-Series Database** (TSDB): 按时间的读数;按窗口查询,旧数据过期。
- **Search Engine** (全文搜索): 带排序和拼写容错的全文搜索。是副本,不是真相。
- **Vector Database** (向量存储): Embedding 与按含义找最近邻的搜索。RAG 的基础。
- **Object Storage** (S3): 大文件按键放进 bucket;数据库只留一个指针。

### 保证

- **ACID** (严格的承诺): 一个事务要么完全正确,要么没发生。用于钱。
- **BASE** (最终一致性): 始终可用,副本最终收敛。用于点赞。
- **CAP Theorem** (在 C 与 A 间选一个): 网络分区期间——要么正确性,要么可用性。

### 规模

- **Index** (B-tree index): 一个让读取变快的指针;代价是空间和更慢的写入。
- **Replication** (主从): 相同的数据放在多台机器上:可靠性与读取。存在延迟。
- **Sharding** (水平分区): 按键把不同的数据放在不同的机器上。这个键近乎永久。
- **Normalization / Denormalization** (一处存放 vs 副本): 一个事实放在一处——或者为了速度有意留下副本。

### 选择

- **Polyglot Persistence** (多个存储): 每项活儿有自己的存储,只有一个事实来源。
- **Postgres First** (从 Postgres 开始): 先用一个 Postgres,直到真实的痛点需要更多。

> 最好的数据库决定,通常是「暂时用 Postgres」——外加恰好知道「暂时」何时结束的智慧。

速成课结束 · 7 章 · 20 个概念

接下来是深入:Martin Kleppmann 的 _Designing Data-Intensive Applications_ 是唯一值得读的那本书,再配上 Postgres 文档和你所用存储自己的手册。但在跳进深水区之前——先设想一下你的数据将如何被读取、被写入、被信任。数据库是你的系统记忆的地方;在选择用什么来构建它之前,先选择它如何记忆。
