速成课 · No. 06
语言和框架来了又走。而你存下的数据、以及你用来存它的那个形状,却能比它们全都活得更久——所以挑一个数据库,就是在选择你的系统如何记忆,以及要改变它的想法有多难。
只讲精髓 · 每个存储一幅画面 · 用例子代替跑分
数据库是你系统的长期记忆。代码会被重写;而数据、以及你为它选定的那个形状,往往比周围的一切都活得更久。
数据比代码活得久
一栋房子几十年间会重新粉刷、换家具、甚至翻修屋顶——但第一天浇下的地基会留着,因为换掉它意味着要把整栋房子抬起来。
语言会变,框架会被换掉,UI 会被重画十遍。数据和它的结构通常留着,因为在一个装满真实记录的运行中系统上去改它们,既慢又险。所以数据库是第一件要认真想清楚的事,也是最不能仓促的事。
数据库回答两个问题
挑一个存东西的地方,要问两件事:这些东西是什么形状——是书架上的书,还是挂钩上奇形怪状的工具?——以及它们必须多安全——是一个保险库,还是一个顺手的抽屉?
每一种存储都由两件事定义:它如何塑造数据(刚性的表、灵活的文档、一张由链接构成的图),以及它保证什么(严格的正确性,还是用更松的承诺换来速度与可用性)。几乎每一次数据库选型,都是对这两点的一个不同回答——而这门课其余的部分,就是那份菜单。
按你怎么读和写来挑,而不是按炒作来挑
一个文件柜、一个卡片目录、一条传送带,全都是「存储」——但你按自己将怎么取东西来挑,而不是按哪个看上去最新。
没有最好的数据库。一个对快速键查找堪称完美的存储,用在深层关系查询上就错了;一个为巨量写入而生的存储,用在严格的银行事务上就错了。对的问题是你的访问模式——你读什么,你写什么,多频繁,以及当事情出错时它必须怎么表现。
代码容易改。数据不容易。挑这个存储,要像挑一个你将与之相处多年的决定——因为你真的会。
对大多数系统而言,默认就是 relational 数据库——而它配得上这个默认。从这里起步,只在有理由时才离开。
表、行与关系
一张做对了的电子表格:每个工作表是一类东西(客户、订单),每一行是一条记录,而订单指回它所属的那个客户。
Relational 数据库(Postgres、MySQL)把数据存在带固定 schema 的表里,并通过引用把表连起来——一条订单行存着它所属客户的 id。schema 是一份关于形状的承诺:每条订单都有它该有的列,在进来的路上就被校验。先付出结构,换取日后的安稳。
ACID:要么全做、要么不做的正确性
一笔转账是两次写入——这边减、那边加——但只是一个意图。要么两者都发生,要么都不发生;一笔做了一半的转账是一场灾难。
Relational 数据库给你带 ACID 保证的事务:一组改动要么完整提交,要么完整回滚,彼此隔离,而且一旦确认就能挺过宕机。这就是为什么银行、订单和库存都活在 SQL 里——当正确性比纯粹的速度更要紧时,你想要的就是这份承诺。
SQL:说出你要什么,而不是怎么拿到
你告诉一位图书管理员「1950 年以后出版的法国作家的每一本小说,按年份排序」——而他们去想办法把它们找出来。你描述的是结果,而不是检索。
SQL 是一门声明式查询语言:你陈述你想要的答案,而数据库规划如何去取,包括把表连接起来。SELECT ... JOIN ... WHERE ... 能回答那些你在设计时从未预料到的问题——这是 relational 模型一项不动声色的超能力。
为什么它是对的默认
一件好用的通用工具:在任何单一活儿上都不是最快的,却是你一直伸手去拿的那个,直到某项活儿明确要求别的东西。
结构化的数据、真实的关系、临时的查询、强保证——relational 数据库把这一切都做得很好,而现代 Postgres 还能搞定 JSON、全文搜索、甚至 vector。**每个项目都从这里起步;**只在某个具体的痛——规模、形状或访问模式——把你推出去时,才去够别的东西。
从 Postgres 起步。问题不是「为什么用 SQL」——而是你有没有一个不用它的真实理由。
有时候刚性的表就是错的形状。「NoSQL」其实是一个存储家族,每一个都朝不同方向、为不同的活儿弯曲着规则。
document 存储:每样东西一个 JSON blob
一个装满填好的表格的文件夹,每张表格的字段都可以略有不同——没有一个中央模板强迫每张都一致。
Document 数据库(MongoDB)把记录存成灵活的类 JSON 文档,可嵌套、schema 轻,所以一个 collection 里的两条记录可以不一样。当形状各异或快速演变时它很棒,而且整样东西——一个商品、一份资料——都作为一个文档存着,一次读取就取回。代价是:比 SQL 更少的保证,以及更难的跨文档查询。
key-value 存储:一张巨大的哈希表
一个衣帽寄存处——你递上一个号码牌,拿回的正好是你的外套。不用搜,不用问,瞬间到手。
Key-value 存储(Redis、DynamoDB)是最简单的形状:给一个键、出一个值,快得惊人。对缓存、会话、限流计数器,以及任何你靠已知 id 去取的东西,都堪称完美。取舍是:你只能按键查找——没法对值本身做丰富的查询。
wide-column 存储:为巨量写入而生
一座辽阔的仓库,有数不清的一模一样的过道,布局让一千辆叉车能同时上货而互不碰撞。
Wide-column 存储(Cassandra)把数据铺在许多机器上,换来巨大的写入吞吐量与规模,以丰富查询和严格一致性为代价,换取吸纳一道洪流的能力。用在事件日志、遥测和信息流上,规模大到单台 SQL 机器会撑不住。强大——但要运行好,运维上的活儿也更多。
graph 数据库:当链接才是重点时
一块侦探的钉板——照片之间用线连着。价值不在任何一张照片里;而在那张「谁与谁相连」的网中。
Graph 数据库(Neo4j)把数据建模成节点和它们之间的边,让关系查询——「喜欢这个的朋友的朋友」「这两个账户之间的路径」——既自然又快,而 SQL 在这里需要痛苦的多路 join。这是社交图谱、推荐和欺诈检测的家。当连接比记录本身更要紧时,就去够它。
NoSQL 不是「更新的 SQL」。每一种存储都丢掉一个保证去赢得某样具体的东西——要知道你换走的是哪一个。
在通用存储之外,坐着一些为单一活儿打造的数据库,在那里一个专用引擎能把通用引擎甩开一大截。
time-series:被时钟打了戳的数据
一台心率监护仪的纸带——一条没完没了的读数长条,每个都系在一个时刻上,而你几乎总在问「这两个时间之间发生了什么?」
Time-series 数据库(InfluxDB、TimescaleDB)为按时间顺序到达、并按时间查询的数据而调校——指标、传感器读数、价格。它们吞吐巨大的流,并能快速回答「过去一周每分钟的平均值」,还内建了旧数据的过期。当你存的一切都有一个时间戳、而时间是主轴时,就是这个。
search 引擎:全文与相关性
一个搜索框,它原谅拼写错误,把最好的匹配排在最前,你打「run」时也能找到「running」——就像一位懂你意思的图书管理员。
Search 引擎(Elasticsearch、OpenSearch)为带排序、模糊匹配和过滤的快速全文搜索索引文本——这正是 SQL 里一个 LIKE '%word%' 慢吞吞又笨拙地做的事。它们通常跟你的主数据库并排跑、保持同步,驱动着搜索框和日志探索。它不是你的真相来源——而是对准它的一面快速透镜。
vector 数据库:按含义来搜
不再去匹配确切的词,你问的是「找些感觉像这个的东西给我」——而它按相似度、而非关键词,返回那些邻居。
Vector 数据库(pgvector、Pinecone)存的是 embeddings——含义的数字指纹——并找出最近的那些,于是你能按相似度、而非确切文本来搜索。这是 semantic search 和 RAG 底下的引擎,在那里一个 AI 应用检索最相关的片段来据以作答。AI 时代的新必备存储。
object storage:大文件去的地方
一个自助仓储单元——不是为卡片索引,而是为那些家具:你按标签存着、整件拖出来的笨重东西。
Object storage(S3 及其同类)不是数据库,但它是那些大块 blob 存放的地方——图片、视频、备份、文档——按一个键来寻址,并能廉价地大规模提供。这个模式几乎是通用的:把文件放在 object storage 里,把一行指向它的记录放在你的数据库里。记录进 DB,负载进 bucket。
别把你的主数据库硬掰成一个搜索引擎或文件服务器。把专用的活儿放进为它打造的那个存储里。
一个数据库最深的选择不是它的形状——而是当事情出错时它承诺什么。一点字母汤,给那笔取舍起了名字。
ACID:悲观、正确、小心
一位银行柜员,核对每一笔记录,在账目对平之前绝不往下走——慢一些,但从不出错。
ACID(Atomicity、Consistency、Isolation、Durability)是 relational 数据库的严格承诺:每个事务要么让数据保持正确且完整,要么压根不发生。你付出一些速度,以及跨机器更难的扩展。对于钱、订单,以及任何一个数字错了都不可接受的事,这就是你想要的那笔取舍。
BASE:乐观、可用、最终正确
一家繁忙的店,在价签还在更新的当口也让每个人继续买——那些数字过一会儿就追上来,而那没关系。
许多 NoSQL 系统采取相反的立场:BASE(Basically Available、Soft-state、Eventually consistent)。它们保持又快又可用,并接受数据的各份副本在收敛之前可能短暂地不一致。当可用性胜过瞬时精确时它很完美——一个点赞数、一道信息流、一个浏览计数器——而一秒钟的陈旧不值什么。
CAP:当网络断裂时,二选一
一个被断掉的电话线劈开的团队:每一半都能独自接着干(但他们会分叉),或者都停手直到重新接上(但你停摆了)。你不能两者兼得。
CAP 定理说,当一个网络分区把你数据库的机器劈开时,你必须选择 Consistency(拒绝写入、保持正确)或 Availability(继续服务、冒不一致的险)——在分区期间你不能两者兼得。每一个分布式数据库都做这个抉择。知道你的存储选了哪个,就知道它在最糟的那天会怎么表现。
钱用 ACID,点赞用 BASE。你需要的那个保证,由「短暂出错」的代价来决定。
少数几样技术,把一个数据库从一台脆弱的单机变成又快又能挺住的东西。它们你都会遇上。
索引:数据版的书末索引
要在一本 900 页的书里找一个词,你不会逐页读——你翻到末尾的索引,直接跳过去。
索引是一个旁路结构,把一次全表扫描变成一次直接查找——**对读取速度而言最大的那根杠杆。**问题在于:每个索引都要花存储,并拖慢写入(每次插入都得连它一起更新)。所以你给那些你实际用来过滤和排序的列建索引——不是每一列、以防万一。
复制:留着副本,挺过故障
一份文件存在三栋楼里的三个保险柜中——若一个烧毁了,其余的仍然存着它,而三个人可以同时来读。
复制把数据的副本放在好几台机器上:若主节点挂了,一个从节点接管,而读取可以分散到各个副本上。它买来可靠性与读取扩展。问题是复制延迟——一个从节点可能落后一瞬,所以你刚写下的一个值,可能还没在某个副本上出现。
分片:当一台机器不够时就拆开
一个塞爆的文件柜变成了一个柜里放 A–M、另一个柜里放 N–Z。地方多了——但现在你必须知道一个名字住在哪个柜里。
当数据或写入负载超出一台机器时,分片把数据拆到许多台上——用户 A–M 在这儿,N–Z 在那儿。它解锁了近乎无限的规模,但**分片键是一个艰难、近乎永久的选择:**选差了,你就得到热点,或是不得不命中每一个分片的查询。复制是复制同一份数据;分片是拆开不同的数据。
先规范化,再有意地反规范化
一份人人都参照的主地址清单(没有矛盾),对上把一份地址的副本钉在每一张订单上(读起来更快,但现在有了得各自保持诚实的副本)。
规范化把每个事实恰好放在一个地方——干净、没有矛盾,relational 的默认。反规范化有意地复制数据,好让读取更快、需要更少的 join。两者都正当;规矩是要有意识地去做,并且永远知道哪份副本是真相来源、哪份只是一个快速的复制品。(和缓存是同一课。)
先加索引再加服务器,先加副本再加分片。最便宜的扩展,是那种你不必去运维的。
你不是挑一个数据库,再把一切都硬塞进去。真实的系统会用上好几个,每一个都用在它最擅长的活儿上。
polyglot persistence:有意地用上好几个存储
一间厨房有冰箱、冷冻柜和食品柜——不是一个箱子被逼着把三件事都干了。每一个存着适合它的东西。
成熟的系统通常是 polyglot 的:Postgres 作为真相来源,Redis 用于缓存和会话,一个搜索索引用于搜索框,也许还有一个 vector 存储用于 AI 功能、一个 object storage 用于文件。每一个存储都做它擅长的事。这门功夫在于始终清楚哪一个握着真相,其余的作为保持同步的快速副本。
从 Postgres 起步;有意地从它里头长出去
一把瑞士军刀,在你真需要那整套工具箱之前,能搞定多到惊人的事。
现代 Postgres 强得惊人——relational 内核、JSON 文档、全文搜索、vector,甚至一个能用的任务队列。大多数产品都该只从 Postgres 起步,只在一个真实的、被度量过的痛要求时才加一个专用存储。每一个额外的数据库,都是又一样要运行、备份、保持一致的东西。最便宜的存储,是你没加的那个。
- 数据是什么形状——表、文档、key-value,还是一张图? - 我将怎样查询它——按 id、按关系、按文本、按相似度,还是按时间? - 我需要什么保证——严格的 ACID,还是 eventually consistent 就行? - 读写的平衡如何,大致的规模如何? - 在我加另一个存储之前,Postgres 能不能已经搞定这个? - 哪个存储握着真相 来源,而哪些只是副本?
- 你在用
LIKE '%...%'做搜索,而不是一个搜索引擎。 - 一个 key-value 存储,而 你却不断需要按值去查询。 - 一个两人维护的产品,用了五个数据库。 - 你挑了 MongoDB 来躲开 schema,然后又亲手把校验重新搭了一遍。 - 没有清晰的真相 来源——两个存储都自称是权威的。
- 你跑得最多的那个查询,正是这个存储最快的那个。 - 保证对上了 利害——给钱用严格的,给点赞用宽松的。 - 对每一个重要的事实,你都能说出它的真相 来源。 - 你加每一个存储,都是为了解决一个真实、感受得到的痛,而不是一个假想。 - 你 能在几分钟内向一个新工程师讲清楚那个数据模型。
最好的数据库决定通常是「暂时用 Postgres」——再加上那份智慧,去准确知道「暂时」何时已经结束。