# 开始之前

> 为什么本书要数页，为什么每个结论都配有验证，适合哪些读者，如何以两种深度阅读，以及如何运行每章使用的同一个 PostgreSQL 18 服务器和数据。

---

LLMS 索引： [llms.txt](/llms.txt)

---

<span id="basics-chapter-1"></span>

下面是针对同一张表、在同一台服务器上、相隔几秒执行的两个查询：

```sql
select * from events where id = 777777;
select count(*) from events where project_id = 4242;
```

第一个只访问了 4 个页，第二个却访问了 35,173 个页。

两个查询几乎都不返回数据，看起来也都很简单，ORM 生成它们中的任何一个都不会有意见。一个让数据库只用 3% CPU 空闲运行，另一个却会让某人在凌晨 2 点被叫起来处理告警。

这就是本书存在的原因。

## 数一数页面 {#count-the-pages}

大多数 PostgreSQL 建议都来自经验之谈。加一个索引。调高 `shared_buffers`。运行清理。每条建议都有一部分时间是对的；但如果你没有服务器的模型，就无法判断现在是否正好属于那部分时间。

这个模型比你想象的更小。Postgres 把所有东西（表、索引以及它们旁边的簿记信息）都存放在 8 KB 的**页**中，并以整页为单位读取、写入和缓存。慢查询几乎总是在做两件事之一：访问了远超需要的页，或者进行了远超需要的往返。

因此，本书要培养的第一个习惯只有一句话：**数一数页面**。

- 设计表时，问问一页能容纳多少行。
- 添加索引时，问问一次查找前后分别会访问多少个页。
- 阅读执行计划时，先看 `Buffers:` 行。它统计的是页数。
- 当表增长速度超过数据本身时，问问那些页里到底放着什么不属于你的东西。

你不需要整天思考字节数。你需要对 8 KB 能装下什么有一个直觉，因为 Postgres 几乎所有性能特征都由此而来。书名也因此得名。

## 先用数据证明，该换更大的机器了 {#earn-the-bigger-box}

第二个习惯与钱有关。数据库变慢时，最容易买的是一台更大的机器，而且一开始确实有效。然后数据追上来，你每个月花更多钱，却还是同一个问题。

索引每个月不花钱。重写查询只需要一个下午。连接池代理、清理设置，或者更小的行，只需要读一点材料。所以，本书有一个比其他观点都更坚持的看法：先用完便宜的修复手段，再购买硬件；真的要升级时，用数字证明确实需要升级（[先用数据证明，该换更大的机器了](/37-scale/#earn-the-bigger-box)）。每一章归根结底都是一个便宜的修复手段，以及证明它有效的证据。

## 先从一个数据库开始，凭证据离开 {#start-with-one-database-and-leave-on-evidence}

不断成长的产品会收集各种数据库：缓存、文档存储、队列、搜索引擎、向量存储。每一种都带来自己的备份、升级、故障模式和告警值班，而且它的数据还必须与其他所有系统保持同步。

Postgres 可以在一台服务器上，与其余数据一起处在同一个事务中，覆盖其中大多数需求。文档放进 `jsonb`。队列就是一张表，再配合 `for update skip locked`。全文检索和向量数据与它们所描述的行放在一起。时间序列适合 BRIN 和按月分区，地图则交给 PostGIS。这是一种能满足大多数需求的方案，而“大多数”能覆盖的范围比你猜的更远（[你还不需要另一个数据库](/15-models/#basics-chapter-15)）。

大多数不等于全部，本书也会说明边界在哪里。在同样的两百万条事件数据上，两个列存储系统只用不到五分之一的字节数保存数据，查询速度约为调优后 Postgres 的十倍；即使只用一个核心，仍然至少快两倍。大规模搜索相关性、流、全局写入以及超大型向量集，都有各自的边界（[Postgres 不擅长什么](/41-limits/#basics-chapter-41)）。

所以，我们让每个产品都从一个 Postgres 开始；只有当我们能说清某项特性，并用数字证明确实需要它时，才添加第二个系统，同时仍然让 Postgres 作为权威数据源。这又回到了更大的机器：应用到架构上，就是从扎实的基础开始，凭证据离开。

## 每个结论都配有验证 {#every-claim-comes-with-a-proof}

你不该只因为一本数据库书这么说（包括这一本）就接受任何结论。因此，这里每个重要结论都附有证据。

这里的每个查询都在一台真实的 PostgreSQL 18 服务器上运行，使用的是一分钟内就能加载的示例模式。执行计划是真实的执行计划，大小是真实的大小；当某一章说索引有帮助时，还会展示添加索引前后读取的页数。重要结论旁边会放置**动手验证**框。每个框都包含该章实验脚本中的精确检查，也就是一个声明不成立就会大声失败的 `lab.prove(...)`，这样你可以重新运行证据，而不是相信我们。

这也是本书保持诚实的方式。Postgres 发生变化时，检查会失败，章节就会被修正。

## 你会 SQL；这里讲的是它的底层机制 {#you-know-sql-this-is-whats-underneath-it}

你每天都在写应用代码、使用 Postgres，可能通过 ORM，也可能手写 SQL。你知道 `select`、`join`、`group by`、`insert` 和 `update`。也许最近因为总得有人负责，你成了“数据库那个人”。

你缺少的是底层机制的模型：为什么一次 `update` 会让表变大，为什么上个月还很快的查询现在变慢了，为什么规划器不使用你刚建的索引。没有这个模型，调优就只能靠猜。每个内部概念都会在第一次出现时解释。不需要懂 C、不需要读源代码，也不需要有生产经验。

## 以两种深度阅读 {#read-it-at-two-depths}

每一章其实是两本书合在一起。**简明**路线是一篇短文：它的论点、证明论点的一条证据，以及你该怎么做。每章只读这一部分，你就能获得整本书的内容。**深入阅读**包含内部机制、完整测量结果和边界情况，每一段都紧跟在它所展开的段落之后。

在网站上，章节顶部的**简明 / 深入阅读**开关会为整本书设置阅读深度；在简明模式下，每个深入阅读块会折叠成一行，你可以单独打开。在纸质版上，每个深入阅读都会打印出来，并单独排版，方便跳过；每个动手验证框也会打印出它所验证的结论，以及保存检查的实验文件。

## 我为什么写这本书 {#why-i-wrote-this}

我是 Chase Pierce。我创办了 [Flagon](https://www.flagon.io)，希望公开地构建软件，也公开地经营一家公司。

我没有什么关于自己如何走到今天的传奇故事。我因为一些朴素的理由选择了 Postgres，然后一直坚持选择它。相较于 MySQL，是因为它能做更多事情：支持事务性的模式变更，拥有更丰富的类型，以及能让数据库成长为你所需形态的扩展系统。相较于 Oracle 和 SQL Server，是因为它开源、可以在任何地方运行，而且没有按核心数量计费的许可证。这些理由在之后的每个项目中都经受住了考验。

真正经不起考验的是围绕它的建议。我想要的那本书并不存在于一个地方。内部机制类书籍默认你想参与服务器本身的开发。调优指南把设置交给你，却不给出解释这些设置的模型。博客文章只对某个人的负载、某个人的版本、某一年成立。我想把模型、证据和观点放在一起，写给那个刚刚接手数据库的人。所以我写了这本书，并让每个结论都带有一个检查，因为无法重新运行的结论，只是又一条经验之谈。

本书其余部分会说“我们”。这是 Flagon 的声音，也是在同一个终端前看着同一份执行计划的你和我。我们持有什么观点，就直说，并展示背后的数字。如果你发现错误，那就是一个 bug：[提交问题或发送修复](https://github.com/flagon-io/flagon/issues)。

## 先理解运行机制，再跳到痛点所在 {#the-machine-comes-first-then-jump-to-what-hurts}

| 部分 | 内容 |
| --- | --- |
| 理解运行机制 | 延迟、页、缓冲区缓存、MVCC、HOT 更新、预写式日志、ACID、清理，以及这些内容与 MySQL 的差异 |
| 数据库设计 | 类型、键、约束、数据模型、扩展 |
| 高效查找数据 | 索引、EXPLAIN、规划器、查询模式、搜索、函数 |
| 变更数据库 | 锁、保持网站运行的迁移、大批量加载 |
| 生产运维 | 连接、基准测试、配置、监控、备份、安全、托管服务，以及处理慢数据库的行动手册 |
| 横向扩展 | 先纵向扩容、复制、分区、分片，以及 Postgres 的短板 |

<span id="tbl-1-1"></span>

**Table 1-1.** 先理解运行机制，再跳到痛点所在


首先是入门篇[先认识这些基本概念](/02-basics/#basics-chapter-2)：快速认识 SQL 和服务器词汇，包括表、查询、更新、连接、索引、数据库、模式、角色和目录。如果 `search_path` 和 `pg_class` 对你来说已经是老朋友，可以跳过这一章。运行机制是基础。如果其他内容都不读，至少读[一切都从页说起](/04-storage/#basics-chapter-4)和[更新就是插入](/06-mvcc/#basics-chapter-6)；后面几乎每一章都会依赖它们。然后跳到你正在遭遇的问题。慢查询正在毁掉你的一周？从[读执行计划，别只看耗时](/19-explain/#basics-chapter-19)开始。你从 MySQL 转来？[Postgres 不是 MySQL](/11-not-mysql/#basics-chapter-11)会指出哪些习惯不能直接迁移。

每章最后都有“本章要点”和“延伸阅读”。

## 每个示例都运行在 PostgreSQL 18 上 {#every-example-ran-on-postgresql-18}

本书以**PostgreSQL 18**为目标。几乎所有示例都运行在 18.6 上；少数需要第三方镜像的章节会固定另一个小版本，并明确说明（[最后再考虑分片](/40-sharding/#basics-chapter-40)中的 Citus 使用 18.4，[Postgres 不擅长什么](/41-limits/#basics-chapter-41)中的 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 {#run-your-own-postgresql-18}

你需要 Docker 和 bash：Linux 或 macOS 终端都可以，Windows 上则可以使用 Git Bash 或 WSL。实验套件是一个小型 Compose 项目：里面有带示例数据的 PostgreSQL 18，以及每章一个将章节结论转为检查的脚本。下载后运行某一章：

```bash
curl -L https://www.flagon.io/books/eight-kilobytes/lab.tar.gz | tar xz
cd eight-kilobytes-lab
./lab 03-storage
```

章节编号或名称都可以使用（`./lab 3`、`./lab storage`），单独运行 `./lab` 则会列出全部章节。第一次运行会启动服务器并创建示例数据库，大约需要一分钟。每次运行都会获得一份全新的数据副本，所有成立的结论都会打印 `PROVED:`，遇到第一个不成立的结论就会停止。[实验页面](https://www.flagon.io/books/eight-kilobytes/lab)会解释其余内容；运行 `./lab all` 会执行所有章节，并列出失败的章节。

如果你更喜欢图形化客户端，服务器只监听本机的 `127.0.0.1:5418`。有几章（复制、分片、MySQL 对比）需要多台服务器；对于这些章节，`./lab` 会启动额外容器，并在结束后将其移除。

套件中的 `postgres` 用户是超级用户。这一点很重要：本书最有用的一些工具（尤其是读取页原始字节的 `pageinspect`）默认只有超级用户才能使用，而托管服务通常不会给你超级用户权限。完成后，`docker compose down -v` 会把所有内容彻底删除。

## 一套普通的 SaaS 模式贯穿每一章 {#one-ordinary-saas-schema-runs-through-every-chapter}

每章都使用同一套小型 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` |

<span id="tbl-1-2"></span>

**Table 1-2.** 一套普通的 SaaS 模式贯穿每一章


每张表都有一个 `bigint` 类型的 identity `id`，以及一个 `created_at`。数据使用固定的随机种子生成，时间从一个固定时刻，即 2026 年 10 月 6 日，向前回溯，因此你的执行计划、计数和日期都会与我们的相同。

有两点是故意这样设计的，所以暂时不要“修复”它们：

- `events` 没有任何二级索引，只有主键。索引相关章节会逐一添加索引，让你准确看到每个索引带来了什么。
- **只有一个外键列有索引，而且只是碰巧。** `users.account_id` 是 `unique (account_id, email)` 约束所建索引的前导列；其他引用列都没有索引。Postgres 不会替你为引用列建立索引，而这正是现实模式中最常见的错误之一。[有目的地选择约束](/14-relationships/#basics-chapter-14)会展示它的代价。

<span id="how-the-seed-makes-the-same-two-million-events-on-every-machine" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-1-how-the-seed-makes-the-same-two-million-events-on-every-machine" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：为什么每台机器生成的两百万条事件都相同
>
> 套件中的 [seed.sql](https://www.flagon.io/books/eight-kilobytes/lab/seed.sql) 包含完整种子：五个扩展、四张表，以及每张表一条 `insert`。有意思的部分是生成器。下面是 `events` 以及填充它的语句：
>
> ```sql
> create table events (
>   id          bigint generated always as identity primary key,
>   account_id  bigint not null references accounts (id),
>   project_id  bigint not null references projects (id),
>   user_id     bigint references users (id),
>   kind        text not null,
>   payload     jsonb not null default '{}',
>   created_at  timestamptz not null default now()
> );
>
> select setseed(0.42);
> -- ... accounts, users and projects first ...
>
> insert into events (account_id, project_id, user_id, kind, payload, created_at)
> select p.account_id, p.id, p.owner_id, r.kind,
>        jsonb_build_object('status', r.status,
>                           'duration_ms', r.duration_ms,
>                           'region', r.region),
>        r.created_at
> from (
>   select g,
>          1 + floor(random()*20000)::bigint as project_id,
>          (array['deploy','deploy','build','build','build',
>                 'comment','alert','login'])[1 + floor(random()*8)::int] as kind,
>          (array['ok','ok','ok','failed'])[1 + floor(random()*4)::int] as status,
>          floor(random()*5000)::int as duration_ms,
>          (array['iad','sfo','ams','fra','syd'])
>            [1 + floor(random()*5)::int] as region,
>          timestamptz '2026-10-06' - interval '365 days'
>            + (g * interval '365 days' / 2000000) as created_at
>   from generate_series(1, 2000000) g
> ) r
> join projects p on p.id = r.project_id
> order by r.g;
>
> vacuum analyze;
> ```
>
> `setseed` 会让 `random()` 每次返回同一序列，因此相同的项目会得到相同的事件。`created_at` 均匀分布在 2026 年 10 月 6 日之前的一年中，而 `order by` 按时间顺序插入行，所以最近的事件会集中在表的末尾（延迟和索引章节会用到这一事实）。数组中重复出现的值会造成分布倾斜：部署和构建很常见，评论、告警和登录各占八分之一，四分之一的状态为 `failed`。最后的 `vacuum analyze` 会让每个页都标记为全可见，并让规划器的统计信息保持最新。

套件会把它加载到名为 `book` 的数据库中（约 345 MB），并将其标记为模板。复制模板比重新运行种子便宜得多，所以请在副本中探索：

```bash
docker compose exec postgres psql -c "create database scratch template book"
docker compose exec postgres psql -d scratch
```

> [!TIP] 在副本中工作
>
> 许多章节会更新每一行、删除半张表，或者添加并删除索引。永远不要直接操作 `book`（套件甚至不允许连接到它）。当你把 `scratch` 搞乱后，执行 `drop database scratch`，再重新复制一份。

## 在这里搞坏东西，别在生产环境里搞 {#break-things-here-not-in-production}

有些示例是故意具有破坏性的：它们会更新数百万行、长时间保持事务打开，或者删除半张表来观察清理工作。请在可丢弃的服务器上运行它们，永远不要在任何人依赖的数据库上运行。对于面向生产环境的技术（迁移模式、监控查询、设置），正文会说明这一点，以及它会加什么锁、付出什么代价。

在简要认识这些术语之后，第一站是硬件，因为一个页在你请求它时所处的位置不同，访问它所需的时间也可能有天壤之别。

## 本章要点 {#the-short-version}

- 几乎每个 Postgres 性能问题都可以归结为访问了多少页、进行了多少次往返。数一数页面。
- 先用完便宜的修复手段，再购买硬件；真的要升级时，用数字证明需要升级。先用数据证明，该换更大的机器了。
- 从一个 Postgres 开始。只有当你能说出某项特性，并用数字证明确实需要它时，才添加第二个系统。
- 先看执行计划的 `Buffers:` 行，再看耗时。缓冲区数量和行数可以复现，毫秒数不行。
- 每个重要结论都有一个动手验证检查。重新运行它，不要只相信我们。
- 只读简明路线来理解论点；想看内部机制或完整测量结果时，再打开深入阅读。
- 获取实验套件，在名为 `book` 的模板中运行带有示例数据、可随时丢弃的 PostgreSQL 18。
- 在套件目录中运行 `./lab <chapter>`，重新执行任意章节的证据；动手验证框会展示具体检查。
- 在副本中工作（`create database scratch template book`），弄乱后将其删除。

## 延伸阅读 {#further-reading}

- [PostgreSQL 18 documentation](https://www.postgresql.org/docs/18/index.html)，PGDG。整本书都依赖的参考资料；先学会在里面找到所需内容。
- [PostgreSQL 18 release notes](https://www.postgresql.org/docs/18/release-18.html)，PGDG。本书所有“（PostgreSQL 18+）”标签的来源。
- [Versioning policy](https://www.postgresql.org/support/versioning/)，PGDG。五年的支持窗口，以及你的大版本还剩多长时间支持。
- [psql reference](https://www.postgresql.org/docs/18/app-psql.html)，PGDG。每个示例背后的客户端；花一个小时读它的反斜杠命令很值得。
- [Set returning functions](https://www.postgresql.org/docs/18/functions-srf.html)，PGDG。了解 `generate_series` 如何生成种子，也了解如何生成自己的测试数据。
