# 统计信息概览

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

---

<span id="mon-chapter-1" class="pdf-anchor"></span><span id="pdf-page-16" class="pdf-anchor"></span>

> [!NOTE]
> **本章将介绍：**
>
> - PostgreSQL 的内部结构；
>
> - 活动统计信息的重要性；
>
> - 活动统计信息的特点。

本章概括介绍活动统计信息，以及它为什么如此重要。我们将以 PostgreSQL 的内部结构为例，说明为什么需要统计信息来理解数据库管理系统（DBMS）内部进程的运行方式，并对这些进程进行有效监控。本书将一直讨论的统计信息，是构建大多数 PostgreSQL 监控工具的基础。

## 1.1. “Postgres 是一项服务” {#mon-section-1-1}

我想像平时开始 PostgreSQL 监控演讲那样，来开始第一章。PostgreSQL 数据库管理系统是一款历史悠久的产品。在它存在的整个过程中，人们为它加入了许多功能，这当然也反映在它的内部结构上。图 [1.1](/01-statistics-overview/#mon-figure-caption-14) 以我个人的粗略理解画出了这套结构。从图中可以看出，数据库管理系统由许多组件组成。不仅如此，这些组件彼此相连，并且在处理数据的过程中持续相互交互。

<span id="mon-item-1" class="pdf-anchor"></span>

如果把问题大幅简化，数据库管理系统可以看成一种提供两项基本服务的服务程序：

<span id="mon-item-2" class="pdf-anchor"></span>

1. 可靠地存储数据。用户和服务会把数据加载到数据库管理系统中，而数据库管理系统的任务就是接收并保存这些数据。

2. 提供数据访问。用户和服务可能需要读取数据，数据库管理系统则提供了用于处理这些数据的查询语言。

基本服务还可以扩展出一些附加服务：

- 多种数据类型，以及灵活组织访问模式的方式；

- 借助索引快速访问数据；

<span id="pdf-page-17" class="pdf-anchor"></span><span id="mon-figure-caption-14" class="pdf-anchor"></span>

![PostgreSQL 数据库管理系统的内部结构：客户端后端进程；Postmaster；查询规划；共享缓冲区；后台工作进程；查询执行；自动清理启动器；索引使用情况；表使用情况；缓冲区 I/O；SLRU 缓存；自动清理工作进程；预写式日志；日志进程；统计信息收集器；逻辑 WAL 发送进程；WAL 归档进程；后台检查点进程；复制进程；写入进程；网络存储；WAL 接收进程；表/索引数据文件；恢复进程](/images/monitoring/figure-1.1.svg)
{#fig-1.1 num="1.1" caption="PostgreSQL 数据库管理系统的内部结构" width=709 height=586}

- 具有原子性、一致性和隔离性保证的并发事务数据访问（这些性质构成了著名的 ACID 缩写[^mon-17-1]）。

为了实现这些服务以及许多其他服务，数据库管理系统依赖各种子系统和机制。

## 1.2. PostgreSQL 的内部结构 {#mon-section-1-2}

下面让我们以尽可能少的细节，粗略了解数据库管理系统的主要子系统，以及执行以下常规任务时内部进程的工作方式：

- 建立连接并维护会话；

- 规划和执行查询；

- 处理数据时进行输入/输出；

- 记录变更；

<span id="pdf-page-18" class="pdf-anchor"></span>

- 复制并归档事务日志；

- 在后台同步数据；

- 记录服务器日志；

- 自动清理。

### 建立连接与会话运行 {#mon-section-star-15}

在通常模式下，数据库管理系统作为服务（服务器程序）运行，等待客户端连接。客户端可以是应用程序，也可以是通过客户端程序（psql、pgAdmin、DataGrip 等）连接的用户。数据库管理系统服务器会为每个客户端创建独立的操作系统进程；按照管理员熟悉的术语，这些进程称为*后端进程*（backend）。在每个这样的进程中，客户端与数据库管理系统服务器之间会建立一个*会话*，这是一条允许客户端与数据库管理系统交互的双向连接。建立会话时，数据库管理系统会根据 `pg\_hba.conf` 中的配置对客户端进行身份验证，初始化内部工作结构，并应用影响后续运行的各种设置。会话准备好后，客户端就可以向服务器发送*命令*，其中既可以是 SQL 命令，也可以是系统控制命令[^mon-18-1]。

在运维数据库管理系统时，了解已连接的客户端、已建立的会话以及这些会话中正在发生的事情非常重要。在某些情况下，客户端的行为和会话状态可能会对数据库管理系统的运行和性能产生负面影响。

### 查询是工作负载的基本单位 {#mon-section-star-16}

查询是命令的一个子集，也是工作负载的基本单位。可以把工作负载理解为所有应用程序发送、并在数据库管理系统一侧执行的全部查询。服务器要处理这些工作负载，需要 CPU、内存、输入/输出和磁盘空间等资源。在运维数据库管理系统时，准确了解数据库管理系统正在执行哪些查询，以及是否有足够资源高效执行这些查询，非常重要。统计信息可以回答这些问题，并为查询优化和性能优化提供起点。

### 查询规划与执行 {#mon-section-star-17}

服务器收到客户端命令后，会检查命令是否正确。最常见的命令是 SQL 查询；在这种情况下，数据库管理系统会开始对它进行*规划*。规划就是为访问数据制定最优计划。访问和中间数据处理的成本（也就是系统资源的使用量）可能各不相同，因此<span id="pdf-page-19" class="pdf-anchor"></span>规划的任务，就是制定并选择成本最低的计划。通常会把计划表示成一张图，其中每个节点都是对数据执行的一项具体操作。

数据库管理系统能够规划某些操作的并行执行，例如从表或索引中读取数据。如果计划要求并行执行，系统会自动启动*辅助进程*（background workers），由它们承担一部分工作。操作完成后，辅助进程也会自动结束。

计划选定后，服务器便按照计划开始执行查询。计划中的每个节点都是一项具体的数据操作（例如对表中的行进行操作）。查询完成后，结果会返回给客户端。结果可以是一组行，也可以是表示执行成功的命令标签（command tag）。如果查询修改了数据，查询结果必须写入*事务日志*（Write-Ahead Log，WAL）。根据数据库管理系统的设置，这个过程可以同步进行，也可以异步进行。无论采用哪种模式，查询执行完成后，数据库管理系统都会把控制权交还给客户端，客户端可以发送下一个查询。

### 查询执行期间的输入/输出 {#mon-section-star-18}

执行查询时，几乎所有数据操作都经过缓冲，在系统为进程分配的内存中完成。数据库管理系统分配的所有内存都由若干段组成；从数据库管理系统自身的使用方式来看，可以将这些内存分成两类：本地内存和共享内存。

本地内存区域是为每个进程（在会话范围内）单独分配的内存段，各进程不能访问彼此的本地内存段。其中有以下几种典型区域：

- 进程工作内存（见参数 *work\_mem*）：在需要时为查询中的一些中间操作临时存放数据而分配，例如排序、去重（`DISTINCT`）、使用 merge join 和 hash join 算法连接表等。如果工作内存不足以完成操作，系统会在磁盘上创建临时文件，并在操作结束后删除这些文件；

- 临时缓冲区（见参数 *temp\_buffers*）：用于处理临时表（temporary tables）的数据。临时表的生命周期可以是整个会话，甚至只是一个事务。这类表不写入日志，常用于保存中间结果；

- 维护操作工作内存（见参数 *maintenance\_work\_mem*）：为 `VACUUM`、`CREATE INDEX`、`REINDEX` 等操作分配。自动清理后台进程使用自己单独的工作内存（见参数 *autovacuum\_work\_mem*）。

> [!NOTE]
> <span id="pdf-page-20" class="pdf-anchor"></span>
>
> **数据库管理系统的配置参数**
>
> 本章及后续章节会经常遇到配置参数。所有参数的完整列表见文档：[postgrespro.ru/docs/postgresql/current/runtime-config](<https://postgrespro.ru/docs/postgresql/current/runtime-config>)。

共享内存（shared memory）是一个通常规模相当大的内存段，在数据库管理系统启动时分配一次。数据库管理系统的所有进程都可以访问共享内存。共享内存中包含：

- 表和索引页的缓冲区缓存；

- WAL 日志缓冲区；

- 保存所有事务状态的日志数据（事务提交状态日志，commit log），以及多版本并发控制的可见性规则所使用的数据；

- 用于管理访问的内部结构（重量级锁、轻量级锁、信号量等）；

- 用于保存点（savepoint）[^mon-20-1]和两阶段提交（two-phase commit）[^mon-20-2]的事务内部结构。两阶段提交使事务即使在提交前发生灾难性故障，也有可能成功提交；

- 后台服务的内部结构；

- 服务器统计信息；

- 以及其他内容。

> [!NOTE]
> **缓冲区缓存**
>
> 本书在这里以及后文中所说的“缓冲区缓存”，特指称为 shared buffers 的共享缓冲区缓存。如果叙述中提到本地缓冲区缓存，会明确说明。

除了主要共享内存外，在某些操作并行执行时，还可能创建较小的共享内存段，供辅助进程相互通信。这些内存段的存在时间受辅助进程生命周期限制，其大小取决于需要传输的数据量。

无论数据区域是本地区域还是共享区域，处理用户数据的方式大致相同。数据库管理系统使用分页的数据组织方式，采用固定大小的*页*（page）（默认大小为 8 KB），这决定了输入/输出的最小单位。执行数据读取或修改操作时，进程会检查所需数据是否存在于缓冲区中，无论该缓冲区是本地的还是共享的。如果数据<span id="pdf-page-21" class="pdf-anchor"></span>存在，这就算一次成功的访问（hit）。如果缓存中没有数据，就需要从磁盘把数据加载到缓存中才能继续工作，这称为缓存未命中（miss，也就是 read）。如果此前访问过这些数据，它们可能位于操作系统的页缓存（page cache）中，从那里取出数据会比从主存储读取更快。更新（`INSERT`、`UPDATE`、`DELETE`）时，缓存中的页会发生变化；这样的页称为脏页（dirtied），表示需要把它们同步到主存储中的数据文件。通常由两个后台进程——检查点进程（checkpointer）和后台写入进程（background writer）——负责同步页面，但某些情况下后端进程也可能执行这项工作。如果进程需要读取缓存中不存在的页面，而从磁盘读取这些页面又需要空闲缓冲区，但缓存中同样没有空闲缓冲区，就会出现这种情况。为了给目标页面腾出缓冲区，进程会开始搜索并淘汰长时间没有被访问的页面。找到的页面可能是脏页；这时进程必须先将它同步（written），然后才能释放缓冲区。总的来说，这不是一项成本很低的操作，尤其是在需要频繁执行时。

总共有四种可能情况：

- hit：最快、成本最低，但仍然不是免费的操作，因为页面位于共享内存中；

- miss，也就是 read：成本更高的操作，页面最好能从页缓存中读取，最差的情况则必须从磁盘读取；

- dirtied：同样是成本较高的操作，因为页面随后还需要同步到主存储；

- written：另一种成本较高的操作；页面不能等待后台延迟同步，而必须由后端进程（执行查询的进程）立即同步。

上述工作模型对共享缓存和本地缓存都适用。使用普通表和临时表时，缓存中的页面始终与磁盘上的表文件相关联。使用工作内存时，在工作内存的限制（参数 *work\_mem*）尚未超出之前，并不存在这样的文件。超过限制后，系统会创建一个临时文件，并在操作完成后删除。维护操作（自动清理、创建索引）缺少工作内存时，数据库管理系统会重复使用已经分配的内存，而不会创建额外文件。

从性能角度看，绝大多数数据位于内存中、每次访问都能在缓存中找到页面，是非常理想的情况。如果经常需要从主存储读取数据，或者为了给新页面腾出缓冲区而淘汰脏页，情况就会更差。

### 预写式日志——Write-Ahead Log {#mon-section-star-19}

在返回修改数据的查询结果之前，数据库管理系统会把这些变更写入*预写式日志*（Write-Ahead Log）。你还可能看到*事务日志*这一称呼，但更常用的是缩写 WAL。写入日志是为了保证可靠性，保存所有数据变更的顺序，并支持数据库管理系统异常终止后的恢复。数据库管理系统随后重新启动时，会使用日志重放那些尚未从缓冲区缓存刷入主存储的变更。数据库管理系统的性能也取决于 WAL 日志的处理速度，而这一速度可以通过统计信息了解。

<span id="pdf-page-22" class="pdf-anchor"></span>

### 数据库管理系统的服务器日志 {#mon-section-star-20}

命令执行结束后，客户端会收到执行结果，可以是一组行、一个标签，甚至是一条错误消息。系统还可以执行*日志记录*，即保存命令执行的内部信息。这些信息可能包括客户端信息、查询文本和参数等。日志记录补充了统计信息，也是了解数据库管理系统运行情况的重要信息来源。之后，服务器便准备好接收并执行下一条命令。

### 变更复制 {#mon-section-star-21}

除了处理客户端的主要工作外，数据库管理系统还会执行一些内部任务。为此，系统会运行若干独立进程，并在后台完成这些任务。其中一项任务是基于预写式日志实现的物理*复制*。所有进入日志的数据变更都会由 walsender 进程读取，并通过复制协议发送到副本。在副本上，walreceiver 进程接收日志数据并把内容保存到磁盘。另一个后台进程 startup 会读取收到的日志数据，并在本地数据副本上重放变更序列。这样，主服务器传来的变更就会对副本上执行的查询可见。

除了物理复制，PostgreSQL 还支持逻辑复制。物理复制会传输并应用所有变更，而逻辑复制则允许在单独的表甚至单独的操作层面选择性地传输数据。使用复制可以出于不同目的，例如分散工作负载，或者实现容错并在故障时快速切换。因此，跟踪所有节点的状态非常重要，而统计信息在这里同样能提供帮助。

### 预写式日志归档 {#mon-section-star-22}

与复制相似，WAL 日志也需要归档。从操作系统的角度看，WAL 日志只是一系列称为 WAL 段的文件。当一个 WAL 段完成写入后，专用的 archiver 进程可以把这个段放入*归档*。归档是一个与数据库管理系统分离的<span id="pdf-page-23" class="pdf-anchor"></span>存储，用于备份和灾难恢复。

### 后台数据同步 {#mon-section-star-23}

系统还会执行其他后台任务。其中一项是把缓冲区缓存中已变更的数据同步到主存储。这里会用到两个进程：background writer 和 checkpointer。前者持续查找脏页并将它们写入磁盘；后者，即 checkpointer，负责执行*检查点*。检查点是 WAL 日志中的一个标记，表示在该标记之前的所有变更都已经写入可靠存储。执行检查点时，checkpointer 进程会写入所有脏数据，而 background writer 只会选择性地写入。换句话说，checkpointer 和 background writer 一样，会把缓冲区缓存中的变更同步到磁盘；除此之外，它还会在 WAL 日志中设置一个特殊标记，表示所有脏数据都已经同步。

既然预写式日志记录了所有变更，就会产生一个问题：这段历史应该保留多久？checkpointer 进程给出了答案：设置检查点意味着此前的所有变更都已可靠地写入主存储，早于该检查点的所有日志段都可以删除。因此，预写式日志的规模会大致保持稳定，WAL 段不会不断累积。

### 自动清理 {#mon-section-star-24}

另一项常规后台任务，是清理表和索引中过时的行版本，这个过程称为*自动清理*（autovacuum）。自动清理的必要性，源于数据更新操作及并发访问的实现方式。数据发生变更、多个客户端并发工作时，同一行可能在表中产生多个版本。随着时间推移，最老的行版本会失去效用，可以被删除。删除这些行版本的正是自动清理进程。后台进程 autovacuum launcher 按一定周期（见 *autovacuum\_naptime*）启动 autovacuum worker 工作进程，由它们执行清理。此外，工作进程还可以为规划器收集表中数据质量和数量特征的统计信息。这些统计信息用于构建查询计划。自动清理工作不当，长远来看会对性能产生负面影响；借助活动统计信息，可以跟踪清理工作，也可以跟踪其他后台进程。

总结来说，PostgreSQL 中有许多复杂的进程，它们相互影响、彼此关联。发生问题时，数据库管理员需要了解相关进程是如何运行的。为了跟踪不同事件，数据库管理系统提供了内部工具，在共享内存中的一个特殊<span id="pdf-page-24" class="pdf-anchor"></span>区域记录并保存统计信息。在 PostgreSQL 15 之前，统计信息由独立的 stats collector 进程收集并保存。后端进程通过 UDP 协议把统计信息传给它，然后由它把信息保存到磁盘。从版本 15 开始，统计信息会直接保存在共享内存中[^mon-24-1]，因此不再需要独立的进程。本书将始终利用这些统计信息来理解数据库管理系统的运行方式、识别问题并解决问题。

## 1.3. 统计信息接口 {#mon-section-1-3}

统计信息收集子系统会积累各种可能用于解决问题的信息。要使用这些信息，只需准备一个支持连接 PostgreSQL 的 SQL 客户端即可。psql 就是一个例子，它是 PostgreSQL 的官方客户端。也可以使用包含在高级工具中的客户端，这些工具为数据库管理系统提供了更多操作功能，例如 pgAdmin、DBeaver 或 DataGrip。

所有统计信息都以数据库管理系统的内部数据形式存在，访问它们不需要额外操作。可以通过内部*函数*获取统计信息。不过，直接使用函数并不总是方便，因此系统还通过系统*视图*（view）组织函数返回的统计信息，用户可以对这些视图执行 SQL 查询。这些视图与表类似，但没有实际的数据存储层；用户在操作它们时，几乎可以使用 SQL 的所有功能（写入除外），包括连接、聚合、窗口函数、子查询等。

随着时间推移，用户在处理统计信息时可能会形成一些“常用”查询，并在其中加入额外逻辑：一条查询可能使用多个视图，也可能结合函数；结果还可以被转换和格式化。可以使用 SQL 把这些查询定义为独立视图。以后就不必再查找、回忆或重新编写查询文本，只需查询这个视图即可获得准备好的结果。

PostgreSQL 的一个优势，是可以创建*扩展*（extensions），即安装到数据库管理系统中的特殊模块，用于增加新功能，而不需要修改系统的核心源代码。借助这一机制，开发人员可以补充缺少的功能，用户也可以相对轻松地安装并使用这些功能。有一部分统计信息就是以扩展形式发布的。安装这些扩展后，就可以用新的数据补充统计信息。后续章节还会讨论通过扩展提供的统计信息，届时会特别说明。

<span id="pdf-page-25" class="pdf-anchor"></span>

## 1.4. 统计信息是监控工具的起点 {#mon-section-1-4}

以函数和视图形式提供的统计信息，是构建更高级数据库管理系统监控和观察工具的坚实基础。最简单的做法，是使用同样的视图提供扩展后的统计信息，并以更便于人阅读的方式格式化。例如，各种 SQL 脚本仓库[^mon-25-1]就属于这一类。更复杂的工具可以内置 PostgreSQL 客户端连接，负责收集、处理、传输和提供信息。实际上，几乎所有监控代理和数据库管理员工具都采用了这种方式。这类代理的任务，就是连接数据库管理系统，使用预先准备好的查询获取信息，然后将信息发送到监控系统，或者在自己的用户界面中展示。用户只需启动代理，提供数据库管理系统的连接参数（以及代理可能需要的其他特定运行参数）。这类工具包括 pgAdmin、pgcenter、pg\_activity 和 pgstats[^mon-25-2]。

## 1.5. 统计信息的特点 {#mon-section-1-5}

统计信息接口简单，但也因此带来了一些特点。其中一个主要特点是入门门槛。要使用统计信息，至少需要掌握 SQL 的基础知识，能够编写最简单的 `SELECT` 查询。要使用更复杂的方法，例如从多个视图中提取统计信息，则需要使用连接、子查询和更高级的 SQL 功能。另一个特点，是视图和函数数量很多，这可能使人难以记住视图的名称及其内容。在事故场景中，尤其难以想起这些名称，因为时间可能按秒计算。统计信息的使用方式还会带来另一个问题：用户必须以命令行方式工作，即编写并发送查询，取得并分析结果，必要时再重复这一过程。通常可以通过建立脚本仓库来弥补这一不足，把脚本随时放在手边，或者预先安装到数据库中。

除了 psql 之外，PostgreSQL 没有提供自己的高级工具来操作统计信息和视图。正因为缺少这样的工具，才出现了大量面向数据库管理员的第三方统计信息工具。每位作者都有自己展示统计信息的方式，这也导致工具和功能呈现出很大的多样性。

<span id="pdf-page-26" class="pdf-anchor"></span>

统计信息的另一个特点，甚至可以说是一个缺点，是没有关于服务器日志的统计信息。要处理日志，用户必须能够访问日志文件。它们通常位于运行数据库管理系统的操作系统中。某些扩展试图解决这个问题，但遗憾的是，它们没有得到 PostgreSQL 开发人员的官方支持，因此传播范围较小，使用起来也更复杂。

## 1.6. 测试环境 {#mon-section-1-6}

附录中给出了测试环境的说明和部署指南。全书的实践任务和示例都会使用这个测试环境。我们将以它为基础演示统计信息的使用方法，并解释重要特点。对于希望进行实验、把书中内容付诸实践并超越正文范围的好奇读者，测试环境也会很有帮助。如果愿意，也可以跳过测试环境的安装，这不会明显影响对书中内容的理解。

<span id="mon-section-star-25" class="pdf-anchor"></span>

## 小结 {#mon-p26-h364}

- PostgreSQL 的内部结构复杂，由不同的子系统组成。

- 在运维过程中，始终存在发生各种问题的风险。

- 活动统计信息能够跟踪数据库管理系统的运行，并让管理员有机会提前发现问题。

- 可以通过 SQL 函数和视图获取统计信息。

- 要使用统计信息，需要掌握 SQL 语言。

- 统计信息接口的极简特性，促成了面向数据库管理员的众多工具。

- 可以使用测试环境来研究统计信息的实践层面。

<span id="pdf-page-27" class="pdf-anchor"></span>

[^mon-17-1]: [en.wikipedia.org/wiki/ACID](<https://en.wikipedia.org/wiki/ACID>)
[^mon-18-1]: [www.postgrespro.ru/docs/postgresql/current/sql-commands](<https://www.postgrespro.ru/docs/postgresql/current/sql-commands>)
[^mon-20-1]: [postgrespro.ru/docs/postgrespro/current/sql-savepoint](<https://postgrespro.ru/docs/postgrespro/current/sql-savepoint>)
[^mon-20-2]: [postgrespro.ru/docs/postgrespro/current/sql-prepare-transaction](<https://postgrespro.ru/docs/postgrespro/current/sql-prepare-transaction>)
[^mon-24-1]: [git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=5891c7a8ed8f2d3d577e7eea34dacff12d7b6bbd](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=5891c7a8ed8f2d3d577e7eea34dacff12d7b6bbd>)
[^mon-25-1]: [github.com/dataegret/pg-utils/tree/master/sql](<https://github.com/dataegret/pg-utils/tree/master/sql>)
[^mon-25-2]: [wiki.postgresql.org/wiki/Monitoring](<https://wiki.postgresql.org/wiki/Monitoring>)
