1 开始之前
下面是针对同一张表、在同一台服务器上、相隔几秒执行的两个查询:
第一个只访问了 4 个页,第二个却访问了 35,173 个页。
两个查询几乎都不返回数据,看起来也都很简单,ORM 生成它们中的任何一个都不会有意见。一个让数据库只用 3% CPU 空闲运行,另一个却会让某人在凌晨 2 点被叫起来处理告警。
这就是本书存在的原因。
数一数页面
大多数 PostgreSQL 建议都来自经验之谈。加一个索引。调高 shared_buffers。运行清理。每条建议都有一部分时间是对的;但如果你没有服务器的模型,就无法判断现在是否正好属于那部分时间。
这个模型比你想象的更小。Postgres 把所有东西(表、索引以及它们旁边的簿记信息)都存放在 8 KB 的页中,并以整页为单位读取、写入和缓存。慢查询几乎总是在做两件事之一:访问了远超需要的页,或者进行了远超需要的往返。
因此,本书要培养的第一个习惯只有一句话:数一数页面。
- 设计表时,问问一页能容纳多少行。
- 添加索引时,问问一次查找前后分别会访问多少个页。
- 阅读执行计划时,先看
Buffers:行。它统计的是页数。 - 当表增长速度超过数据本身时,问问那些页里到底放着什么不属于你的东西。
你不需要整天思考字节数。你需要对 8 KB 能装下什么有一个直觉,因为 Postgres 几乎所有性能特征都由此而来。书名也因此得名。
先用数据证明,该换更大的机器了
第二个习惯与钱有关。数据库变慢时,最容易买的是一台更大的机器,而且一开始确实有效。然后数据追上来,你每个月花更多钱,却还是同一个问题。
索引每个月不花钱。重写查询只需要一个下午。连接池代理、清理设置,或者更小的行,只需要读一点材料。所以,本书有一个比其他观点都更坚持的看法:先用完便宜的修复手段,再购买硬件;真的要升级时,用数字证明确实需要升级(先用数据证明,该换更大的机器了)。每一章归根结底都是一个便宜的修复手段,以及证明它有效的证据。
先从一个数据库开始,凭证据离开
不断成长的产品会收集各种数据库:缓存、文档存储、队列、搜索引擎、向量存储。每一种都带来自己的备份、升级、故障模式和告警值班,而且它的数据还必须与其他所有系统保持同步。
Postgres 可以在一台服务器上,与其余数据一起处在同一个事务中,覆盖其中大多数需求。文档放进 jsonb。队列就是一张表,再配合 for update skip locked。全文检索和向量数据与它们所描述的行放在一起。时间序列适合 BRIN 和按月分区,地图则交给 PostGIS。这是一种能满足大多数需求的方案,而“大多数”能覆盖的范围比你猜的更远(你还不需要另一个数据库)。
大多数不等于全部,本书也会说明边界在哪里。在同样的两百万条事件数据上,两个列存储系统只用不到五分之一的字节数保存数据,查询速度约为调优后 Postgres 的十倍;即使只用一个核心,仍然至少快两倍。大规模搜索相关性、流、全局写入以及超大型向量集,都有各自的边界(Postgres 不擅长什么)。
所以,我们让每个产品都从一个 Postgres 开始;只有当我们能说清某项特性,并用数字证明确实需要它时,才添加第二个系统,同时仍然让 Postgres 作为权威数据源。这又回到了更大的机器:应用到架构上,就是从扎实的基础开始,凭证据离开。
每个结论都配有验证
你不该只因为一本数据库书这么说(包括这一本)就接受任何结论。因此,这里每个重要结论都附有证据。
这里的每个查询都在一台真实的 PostgreSQL 18 服务器上运行,使用的是一分钟内就能加载的示例模式。执行计划是真实的执行计划,大小是真实的大小;当某一章说索引有帮助时,还会展示添加索引前后读取的页数。重要结论旁边会放置动手验证框。每个框都包含该章实验脚本中的精确检查,也就是一个声明不成立就会大声失败的 lab.prove(...),这样你可以重新运行证据,而不是相信我们。
这也是本书保持诚实的方式。Postgres 发生变化时,检查会失败,章节就会被修正。
你会 SQL;这里讲的是它的底层机制
你每天都在写应用代码、使用 Postgres,可能通过 ORM,也可能手写 SQL。你知道 select、join、group by、insert 和 update。也许最近因为总得有人负责,你成了“数据库那个人”。
你缺少的是底层机制的模型:为什么一次 update 会让表变大,为什么上个月还很快的查询现在变慢了,为什么规划器不使用你刚建的索引。没有这个模型,调优就只能靠猜。每个内部概念都会在第一次出现时解释。不需要懂 C、不需要读源代码,也不需要有生产经验。
以两种深度阅读
每一章其实是两本书合在一起。简明路线是一篇短文:它的论点、证明论点的一条证据,以及你该怎么做。每章只读这一部分,你就能获得整本书的内容。深入阅读包含内部机制、完整测量结果和边界情况,每一段都紧跟在它所展开的段落之后。
在网站上,章节顶部的简明 / 深入阅读开关会为整本书设置阅读深度;在简明模式下,每个深入阅读块会折叠成一行,你可以单独打开。在纸质版上,每个深入阅读都会打印出来,并单独排版,方便跳过;每个动手验证框也会打印出它所验证的结论,以及保存检查的实验文件。
我为什么写这本书
我是 Chase Pierce。我创办了 Flagon,希望公开地构建软件,也公开地经营一家公司。
我没有什么关于自己如何走到今天的传奇故事。我因为一些朴素的理由选择了 Postgres,然后一直坚持选择它。相较于 MySQL,是因为它能做更多事情:支持事务性的模式变更,拥有更丰富的类型,以及能让数据库成长为你所需形态的扩展系统。相较于 Oracle 和 SQL Server,是因为它开源、可以在任何地方运行,而且没有按核心数量计费的许可证。这些理由在之后的每个项目中都经受住了考验。
真正经不起考验的是围绕它的建议。我想要的那本书并不存在于一个地方。内部机制类书籍默认你想参与服务器本身的开发。调优指南把设置交给你,却不给出解释这些设置的模型。博客文章只对某个人的负载、某个人的版本、某一年成立。我想把模型、证据和观点放在一起,写给那个刚刚接手数据库的人。所以我写了这本书,并让每个结论都带有一个检查,因为无法重新运行的结论,只是又一条经验之谈。
本书其余部分会说“我们”。这是 Flagon 的声音,也是在同一个终端前看着同一份执行计划的你和我。我们持有什么观点,就直说,并展示背后的数字。如果你发现错误,那就是一个 bug:提交问题或发送修复。
先理解运行机制,再跳到痛点所在
| 部分 | 内容 |
|---|---|
| 理解运行机制 | 延迟、页、缓冲区缓存、MVCC、HOT 更新、预写式日志、ACID、清理,以及这些内容与 MySQL 的差异 |
| 数据库设计 | 类型、键、约束、数据模型、扩展 |
| 高效查找数据 | 索引、EXPLAIN、规划器、查询模式、搜索、函数 |
| 变更数据库 | 锁、保持网站运行的迁移、大批量加载 |
| 生产运维 | 连接、基准测试、配置、监控、备份、安全、托管服务,以及处理慢数据库的行动手册 |
| 横向扩展 | 先纵向扩容、复制、分区、分片,以及 Postgres 的短板 |
首先是入门篇先认识这些基本概念:快速认识 SQL 和服务器词汇,包括表、查询、更新、连接、索引、数据库、模式、角色和目录。如果 search_path 和 pg_class 对你来说已经是老朋友,可以跳过这一章。运行机制是基础。如果其他内容都不读,至少读一切都从页说起和更新就是插入;后面几乎每一章都会依赖它们。然后跳到你正在遭遇的问题。慢查询正在毁掉你的一周?从读执行计划,别只看耗时开始。你从 MySQL 转来?Postgres 不是 MySQL会指出哪些习惯不能直接迁移。
每章最后都有“本章要点”和“延伸阅读”。
每个示例都运行在 PostgreSQL 18 上
本书以PostgreSQL 18为目标。几乎所有示例都运行在 18.6 上;少数需要第三方镜像的章节会固定另一个小版本,并明确说明(最后再考虑分片中的 Citus 使用 18.4,Postgres 不擅长什么中的 pg_duckdb 使用 18.1)。其中大多数内容适用于从 13 开始的任何版本;较新的特性会标注引入它的版本,例如 MERGE(PostgreSQL 15+)。页面布局、MVCC、清理、规划器和预写式日志在细节上发生过变化,但形态没有改变。
psql 和 EXPLAIN 的输出紧跟在生成它的查询后面,单独放在代码块中;长输出会用 ... 截短。其中有些内容在你的机器上会不同,这是正常的:
- 事务 ID、OID 和文件名由服务器在运行时分配。你的值会不同,但它们之间的关系不会变。
- 耗时取决于硬件和负载。我们的实验在笔记本电脑上的 Docker 中运行,旁边还有其他负载;有几个写入量很大的实验会打开
synchronous_commit = off以更快完成(实验会明确说明),所以把每一毫秒都视为粗略值。 - 缓冲区数量和行数是证据。 对于给定的数据集和执行计划,它们每次都会相同。完整查询的计数可能会比表本身多访问几十个页,因为还要读取目录和其他小对象:对包含 35,173 个页的
events堆做一次全表扫描,报告的数量约为 35,249。 - 大小是
pg_size_pretty按二进制口径报告的数值:288,137,216 字节的events堆是 275 MB,而不是 288。
提示框会标出你绝对不能错过的内容。每一条警告都描述了一种可能让生产数据库瘫痪的方式。
运行你自己的 PostgreSQL 18
你需要 Docker 和 bash:Linux 或 macOS 终端都可以,Windows 上则可以使用 Git Bash 或 WSL。实验套件是一个小型 Compose 项目:里面有带示例数据的 PostgreSQL 18,以及每章一个将章节结论转为检查的脚本。下载后运行某一章:
章节编号或名称都可以使用(./lab 3、./lab storage),单独运行 ./lab 则会列出全部章节。第一次运行会启动服务器并创建示例数据库,大约需要一分钟。每次运行都会获得一份全新的数据副本,所有成立的结论都会打印 PROVED:,遇到第一个不成立的结论就会停止。实验页面会解释其余内容;运行 ./lab all 会执行所有章节,并列出失败的章节。
如果你更喜欢图形化客户端,服务器只监听本机的 127.0.0.1:5418。有几章(复制、分片、MySQL 对比)需要多台服务器;对于这些章节,./lab 会启动额外容器,并在结束后将其移除。
套件中的 postgres 用户是超级用户。这一点很重要:本书最有用的一些工具(尤其是读取页原始字节的 pageinspect)默认只有超级用户才能使用,而托管服务通常不会给你超级用户权限。完成后,docker compose down -v 会把所有内容彻底删除。
一套普通的 SaaS 模式贯穿每一章
每章都使用同一套小型 SaaS 模式:账户、账户中的用户、它们拥有的项目,以及一张不断增长的大型事件表(部署、构建、评论、告警、登录)。它足够普通,也足够大,能让规划器做出真实决策,让错误变成可以测量的代价:
| 表 | 行数 | 堆大小 | 值得了解的列 |
|---|---|---|---|
| accounts | 1,000 | 72 KB | plan |
| users | 100,000 | 9 MB | account_id、email(组合唯一) |
| projects | 20,000 | 1.5 MB | account_id、owner_id、archived_at |
| events | 2,000,000 | 275 MB | account_id、project_id、user_id、kind、payload(jsonb)、created_at |
每张表都有一个 bigint 类型的 identity id,以及一个 created_at。数据使用固定的随机种子生成,时间从一个固定时刻,即 2026 年 10 月 6 日,向前回溯,因此你的执行计划、计数和日期都会与我们的相同。
有两点是故意这样设计的,所以暂时不要“修复”它们:
events没有任何二级索引,只有主键。索引相关章节会逐一添加索引,让你准确看到每个索引带来了什么。- 只有一个外键列有索引,而且只是碰巧。
users.account_id是unique (account_id, email)约束所建索引的前导列;其他引用列都没有索引。Postgres 不会替你为引用列建立索引,而这正是现实模式中最常见的错误之一。有目的地选择约束会展示它的代价。
深入阅读:为什么每台机器生成的两百万条事件都相同
套件中的 seed.sql 包含完整种子:五个扩展、四张表,以及每张表一条 insert。有意思的部分是生成器。下面是 events 以及填充它的语句:
setseed 会让 random() 每次返回同一序列,因此相同的项目会得到相同的事件。created_at 均匀分布在 2026 年 10 月 6 日之前的一年中,而 order by 按时间顺序插入行,所以最近的事件会集中在表的末尾(延迟和索引章节会用到这一事实)。数组中重复出现的值会造成分布倾斜:部署和构建很常见,评论、告警和登录各占八分之一,四分之一的状态为 failed。最后的 vacuum analyze 会让每个页都标记为全可见,并让规划器的统计信息保持最新。
套件会把它加载到名为 book 的数据库中(约 345 MB),并将其标记为模板。复制模板比重新运行种子便宜得多,所以请在副本中探索:
许多章节会更新每一行、删除半张表,或者添加并删除索引。永远不要直接操作 book(套件甚至不允许连接到它)。当你把 scratch 搞乱后,执行 drop database scratch,再重新复制一份。
在这里搞坏东西,别在生产环境里搞
有些示例是故意具有破坏性的:它们会更新数百万行、长时间保持事务打开,或者删除半张表来观察清理工作。请在可丢弃的服务器上运行它们,永远不要在任何人依赖的数据库上运行。对于面向生产环境的技术(迁移模式、监控查询、设置),正文会说明这一点,以及它会加什么锁、付出什么代价。
在简要认识这些术语之后,第一站是硬件,因为一个页在你请求它时所处的位置不同,访问它所需的时间也可能有天壤之别。
本章要点
- 几乎每个 Postgres 性能问题都可以归结为访问了多少页、进行了多少次往返。数一数页面。
- 先用完便宜的修复手段,再购买硬件;真的要升级时,用数字证明需要升级。先用数据证明,该换更大的机器了。
- 从一个 Postgres 开始。只有当你能说出某项特性,并用数字证明确实需要它时,才添加第二个系统。
- 先看执行计划的
Buffers:行,再看耗时。缓冲区数量和行数可以复现,毫秒数不行。 - 每个重要结论都有一个动手验证检查。重新运行它,不要只相信我们。
- 只读简明路线来理解论点;想看内部机制或完整测量结果时,再打开深入阅读。
- 获取实验套件,在名为
book的模板中运行带有示例数据、可随时丢弃的 PostgreSQL 18。 - 在套件目录中运行
./lab <chapter>,重新执行任意章节的证据;动手验证框会展示具体检查。 - 在副本中工作(
create database scratch template book),弄乱后将其删除。
延伸阅读
- PostgreSQL 18 documentation,PGDG。整本书都依赖的参考资料;先学会在里面找到所需内容。
- PostgreSQL 18 release notes,PGDG。本书所有“(PostgreSQL 18+)”标签的来源。
- Versioning policy,PGDG。五年的支持窗口,以及你的大版本还剩多长时间支持。
- psql reference,PGDG。每个示例背后的客户端;花一个小时读它的反斜杠命令很值得。
- Set returning functions,PGDG。了解
generate_series如何生成种子,也了解如何生成自己的测试数据。