fedorthinks

速成课 · No. 06

语言和框架来了又走。而你存下的数据、以及你用来存它的那个形状,却能比它们全都活得更久——所以挑一个数据库,就是在选择你的系统如何记忆,以及要改变它的想法有多难。

20 个概念 · 每个都配图示 · 结尾附速查表

§ 01

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

数据比代码活得久

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

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

数据库回答两个问题

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

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

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

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

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

地图:六组共 20 个概念

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

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

Relational (SQL)Relational modelSQLTransactionsNoSQL familyDocumentKey-valueWide-columnGraphSpecialisedTime-seriesSearch engineVector DBObject storageGuaranteesACIDBASECAPScalingIndexReplicationShardingNormalisationChoosingPolyglot persistencePostgres first
六组概念——第 2–7 章依次讲它们。

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

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

§ 02

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

Relational Database——表、行与关系

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

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

customersidnameemail1Annann@mail.com2Bobbob@mail.comordersidcustomer_idtotal10140.0011115.5012299.00foreign keyschema = fixed columns, checked on the way inorders point back to the customer they belong to
两张表:订单里存着 customer_id——一个指向它所属客户的外键。
StoreRelational 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 ... 能回答你在设计时从没预料到的问题——这是关系模型一项悄无声息的超能力。

you write WHATSELECT title, yearFROM books JOIN authorsWHERE country = 'FR' AND year > 1950ORDER BY yearQuery plannerdecides HOWResultrowsuse indexjoin tablessort by yearthe plan is thedatabase's job
你写下要什么(WHAT);数据库的规划器决定怎么取(HOW)。
LanguageSQL· 查询语言
问题
你需要向数据提问,而不必为每次搜索手写代码。
思路

描述结果;由数据库挑选执行计划(索引、连接、排序)。

例子

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 里——当正确性比原始速度更重要时,你要的就是这份承诺。

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

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

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

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

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

§ 03

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

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

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

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

collection: productsdocument 1{ "id": 1, "name": "Mug", "price": 12, "tags": ["kitchen"] }document 2{ "id": 2, "name": "T-shirt", "sizes": ["S", "M", "L"], "color": "blue" }different fields, same collection — no central templateone read returns the whole object
同一个集合里的两个商品,字段不同;整个商品一次读取就回来了。
StoreDocument Store· 文档数据库
问题
记录的形状各异或变化很快;表格碍事。
思路
每样东西是一个嵌套的 JSON 文档,没有共享的刚性 schema。
例子
MongoDB:商品目录、用户资料。
代价
保证更少,跨文档查询比 SQL 更难。
何时使用
一样东西整个读取,而且不同记录有不同字段。
关联
Postgres 也能处理文档(JSONB);常常遵循 BASE 规则。

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

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

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

GET session:42by known keykey → valuesession:42{ user: 7, cart: 3 }rate:ip:1.2.3.417cache:home<html>…</html>instant, no search✗ no rich queries: “all sessions where cart > 2”
知道键,就能立刻拿到值。按值去查是做不到的。
StoreKey-Value Store· 键 → 值
问题
你需要凭一个已知的 id 非常快地取回数据。
思路
键 → 值,像一张巨大的哈希表。
例子
Redis、DynamoDB:session:42、页面缓存、限流计数器。
代价
只能按键查找——无法对值做丰富的查询。
何时使用
缓存、会话、计数器——任何事先就知道 id 的场合。
关联
在 Polyglot persistence 里,主数据库旁边那个典型的「快速帮手」。

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

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

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

app 1writesapp 2writesapp 3writeshash(key)partition keyNode 1user:7 → t1 · t2 · t3 · t4 · …Node 2user:10 → t1 · t2 · t3 · t4 · …Node 3user:13 → t1 · t2 · t3 · t4 · …Node 4user:16 → t1 · t2 · t3 · t4 · …writes spread across many machines · wide rows · no joins
分区键把写入分散到许多台机器;行很宽,没有 join。
StoreWide-Column Store· 列族存储 (column-family store)
问题
写入多到一台 SQL 机器会被压垮。
思路
按分区键把数据分散到许多节点;并行写入。
例子
Cassandra:事件日志、遥测数据、大规模信息流。
代价
没有丰富的查询,也没有严格的一致性;更难运维。
何时使用
写入如洪流,而查询事先已知且简单。
关联
本质上自带 Sharding;通常遵循 BASE。

Graph Database——当链接才是重点

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

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

FRIENDFRIENDFRIENDWORKS_WITHLIKESAnnBobDanCaraJazz clubplace“friends of friends who like X” = follow the edges, no joins
“喜欢 X 的朋友的朋友”:只需顺着边走 Ann → Bob → Cara → Jazz club。
StoreGraph Database· 图存储 (graph store)
问题
主要的问题都关于链接,而在 SQL 里它们会变成好几层的 JOIN。
思路
节点和边是一等数据;查询就是沿着边走。
例子
Neo4j:社交图谱、推荐、欺诈检测。
代价
适用面窄;处理普通记录和报表很别扭。
何时使用
连接比记录更重要。
关联
当 JOIN 成了痛点时,可替代 Relational Database。

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

§ 04

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

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

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

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

expired(retention)WHERE time BETWEEN t1 AND t2t1t2time →readings arrive in time order and are asked about by time
数据点按时间顺序到来;一次查询就是从 t1 到 t2 的一个窗口;旧数据按保留策略过期。
StoreTime-Series Database· TSDB
问题
海量的读数流,而每个问题都是「这段时间里发生了什么」。
思路
按时间存储并建立索引;快速聚合时间窗口。
例子
InfluxDB、TimescaleDB:服务器指标、传感器、价格。
代价
多一个要运维的存储;不适合普通的业务数据。
何时使用
所有东西都带时间戳,而时间是主轴。
关联
TimescaleDB 是 Postgres 的扩展——见 Postgres first。

Search Engine——全文与相关性

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

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

Postgressource of truthSearch indexElasticsearch“runing shoe”typo + stemmingsyncqueryinverted indexrun → doc 3, doc 7shoe → doc 7, doc 9red → doc 3ranked results1. doc 7 · 0.922. doc 3 · 0.613. doc 9 · 0.40a fast lens onto the data — not the source of truth
数据从 Postgres 复制进搜索索引;一个带拼写错误的查询返回一份排好序的列表。
StoreSearch Engine· 全文搜索
问题
LIKE '%word%' 很慢,不容忍拼写错误,也不排序。
思路
词的倒排索引,外加一个相关性分数。
例子
Elasticsearch、OpenSearch:网站的搜索栏、日志探查。
代价
必须与主数据库保持同步。
何时使用
用户按文字搜索,并期待「聪明」的搜索。
关联

不是事实来源——只是它上面一面快速的镜片;属于 Polyglot persistence 的一部分。

Vector Database——按含义搜索

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

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

“refund policy”query textEmbedding modeltext → numbers[0.12, −0.83, …]vectorrefunds FAQreturnsmoney backpricingcareersoffice mapAPI docsnearest neighbours = closest in meaning
文本变成向量;最近的点就是含义上最接近的文本。
StoreVector Database· 向量存储 (vector store)
问题
你需要含义相近的东西,但字面上可能对不上。
思路
存储 embedding,搜索最近邻。
例子

pgvector、Pinecone:查询「refund policy」能找到「returns」和「money back」。

代价
需要一个 embedding 模型,还要与源数据同步。
何时使用
语义搜索和 RAG——AI 时代新的必备存储。
关联
pgvector 是 Postgres 的扩展;是数据的一份副本,和 Search Engine 一样。

Object Storage——大文件去的地方

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

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

photos (Postgres)iduser_ids3_key17photos/7/a1.jpg29videos/9/b2.mp4S3 bucketphotos/7/a1.jpg4.2 MB · imagevideos/9/b2.mp4310 MB · videobackups/db.tar12 GB · backuprecords in the DBpayloads in the bucket
Postgres 的那一行只存键;文件本身放在一个 S3 bucket 里。
StoreObject Storage· S3、blob storage
问题
大文件放进数据库会让它臃肿,而且提供服务成本高。
思路
文件按键放进 bucket;数据库只留一行带这个键的记录。
例子
S3 及其同类:照片、视频、备份、文档。
代价
不是数据库:不能对内容做查询,只有「按键 put/get」。
何时使用
任何大文件。
关联
形状像 Key-Value Store,只不过装的是庞大的东西。

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

§ 05

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

ACID——悲观、正确、谨慎

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

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

AAtomicityall or nothingCConsistencyrules always holdIIsolationno cross-talkDDurabilitysurvives a crashthe strict promise — for money, orders, inventory
ACID 的四个字母,以及每一个承诺什么。
GuaranteeACID· Atomicity, Consistency, Isolation, Durability
承诺
每个事务要么完全正确,要么根本没发生。
例子
银行转账、下订单、从库存里扣减货物。
代价
损失一些速度,并且跨机器扩展更难。
何时使用
出错的数字不可接受。
关联
Transaction 的各项属性;相反的立场是 BASE。

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

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

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

copies briefly disagreeNode Alikes 10likes 11Node Blikes 10likes 11Node Clikes 10likes 11Convergedeventuallyfast and available now, consistent a moment later
写入立刻落在节点 A 上,稍后才到 B 和 C。短暂的不一致,然后大家达成一致。
GuaranteeBASE· Basically Available, Soft state, Eventually consistent
承诺
系统总有响应;各副本「最终」会收敛。
例子
点赞数、信息流、浏览计数器。
代价
你可能会短暂地看到一个过时的值。
何时使用
可用性比即时的精确更重要,而且过时一秒钟没有任何损失。
关联
ACID 的对立面;CAP 里选「A」的那一边。

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

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

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

Node 1has dataNode 2has data✗partitionthe network split — pick one:CP — Consistencyrefuse writes, stay correctAP — Availabilitykeep serving, may divergecost: downtimecost: stale or conflicting data
网络断了:要么 CP(拒绝,但保持正确),要么 AP(继续工作,但可能分歧)。
TheoremCAP Theorem· Consistency, Availability, Partition tolerance
内容
网络分区期间,你不能既正确又可用。
例子
两个节点失去联系:拒绝这次写入(CP),或者在两边都接受、事后再协调(AP)。
代价
CP——停机;AP——数据过时或冲突。
何时考虑
一旦你的数据存在于不止一台机器上。
关联
由 Replication 和 Sharding 引出;AP 那一侧是 BASE。

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

§ 06

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

Index——数据的书末索引

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

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

WITHOUT INDEXreadallWITH INDEXann@…bob@…cara@…dan@…eve@…sorted lookuptable rowsfast reads · but costs space and slows every write
没有索引时,数据库读每一行;有了索引,它沿着一个有序列表直接跳到正确的那一行。
TechniqueIndex· B-tree index
问题
一次查询要读整张表,数据越多就越慢。
思路
在某一列上建一个有序的旁路结构 → 直接查到那一行。
例子
在 email 上建索引:不用扫描整张表就能找到一个用户。
代价
磁盘空间,而且每次写入都变慢。
何时使用
建在你真正用来过滤和排序的列上——不要为了「以防万一」给每一列都建。
关联
加速 SQL;搜索引擎是针对文本的一种特殊索引。

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

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

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

AppwritesLeaderall writesFollower 1serves readsFollower 2serves readsFollower 3serves readscopylag: a moment behindleader dies → a follower takes oversame data, many machines: survive failure, spread reads
主节点接收写入并发出副本;从节点提供读取,并在主节点倒下时接管。
TechniqueReplication· 主从 (leader–follower)
问题
一台机器既是单点故障,也是读取能力的唯一上限。
思路
同样的数据放在多台机器上:主节点写,从节点读。
例子
带两个只读副本的 Postgres;主节点倒下后,一个副本成为主节点。
代价
延迟:刚写入的数据可能还没到达副本。
何时使用
你需要可靠性,或者读取量超过一台机器能承受的。
关联

复制的是相同的数据;Sharding 拆分的是不同的数据。两者都会引出 CAP。

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

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

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

AppRoutershard key: nameShard 1users A–MShard 2users N–Z“Ann”“Zoe”different data on eachbad key → hotspots, or queries that hit every shard
路由器读取键:“Ann”去 shard 1,“Zoe”去 shard 2。每个 shard 存的是不同的数据。
TechniqueSharding· 水平分区 (horizontal partitioning)
问题
数据或写入流一台机器装不下。
思路
按一个键把数据拆分到许多台机器上。
例子
用户 A–M 在 shard 1,N–Z 在 shard 2。
代价
这个键近乎永久;键选得不好 → 热点,或者要跨每个 shard 的查询。
何时使用
最后一步:在索引和副本之后。
关联
内置于 Wide-Column Store;与 Replication 一起会引出 CAP。

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

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

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

NORMALISEDorder 10order 11order 12address #51 Main StDENORMALISEDorder 10 · 1 Main Storder 11 · 1 Main Storder 12 · 1 Main Steach fact in one place · needs joinsfast reads · copies to keep honest
左:每张订单都指向同一个地址。右:地址被复制进每张订单。
TechniqueNormalization / Denormalization· 一处存放 vs 快速副本
问题
同一个事实放在多处会渐渐对不上;但 join 会拖慢读取。
思路
默认规范化;在读取更重要的地方有意识地反规范化。
例子
地址放在一张 addresses 表里,对比每张订单里各存一份拷贝。
代价
规范化——更多 JOIN;反规范化——要保持同步的副本。
何时使用
当可测量的慢读取要求时,再反规范化。
关联
和 Polyglot persistence 是同一个「事实来源 + 快速副本」的思想。

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

§ 07

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

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

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

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

Your appPostgressource of truthRediscache · sessionsSearchsearch barVector DBAI featuresS3filesdashed = fast copies kept in sync from the source of truth
五个存储,各管各的活儿。真相存在 Postgres 里;其余都是副本。
PatternPolyglot Persistence· 一个系统里的多个存储
问题
没有哪个数据库能样样第一。
思路
每项活儿有自己的存储;有一个明确的事实来源。
例子
Postgres + Redis + 一个搜索索引 + 一个向量存储 + S3。
代价
每个存储都要运维、备份,并保持一致。
何时使用
当一个真实的痛点需要另一个存储时——而不是「以防万一」。
关联
从 Postgres first 出发;把这门课里几乎所有存储串在一起。

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

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

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

Postgresone store to runRelationaltables + SQLJSONBdocumentsFull-text searchtsvectorpgvectorembeddingsJob queueSKIP LOCKED+ Redis, Elastic…only on measured painstart here · add a store only when a real pain demands it
一个 Postgres 就涵盖表、文档、搜索、向量和队列。只有出现真实痛点才加新存储。
PracticePostgres First· 从 Postgres 开始
问题
团队在痛点出现之前就加数据库。
思路
先用一个 Postgres,直到某个可度量的问题要求更多。
例子
用 JSONB 代替单独的文档存储,用 pgvector 代替单独的向量数据库。
代价
在非常大的规模下,专用引擎仍然会赢。
何时使用
几乎任何产品的起步阶段。
关联
迈向 Polyglot persistence 的第一步——等「暂时」结束之后。

这一切是怎么连起来的

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

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

Search enginesynced copyVector DBsynced copyObject storagefiles by keyIndexspeeds readsRelational DBsource of truth · ACIDScale-outreplicas · shardsNoSQL familydoc · KV · wide · graphBASEeventually consistentCAPC or A on a splitfeedsfeedsrows point togrowsforcesoftenopposite of ACID
这门课各概念之间的主要联系:事实来源居于正中。
选数据库之前
  • 数据是什么形状——表、文档、键值,还是图?
  • 我将怎样查询它——按 id、按关系、按文本、按相似度,还是按时间?
  • 我需要什么保证——严格的 ACID,还是最终一致就够了?
  • 读写的比例如何,以及大致的规模?
  • Postgres 是不是已经能做这件事,在我再加一个存储之前?
  • 哪个存储握有事实来源,哪些只是副本?
你选错了的嗅探信号
  • 你用 LIKE '%...%' 做搜索,而不是用搜索引擎。
  • 一个 键值存储,而你却老是需要按值去查询。
  • 两个人维护的产品用了五个数据库。
  • 你选 MongoDB 是为了避开 schema,结果还是手工重建了校验。
  • 没有明确的事实来源——两个存储都自称是权威。
你选对了的迹象
  • 你最常跑的那个查询,正是这个存储最快的。
  • 保证与利害相匹配——钱要严格,点赞可放松。
  • 你能说出每个重要事实的事实来源。
  • 你加每个存储都是为了解决一个真实、切身的痛点,而不是假想的。
  • 你能在几分钟内向新工程师讲清楚数据模型。

速查表:全部 20 个概念

关系型核心

Relational DatabaseRDBMS

带固定 schema 的表,由外键连接。默认之选。

SQL查询语言

描述你要什么;由数据库决定怎么取。

Transaction全有或全无

多次写入作为一个单元:要么全部 COMMIT,要么全部 ROLLBACK。

NoSQL 家族

Document Store文档数据库

灵活的 JSON 文档;一样东西整个读取。

Key-Value Store键 → 值

键 → 值,立刻拿到。缓存、会话、计数器。

Wide-Column Store列族存储

把数据分散到许多机器上,吞下写入洪流。

Graph Database图存储

节点和边;关系查询,没有痛苦的 JOIN。

专用存储

Time-Series DatabaseTSDB

按时间的读数;按窗口查询,旧数据过期。

Search Engine全文搜索

带排序和拼写容错的全文搜索。是副本,不是真相。

Vector Database向量存储

Embedding 与按含义找最近邻的搜索。RAG 的基础。

Object StorageS3

大文件按键放进 bucket;数据库只留一个指针。

保证

ACID严格的承诺

一个事务要么完全正确,要么没发生。用于钱。

BASE最终一致性

始终可用,副本最终收敛。用于点赞。

CAP Theorem在 C 与 A 间选一个

网络分区期间——要么正确性,要么可用性。

规模

IndexB-tree index

一个让读取变快的指针;代价是空间和更慢的写入。

Replication主从

相同的数据放在多台机器上:可靠性与读取。存在延迟。

Sharding水平分区

按键把不同的数据放在不同的机器上。这个键近乎永久。

Normalization / Denormalization一处存放 vs 副本

一个事实放在一处——或者为了速度有意留下副本。

选择

Polyglot Persistence多个存储

每项活儿有自己的存储,只有一个事实来源。

Postgres First从 Postgres 开始

先用一个 Postgres,直到真实的痛点需要更多。

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

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

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