Chapter 05

配置与资源:每个参数对应一个机制

前四章每讲一个机制都点到一个参数——work_mem 之于 hash join、random_page_cost 之于扫描选择、maintenance_work_mem 之于 VACUUM。这一章把它们收拢成一张配置图,每个旋钮都对应前面某个机制。

本章你将建立的心智模型

  • 配置心法:调一个参数前,先问它影响哪个机制——优化器、内存、WAL 还是 VACUUM
  • 内存四件套(shared_buffers / work_mem / maintenance_work_mem / effective_cache_size)各喂哪个机制
  • WAL 与 checkpoint:先写日志再刷数据页,把脏页刷盘从尖峰摊成平缓
  • 代价参数与 I/O:random_page_cost、effective_io_concurrency 直接改优化器的扫描选择
  • 连接成本与连接池:每个连接是一个进程,会放大 work_mem,所以靠池子复用而非堆 max_connections
  • 一份起点配置,加一句硬约束:改完回到第 01 章的测量循环复测

5.1配置心法:别抄神奇配置

调一个参数前,先说出它影响的是优化器、内存、WAL 还是 VACUUM 哪个机制——说不出来就别调。

为什么先讲心法

网上流传的"神奇 postgresql.conf"之所以危险,是因为它把参数当成孤立的加速旋钮:抄一组数字粘进去,期待变快。但每个参数都只是前四章某个机制的开关——work_mem 控制 hash/sort 节点的内存,random_page_cost 控制优化器对随机读的估价,maintenance_work_mem 控制 VACUUM 的工作区。脱离机制谈数值,等于在第 01 章那个"黑暗里拧旋钮"的反面教材里加速。没有放之四海皆准的配置:同一组值在 16GB 的 OLTP 机器上合理,在 256GB 的数仓上就是浪费。本章每个参数都先连回机制,再给数值。

这张图是本章的骨架:左边是可调参数,右边是前四章讲过的机制。调任何一个参数,都先在图上找到它指向的那个机制,问一句"这是要影响这个机制的什么行为"。

shared_buffers / work_mem random_page_cost autovacuum_* max_wal_size 缓存与排序 02 扫描选择 顺序 vs 索引 04 VACUUM WAL 与刷盘 参数(旋钮) 机制(前四章)
图 5.1参数到机制的映射。注意:右列每个机制在前面都有出处——random_page_cost 指向 02 章的扫描选择,autovacuum_* 指向 04 章的 VACUUM。看不到一个参数连回哪个机制,就没有理由动它。

5.2内存四件套

四个内存参数喂四个不同的机制:页缓存、单节点排序/哈希、维护操作、优化器对缓存的认知。

把"内存"当成一个旋钮去调,是最常见的误区。PostgreSQL 的内存分四块,各喂一个机制,调错地方既不提速,还会把内存推向爆掉。逐个拆开:

shared_buffers — PostgreSQL 自己的页缓存

shared_buffers 是 PostgreSQL 在共享内存里维护的页缓存,所有后端进程共用。一个数据页被读进来后留在这里,下次命中就不必再碰磁盘。这正是 01 章 Buffers: shared hit 里 hit 的来源:hit 多说明页在 shared_buffers 命中,read 多说明缓存没接住、落到了磁盘。默认 128MB 对生产机偏小;常见起点是物理内存的 25%。再大未必更好——PostgreSQL 同时也依赖操作系统的页缓存(见 effective_cache_size),两层缓存不必都给满。

work_mem — 每个排序 / 哈希 / 位图节点的内存

work_mem 是 02 章 hash join / sort 节点能用的内存上限。一个 Hash Join 要把小表建成哈希表,如果哈希表装不进 work_mem,就会落盘——执行计划里出现 external merge Disk 或 Batches: > 1,速度断崖式下跌。默认 4MB,排序稍大的结果集就溢出。

陷阱 · work_mem 是 per-node per-connection

work_mem 不是每条查询一份,而是每个排序/哈希/位图节点各一份。一条带多个 JOIN + 排序的复杂查询会同时开几个节点,每个都吃一份 work_mem,单条查询就用上数倍。再乘以并发连接数,峰值内存 = work_mem × 每条查询的节点数 × 并发连接数,会被迅速放大。所以它不能盲目调大——这正是 5.5 节连接池要解决的放大问题。

maintenance_work_mem — 维护操作的工作区

maintenance_work_mem 是 04 章 VACUUM、CREATE INDEX、ALTER TABLE 等维护操作的工作区。VACUUM 用它缓存待回收的 dead tuple 标识,越大则一遍扫过的死元组越多、大表 VACUUM 越快。默认 64MB。它只在维护时占用,且同一时刻这类操作通常不多,所以可以比 work_mem 大方得多。

CURRENCY · PG 17 起取消 1GB 上限

PG 17(2024-09)之前,VACUUM 对 maintenance_work_mem 的有效利用封顶在 1GB,设更大对 VACUUM 也无效。PG 17 起这个上限被移除,VACUUM 能用满分配的值——对超大表,可放心把它调到几个 GB 来缩短 VACUUM 时间。

effective_cache_size — 给优化器的"缓存有多大"提示

effective_cache_size 是四件套里唯一不分配任何内存的:它只是告诉优化器"操作系统页缓存 + shared_buffers 加起来大约有多少可用",作为 02 章代价估算的输入。这个数越大,优化器越相信"重复的索引扫描多半会命中缓存",从而更愿意选索引扫描而非顺序扫描。它不影响实际内存占用,设错了只会让优化器估价偏移,不会 OOM。常设为物理内存的 50–75%。

表 5.1 · 内存四件套 × 各喂哪个机制
参数作用默认(PG18)起点建议(16GB 机)影响哪个机制
shared_buffersPostgreSQL 自己的页缓存(真实分配)128MB物理内存 25%(~4GB)页缓存命中(01 BUFFERS)
work_mem每个排序/哈希/位图节点的内存4MB16–32MB,视并发hash join / sort(02)
maintenance_work_memVACUUM / 建索引的工作区64MB1GB(大表可更大)VACUUM(04)
effective_cache_size给优化器的缓存量提示(不分配)4GB物理内存 50–75%(~12GB)代价估算(02)
想一想

把 effective_cache_size 从 4GB 调到 12GB,机器的实际内存占用会增加吗?这个改动主要影响优化器的什么行为?

展开答案(先想一步再点)

实际内存占用不变——effective_cache_size 不分配任何内存,只是一个告诉优化器"缓存大约有多大"的数字。

它影响的是优化器对索引扫描代价的估算:数字越大,优化器越假设重复的随机读会命中缓存、代价更低,因而更倾向选索引扫描而非顺序扫描。这是纯粹改"优化器的认知输入",对应 02 章那条"调优=改输入"的主线。

5.3WAL 与 checkpoint

WAL 先写日志再写数据页,保证崩溃可恢复;checkpoint 周期性把脏页刷盘——调参的目标是把这次刷盘从尖峰摊成平缓。

机制:为什么要 WAL 和 checkpoint

修改一个数据页时,PostgreSQL 不立刻把数据页写盘,而是先把"改了什么"追加写进 WAL(预写日志,write-ahead log),再在内存里改页(此时页变"脏")。这样崩溃后能用 WAL 重放恢复。但脏页不能永远只在内存——checkpoint 周期性地把累积的脏页批量刷到数据文件,并标记"这个点之前的 WAL 可以回收了"。问题就出在这次批量刷盘上。

痛点:checkpoint 一触发,大量脏页要在短时间内集中写盘,造成 I/O 尖峰——磁盘被打满,期间所有查询都变慢。这对应 01 章 wait events 里的 LWLock(等 WAL / 缓冲区相关的内部锁)。两个参数决定 checkpoint 多频繁、多集中:

  • max_wal_size(默认 1GB):WAL 累积到这个量会强制触发 checkpoint。设太小,WAL 很快写满,checkpoint 被频繁逼出,刷盘尖峰一个接一个。调大它(如 4GB)能让 checkpoint 主要由 checkpoint_timeout(默认 5 分钟)按时间从容触发,而不是被 WAL 体积逼着仓促刷。
  • checkpoint_completion_target:把一次 checkpoint 的刷盘工作摊到整个周期的多大比例里完成。设 0.9 表示用 90% 的周期慢慢刷,而不是一上来全力写——尖峰被摊平成平缓的斜坡。
CURRENCY · 0.9 已是默认

很多旧文章还在教"把 checkpoint_completion_target 从 0.5 改到 0.9"。自 PG 14 起默认就是 0.9,不必再手动改。这条仍值得知道,因为它解释了 PostgreSQL 默认行为已经在帮你摊平刷盘;真正需要动的通常是 max_wal_size。

尖峰 max_wal_size 小 + 集中刷 I/O 频繁打满 → 查询周期性变慢 摊平 max_wal_size 大 + completion_target=0.9 同样的脏页量,摊成平缓的写入
图 5.2WAL 到 checkpoint 刷盘的时间线。注意:两条线刷的脏页总量相同,差别只在分布。调大 max_wal_size 减少 checkpoint 次数,completion_target=0.9 把每次刷盘拉长——尖峰被摊成斜坡,磁盘不再被周期性打满。

5.4代价参数与 I/O

random_page_cost 直接改优化器顺序扫描 vs 索引扫描的切换点;effective_io_concurrency 控制预取并发——两者都把硬件特性喂给优化器。

这一节的参数不分配内存,而是校准 02 章的代价模型,让那套抽象单位贴近你的真实硬件。

random_page_cost — 最值得改的代价参数

random_page_cost 默认 4.0,含义是"随机读一页的代价约等于顺序读一页(seq_page_cost=1.0)的 4 倍"。这个 4 倍是机械盘假设——磁头寻道使随机读远慢于顺序读。但 SSD 和云盘几乎没有寻道代价,随机读和顺序读相差无几,此时 4.0 让优化器系统性高估索引扫描(随机读)的代价,本该走索引却退回顺序扫描。SSD/云盘应把它降到约 1.1。这正是 02 章那条"顺序扫描 vs 索引扫描切换点"背后的旋钮:降低 random_page_cost,切换点左移,优化器更早地愿意走索引。

cost-params.ini ini
# 机械盘默认:随机读贵 4 倍
random_page_cost = 4.0
seq_page_cost    = 1.0

# SSD / 云盘:随机读和顺序读差别很小
random_page_cost = 1.1
seq_page_cost    = 1.0

effective_io_concurrency — 预取并发

effective_io_concurrency 控制 PostgreSQL 一次能并发发起多少个磁盘预取请求,主要作用于 Bitmap Heap Scan 这类需要按位图回表读多页的场景:并发预取多页,而不是读一页等一页。

CURRENCY · 默认从 1 提到 16(PG 18)

PG 18(2025-09)把 effective_io_concurrency 默认从 1 提到 16,并通过新的异步 I/O(io_uring 或 worker 后端)让预取真正生效。旧版本里这个参数对很多平台形同虚设;PG 18 起它实际加速 bitmap heap scan 等的回表预取。SSD 上保持默认或略调高即可。

jit — JIT 编译

jit 控制是否对代价超过 jit_above_cost(默认 100000)的查询做即时编译,把表达式求值编译成机器码。它对扫描海量行的分析型查询有收益,但 JIT 编译本身有固定开销。

CURRENCY · PG 18 默认 on,PG 19 默认 off

jit 在 PG 18 默认 on,但社区已确认 PG 19(2026 beta)默认改回 off——因为对大量 OLTP 短查询,JIT 编译开销常常超过它省下的执行时间,反而变慢。OLTP 负载建议显式关掉(jit = off);只在确认有重分析型查询、且实测 JIT 有正收益时再开。

5.5连接管理与连接池

每个连接是一个独立后端进程,有固定内存开销且会放大 work_mem;答案是连接池复用少量连接,不是堆 max_connections。

机制:一个连接的真实成本

PostgreSQL 对每个客户端连接 fork 一个独立后端进程(不是线程)。每个进程有固定的内存与栈开销,内核还要为它们做进程调度。更关键的是 5.2 节那个放大效应:每个后端跑查询时,每个排序/哈希节点各吃一份 work_mem。连接越多,work_mem 被乘的次数越多,峰值内存越危险。

痛点:遇到"连接不够用"就把 max_connections(默认 100)调到几千,不是答案。几千个后端进程带来沉重的调度开销,还把 work_mem 的放大系数推到内存撑不住的地步——5.6 的挑战题就是这个事故的算术。

解法:用连接池(典型是 PgBouncer)的 transaction 模式。应用连到池子,池子只对后端维持一小撮真实连接(如几十个),在事务粒度上把它们复用给成百上千的应用连接。后端进程数被压到很小,work_mem 的放大系数随之可控。

表 5.2 · 直接调大 max_connections vs 连接池
做法后端进程数对 work_mem 放大的影响适用
把 max_connections 调到几千等于连接数,极多放大系数巨大,峰值内存易 OOM几乎从不推荐
连接池(PgBouncer,transaction 模式)压到几十个放大系数小且可控高并发 OLTP 的标准做法
洞察 · 连接数和内存是一道乘法题

把 5.2 和本节接起来:峰值内存 ≈ work_mem × 每条查询的节点数 × 并发后端数。max_connections 调大会把最后一个因子推高,连乘出危险的峰值。连接池从因子上动手——压低后端数,而不是寄望于把 work_mem 调到很小(那又会拖慢排序)。这就是"调一个参数前先想它牵动哪个机制"的典型:看似是连接问题,根子在内存放大。

5.6一份起点配置(收拢全章)

给一台 16GB、OLTP 负载的机器一组起点值——每个值都标出它对应前面哪个机制。

把全章收拢成一份可抄的起点配置。注意右列:每个数字旁边都写着它牵动哪个机制、出自哪一章。不带这一列的配置就是 5.1 警告过的"神奇配置"。

表 5.3 · 16GB / OLTP 起点配置 × 对应机制
参数起点值对应机制 / 章节
shared_buffers4GB页缓存,物理内存 25%(01 BUFFERS)
effective_cache_size12GB给优化器的缓存提示,~75%(02 代价估算)
work_mem16–32MB排序/哈希节点,视并发取值(02 hash/sort)
maintenance_work_mem1GBVACUUM / 建索引工作区(04 VACUUM)
random_page_cost1.1SSD/云盘的扫描切换点(02 扫描选择)
max_wal_size4GB降低 checkpoint 频率(5.3 WAL)
checkpoint_completion_target0.9摊平刷盘(PG14 起已是默认)
postgresql.conf · 16GB OLTP 起点 ini
# 内存四件套
shared_buffers = 4GB                  # 物理内存 25%,页缓存
effective_cache_size = 12GB           # ~75%,只是给优化器的提示
work_mem = 32MB                       # per-node per-connection,留意放大
maintenance_work_mem = 1GB            # VACUUM / CREATE INDEX

# 代价参数(SSD / 云盘)
random_page_cost = 1.1                # 默认 4.0 是机械盘假设
# effective_io_concurrency = 16       # PG18 默认已是 16

# WAL / checkpoint
max_wal_size = 4GB                    # 减少 checkpoint 尖峰
checkpoint_completion_target = 0.9    # PG14 起默认,摊平刷盘

# 连接:不堆大,前面放 PgBouncer
max_connections = 100
必读 · 这是起点,不是终点

这份配置只是一个合理的出发点,不是终点。每台机器的内存、磁盘、负载形状都不同,真实最优值只能测出来。改完一定回到 第 01 章的测量循环:跑代表性负载,看 EXPLAIN (ANALYZE, BUFFERS)、wait events、缓存命中率,再对比前后。并且一次只改一个变量——同时改 shared_buffers 和 work_mem,即使变快了也分不清是谁的功劳。配置调优和查询调优共用同一个循环。

物理内存 16GB shared_buffers 4GB · 真实分配 各连接 work_mem 之和 操作系统页缓存 其余 · effective_cache_size 估它 work_mem 段会随并发后端数膨胀 → 这一段失控就 OOM
图 5.316GB 内存的切分。注意:shared_buffers 是固定的真实分配;中间的 work_mem 之和会随并发后端数膨胀(5.5 的放大),这一段失控就 OOM;effective_cache_size 只是用来估"操作系统页缓存那一段有多大",本身不占内存。

§本章 self-check

先合上教程,把答案写下来再对照。这一章的题考的不是数值,而是每个参数连回哪个机制。

  1. effective_cache_size 和 shared_buffers 都和"缓存"有关,但一个分配内存、一个不分配。各自影响哪个机制?调大 effective_cache_size 会增加实际内存占用吗?
  2. 一条复杂查询同时有 3 个 hash/sort 节点,work_mem=32MB,有 50 个并发连接都在跑类似查询。粗算这部分内存的峰值上界,并说明为什么不能靠无脑调大 work_mem 解决慢排序。
  3. 把 random_page_cost 从 4.0 降到 1.1,会让优化器更倾向顺序扫描还是索引扫描?它改的是 02 章的哪个机制?为什么 SSD 上该降?
  4. 给一台 32GB、SSD、OLTP 的机器写出 shared_buffers、effective_cache_size、random_page_cost、jit 四个值,并各用一句说出它对应前面哪个机制。
答案(先做完再展开)
  1. shared_buffers 是 PostgreSQL 真实分配的页缓存(影响 01 的缓存命中);effective_cache_size 不分配内存,只是给优化器的"缓存有多大"提示,影响 02 的索引扫描代价估算。调大 effective_cache_size 不增加实际内存占用,只让优化器更倾向走索引。
  2. 上界约 32MB × 3 × 50 = 4800MB ≈ 4.7GB(work_mem × 节点数 × 并发数)。它是 per-node per-connection,无脑调大会把这个乘积推到 OOM;慢排序的正解是减少并发后端(连接池)或针对性提升单查询效率,而不是全局拉高 work_mem。
  3. 更倾向索引扫描:降低随机读的估价,使索引扫描(随机读)的代价更低,02 章的扫描切换点左移。SSD 几乎无寻道代价,随机读和顺序读相差无几,默认 4.0 的机械盘假设会系统性高估索引扫描代价,所以该降到约 1.1。
  4. 示例:shared_buffers=8GB(25%,页缓存)、effective_cache_size=24GB(~75%,给优化器的代价估算提示)、random_page_cost=1.1(SSD 的扫描切换点)、jit=off(OLTP 短查询,JIT 编译开销不划算)。
进阶挑战 · 刚好够不着

一台 64GB 机器报 out of memory,算出放大并给修复方向

一台 64GB RAM、高并发 OLTP 的库频繁报 out of memory。已知 max_connections=2000、work_mem=256MB,典型查询带 2 个 hash/sort 节点。shared_buffers 设了 16GB。问:仅 work_mem 这部分在最坏情况下会吃掉多少内存?和 64GB 物理内存对比说明问题在哪。给出两条修复方向。

提示(卡住再展开)

关键是记住 work_mem 是 per-node per-connection,要乘两次。最坏估算:256MB × 2 节点 × 2000 连接 = 1,024,000MB ≈ 1000GB——远超 64GB 物理内存(还没算 shared_buffers 占的 16GB)。即使只有一部分连接同时跑这类查询,峰值也轻松击穿内存,于是 OOM。

两条修复方向:① 降 work_mem(如降到 16–32MB,把放大系数压下来);② 上连接池(PgBouncer transaction 模式),把真实后端从 2000 压到几十个——这同时砍掉乘式里最大的那个因子。两者通常一起做。注意 max_connections 调大不是解法,它正是把最后那个因子推爆的原因。