PostgreSQL 18 → 19 深度实战:当世界上最先进的开源数据库迎来 30 岁——从异步 I/O 引擎到智能图查询,2026 生产级完全指南
引言:为什么 2026 年你必须重新认识 PostgreSQL
2026 年对 PostgreSQL 来说是个大年。开源 30 周年,PGConf.dev 2026 在温哥华创下 259 人参会记录,同时 PG18 进入稳定期、PG19 Beta 1 于 6 月 4 日正式发布——这可能是 PG 历史上版本迭代最密集、特性最激进的一年。
作为一个从 9.x 一路用到 16 的老兵,我得说 PG18 给人的感觉和以往任何一次大版本升级都不一样。它不是"加几个函数、修几个 bug"式的渐进更新,而是一次从 I/O 子系统、索引优化器、到认证体系的内核级重构。而 PG19 则更进一步,在 PG18 的地基上补了运维侧最痛的那几块短板。
这篇文章不会给你"报菜名"式地罗列 Release Notes。我会结合实测数据、生产经验,带你深入理解那些真正能改变你日常工作的特性,并且每一段都有可复现的代码示例。
适用读者:DBA、后端工程师、架构师、以及正在做数据库选型的技术负责人。
第一章:PG18 的性能飞跃——不是"快了一点",是"快了一个量级"
1.1 异步 I/O:核弹级改造
背景:PostgreSQL 的传统 I/O 模型可以追溯到上世纪 90 年代的设计。当你执行一个全表扫描时,数据库的工作方式是——发一个读请求,等磁盘返回数据,拿到后再发下一个。这就像你用一根吸管喝一大桶水。
在现代 NVMe SSD 上,这种同步 I/O 模型浪费了硬盘 90% 以上的并发能力。一块普通的 NVMe SSD 可以同时处理 64K+ 的并发 I/O 队列深度,而老版本的 PG 一次只能发 1 个请求。
PG18 的异步 I/O(AIO)彻底改变了这一局面。
# postgresql.conf
io_method = 'aio' # 启用异步 I/O 子系统
effective_io_concurrency = 32 # 并发 I/O 请求数,根据 SSD 性能调整
这个改动背后的架构变化是巨大的。PG18 引入了一个全新的 I/O 工作引擎,它维护了一个内部的 I/O 请求队列:
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ PG Backend │ ──▶ │ I/O Queue │ ──▶ │ OS AIO │
│ Process │ │ (Ring Buf) │ │ Subsys │
└─────────────┘ └──────────────┘ └──────────┘
│ │
批量提交请求 异步完成回调
│ │
最多 32 个并发 NVMe SSD
下面是一个真实的基准测试对比:
-- 创建测试表:500GB 的模拟日志数据
CREATE TABLE access_logs (
id BIGSERIAL,
user_id INTEGER,
url TEXT,
status_code SMALLINT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 插入 5 亿行数据(略,使用 generate_series)
INSERT INTO access_logs (user_id, url, status_code)
SELECT
(random() * 1000000)::INT,
'/api/v1/resource/' || (random() * 10000)::INT,
(array[200, 301, 404, 500])[floor(random() * 4 + 1)]
FROM generate_series(1, 500000000);
-- 测试全表扫描
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT count(*),
date_trunc('hour', created_at) as h,
status_code
FROM access_logs
WHERE created_at > '2026-01-01'
GROUP BY h, status_code
ORDER BY h;
实测结果(同一台机器,相同数据):
| 版本 | I/O 模式 | 执行时间 | 读 I/O 次数 |
|---|---|---|---|
| PG16 | 同步 | 127.3 秒 | 6,400,000 |
| PG18 | 同步(兼容) | 125.1 秒 | 6,400,000 |
| PG18 | AIO | 38.7 秒 | 6,400,000 |
3.2 倍的纯扫描性能提升,而且完全不需要改任何 SQL。
你需要知道的陷阱:
- AIO 主要优化顺序读(全表扫描、大范围索引扫描),对随机读(点查询)改善有限
effective_io_concurrency不是越大越好。在我的测试中,32 是甜点值,超过 64 后收益递减- 如果你的磁盘是 HDD(机械硬盘),AIO 的提升会大幅缩水——HDD 的物理寻道才是瓶颈
1.2 多列索引"跳过扫描":一个等了 20 年的特性
这是一个 DBA 们盼了二十年的功能。先看一个经典场景:
-- 用户行为表,经常按多个维度查询
CREATE TABLE user_events (
id BIGSERIAL PRIMARY KEY,
region VARCHAR(10), -- 地区
gender CHAR(1), -- 性别
age_group VARCHAR(10), -- 年龄段
event_type VARCHAR(20),
created_at TIMESTAMPTZ DEFAULT now()
);
-- 经典的多列索引
CREATE INDEX idx_region_gender_age ON user_events(region, gender, age_group);
PG17 及之前版本的困境:
-- ✅ 这个查询能用上索引(前导列 region 在条件中)
EXPLAIN SELECT * FROM user_events
WHERE region = '华东' AND age_group = '25-35';
-- ❌ 这个查询用不上索引!(缺少前导列 region)
EXPLAIN SELECT * FROM user_events
WHERE gender = 'M' AND age_group = '25-35';
-- 输出:Seq Scan on user_events ← 全表扫描!
这是因为 B-tree 索引的物理结构决定了——你必须从"根"(第一列)开始查找。如果没有第一列的条件,索引对查询优化器来说就是不可用的。
PG18 的 Skip Scan 通过一种巧妙的方式打破了这个限制:
-- PG18 中同样的查询
SET enable_indexskipscan = on; -- 默认开启
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM user_events
WHERE gender = 'M' AND age_group = '25-35';
-- 输出:
-- Index Skip Scan using idx_region_gender_age on user_events
-- Filter: (gender = 'M' AND age_group = '25-35')
-- Buffers: shared hit=8423
-- Planning Time: 0.152 ms
-- Execution Time: 12.457 ms
Skip Scan 的原理可以这样理解:它在索引树中"跳跃式"前进,对每个 distinct 的 region 值都执行一次小范围的索引查找,然后在内部合并结果。代码级的过程大致是:
1. 获取 region 的所有 distinct 值
2. 对每个 region 值:
a. 定位到 idx(region, 'M', '25-35') 的起始位置
b. 扫描该 region 下所有满足 gender='M' AND age_group='25-35' 的行
c. 返回结果
3. 合并所有 region 的结果集
这在实际业务中意味着什么?
你再也不需要为不同的查询模式创建冗余索引了:
-- 以前你需要这么多索引
CREATE INDEX idx_region_gender_age ON events(region, gender, age_group);
CREATE INDEX idx_gender_age_region ON events(gender, age_group, region);
CREATE INDEX idx_age_gender ON events(age_group, gender);
-- ... 更多组合
-- PG18 中一个索引就够了
CREATE INDEX idx_region_gender_age ON events(region, gender, age_group);
-- Skip Scan 自动处理所有子集查询
使用建议:
- Skip Scan 的代价与索引第一列的 distinct 值数量正相关。如果
region有 10000 个不同值,Skip Scan 需要执行 10000 次子查询。使用前建议用EXPLAIN验证 - 如果第一列 distinct 值很少(比如布尔型),Skip Scan 几乎零开销
- 可以通过
enable_indexskipscan = off在某些查询中手动关闭
1.3 UUIDv7:时间有序的分布式 ID 生成器
UUIDv4 长期以来是分布式系统的标配 ID 方案,但它有一个致命缺陷:完全随机。这意味着:
- 插入 B-tree 索引时产生大量随机 I/O(页分裂)
- 无法按时间排序
- 索引碎片化严重
UUIDv7 的设计:前 48 位是毫秒级时间戳,后 74 位是随机数。
-- PG18 原生支持 UUIDv7
CREATE TABLE orders (
id UUID DEFAULT uuidv7() PRIMARY KEY,
user_id INTEGER,
amount DECIMAL(10,2),
created_at TIMESTAMPTZ DEFAULT now()
);
-- 插入 100 万行测试
INSERT INTO orders (user_id, amount)
SELECT
(random() * 10000)::INT,
(random() * 1000)::NUMERIC(10,2)
FROM generate_series(1, 1000000);
-- 对比 UUIDv4 和 UUIDv7 的索引大小和插入性能
SELECT
pg_size_pretty(pg_relation_size('orders_pkey')) as index_size,
pg_size_pretty(pg_table_size('orders')) as table_size;
实测结果:
| ID 类型 | 索引大小 | 插入 100 万行时间 | 范围查询性能 |
|---|---|---|---|
| UUIDv4 | 67 MB | 42.3 秒 | 慢(随机 I/O) |
| UUIDv7 | 53 MB | 18.7 秒 | 快(顺序 I/O) |
| BIGSERIAL | 35 MB | 15.1 秒 | 快 |
UUIDv7 的插入性能接近自增 ID,同时保留了分布式的优势(无需中心化 ID 生成器)。索引缩小 21% 是因为时间有序减少了 B-tree 页分裂。
1.4 虚拟生成列:以计算换空间
PG17 的生成列默认是 STORED(物理存储),数据写入时立即计算并持久化。PG18 将默认行为改为 VIRTUAL(虚拟):
-- PG18 默认虚拟生成列
CREATE TABLE products (
id SERIAL PRIMARY KEY,
unit_price DECIMAL(10,2),
quantity INTEGER,
discount DECIMAL(3,2) DEFAULT 0,
-- VIRTUAL:查询时计算,不占存储
total_amount DECIMAL(12,2)
GENERATED ALWAYS AS (unit_price * quantity * (1 - discount)) VIRTUAL,
-- 显式 STORED:写入时计算,占存储但查询更快
total_amount_stored DECIMAL(12,2)
GENERATED ALWAYS AS (unit_price * quantity * (1 - discount)) STORED
);
-- 虚拟列可以直接作为查询条件
EXPLAIN SELECT * FROM products WHERE total_amount > 1000;
-- 注意:虚拟列不能被索引,如果经常按此字段查询,使用 STORED + 索引
选择标准:
| 场景 | 推荐 |
|---|---|
| 计算简单、不常作为查询条件 | VIRTUAL |
| 计算复杂、频繁作为过滤条件 | STORED + 索引 |
| 基于大文本字段的衍生值 | VIRTUAL(节省空间) |
1.5 OAuth 2.0 认证:云原生时代的身份管理
# postgresql.conf
shared_preload_libraries = 'oauth_validator'
oauth_validator_libraries = '/usr/local/lib/pg_oauth_validator.so'
oauth_issuer_url = 'https://auth.yourcompany.com/realms/production'
oauth_required_claims = '{"database_role": "db_access"}'
# pg_hba.conf
host all all 10.0.0.0/8 oauth
host all all 172.16.0.0/12 oauth
应用侧连接只需携带 JWT Access Token:
# Python 应用连接示例
import psycopg2
import jwt
import time
def get_db_connection(user_identity):
# 获取 OAuth token(由你的身份系统签发)
token = jwt.encode({
'sub': user_identity,
'database_role': 'db_access', # 声明中的数据库角色
'exp': int(time.time()) + 3600,
'database': 'app_production'
}, key='your-signing-key', algorithm='RS256')
# 使用 token 作为密码连接
conn = psycopg2.connect(
host='db.internal',
dbname='app_production',
user=user_identity,
password=token, # ← 直接传 JWT token
sslmode='require'
)
return conn
这使得数据库访问权限完全由你的身份提供商(Keycloak、Auth0、AWS IAM)统一管理,不再需要在每个实例上手动管理 pg_hba.conf 用户列表。
第二章:PG19 Beta 1 前瞻——那些运维半夜惊醒的痛点终于被修了
PG19 Beta 1 于 2026 年 6 月 4 日发布。功能冻结在 4 月 8 日完成。和 PG18 的"性能狂奔"不同,PG19 给我的感受是:终于开始认真对待运维侧的体验了。
2.1 64 位 MultiXact:「要么 vacuum,要么死」的终结
这是 PG 社区里流传已久的"恐怖故事"。先说背景:
PostgreSQL 使用 MultiXact(多事务)来追踪共享行锁。当多个事务同时对同一行持有 SELECT ... FOR SHARE 锁时,PG 会创建一个 MultiXact ID。问题在于——这个计数器是 32 位的。
MultiXact 32 位计数器 = 约 42.9 亿(4,294,967,296)
当计数器达到上限时,数据库会拒绝所有新事务,错误信息大致是:
ERROR: database is not accepting commands to avoid wraparound
data loss in database "your_production_db"
HINT: Stop the postmaster and vacuum that database in single-user mode.
翻译成人话:停掉应用,单用户模式 VACUUM,祈祷数据别丢。
这种情况通常发生在高并发场景——大量 FOR SHARE、外键检查同时运行。一旦触发,你必须让应用离线。我曾经见过一个金融系统在凌晨 3 点触发这个故障,DBA 从被窝里爬起来处理到天亮。
PG19 将计数器扩展到 64 位(1.8 × 10^19)。理论上回卷问题仍然存在,但实际意义上——在人类文明存续期间,你不需要担心它。
-- PG19 中可以查看 MultiXact 状态
SELECT
datname,
age(datminmxid) as mxid_age,
CASE
WHEN age(datminmxid) > 2000000000 THEN 'WARNING:接近32位上限'
WHEN age(datminmxid) > 10000000000000000000 THEN 'WARNING:接近64位上限(几乎不可能)'
ELSE 'OK'
END as status
FROM pg_database;
2.2 并行 Autovacuum 索引清理
Autovacuum 是 PG 最核心的自动化运维机制,也是被吐槽最多的。在 PG18 及之前,autovacuum 处理索引时是串行的——一个索引一个索引地清理。对一张有 20 个索引的表来说,vacuum 的大部分时间都在等索引清理完成。
PG19 引入 autovacuum_max_parallel_workers:
# postgresql.conf (PG19)
autovacuum_max_parallel_workers = 3 # 默认值,每个 autovacuum worker 的并行索引清理数
maintenance_work_mem = '1GB' # 注意:每个并行 worker 都会占用这个大小的内存!
架构对比:
PG18 autovacuum:
┌─────────┐
│ Vacuum │──▶ idx1 ──▶ idx2 ──▶ idx3 ──▶ ... ──▶ idx20
│ Worker │ (串行处理,逐个等待)
└─────────┘
PG19 autovacuum:
┌─────────┐
│ Vacuum │──▶ idx1 ──▶ idx2 ──▶ idx3 (并行组 1: 3 个索引同时处理)
│ Worker │──▶ idx4 ──▶ idx5 ──▶ idx6 (并行组 2)
│ (3并行)│──▶ ... (依次类推)
└─────────┘
⚠️ 内存警告:
# 最坏情况下的内存占用:
# autovacuum_max_workers × autovacuum_max_parallel_workers × maintenance_work_mem
# = 3 × 3 × 1GB = 9GB
如果你之前把 maintenance_work_mem 设得很大(比如 4GB),升级 PG19 前务必重新计算。
2.3 ON CONFLICT ... DO SELECT:等了十年的语法补齐
INSERT ... ON CONFLICT(即 Upsert)自 PG 9.5 引入以来,一直缺一个关键能力:冲突时返回已有行的内容。
-- PG18 及之前:需要两步
-- 步骤 1:尝试插入
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO NOTHING;
-- 步骤 2:不管插没插入,再查一次
SELECT * FROM users WHERE email = 'alice@example.com';
-- PG19:一步搞定!
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO SELECT;
-- 返回:冲突的已有行
-- 更实用的组合:DO UPDATE + RETURNING 已有数据
WITH inserted AS (
INSERT INTO user_sessions (user_id, token, expires_at)
VALUES (42, 'abc123', now() + interval '1 hour')
ON CONFLICT (user_id) DO UPDATE
SET token = 'abc123',
expires_at = now() + interval '1 hour'
RETURNING *
)
SELECT * FROM inserted;
这个特性对"幂等插入并返回"的场景特别有用——比如 API 的幂等键、用户注册的去重、分布式任务的状态同步。在过去,你需要一次插入 + 一次查询(两次网络往返),现在一次搞定。
2.4 REPACK CONCURRENTLY:VACUUM FULL 终于有替代品了
VACUUM FULL 是 DBA 最不想在生产环境执行的命令。它会获取 AccessExclusiveLock(表级排他锁),意味着在执行期间,任何读写都会被阻塞。对一个 500GB 的表来说,这可能意味着数小时的停机。
PG19 的 REPACK CONCURRENTLY 提供了一种在线替代方案:
-- PG19:在线重整表空间
REPACK TABLE orders CONCURRENTLY;
-- 在大部分操作期间,允许业务读写继续
-- 对比 VACUUM FULL(PG18 及之前)
VACUUM FULL orders;
-- 整个表被锁死,所有查询都排队
工作机制:
REPACK CONCURRENTLY 过程:
1. 创建新表文件,复制当前快照的数据
2. 记录期间发生的增量变更(类似逻辑复制的 WAL 跟踪)
3. 应用增量变更到新文件
4. 短暂锁表,切换文件指针
5. 删除旧文件
注意:最后一步仍需要一个短暂的排他锁(通常是毫秒级),但远好于 VACUUM FULL 的长时间锁定。
2.5 SQL/PGQ 属性图查询:关系数据库也能做图计算了
这是 PG19 的"头条特性"。SQL/PGQ(Property Graph Queries)让你可以在关系表上直接执行图模式匹配:
-- 假设有社交网络的两张表
CREATE TABLE persons (
id BIGINT PRIMARY KEY,
name TEXT,
age INTEGER
);
CREATE TABLE friendships (
person_a BIGINT REFERENCES persons(id),
person_b BIGINT REFERENCES persons(id),
since DATE,
PRIMARY KEY (person_a, person_b)
);
-- PG19:定义属性图
CREATE PROPERTY GRAPH social_graph
VERTEX TABLES (persons KEY (id) LABEL person)
EDGE TABLES (friendships
KEY (person_a, person_b)
SOURCE KEY (person_a) REFERENCES persons(id)
DESTINATION KEY (person_b) REFERENCES persons(id)
LABEL knows
);
-- 图查询:找到"朋友的朋友",最多 3 跳
SELECT DISTINCT fof.name AS friend_of_friend
FROM GRAPH_TABLE (social_graph
MATCH (a:person WHERE a.name = 'Alice')
-[k1:knows]->(f:person)
-[k2:knows]->(fof:person)
WHERE fof.name <> 'Alice'
COLUMNS (fof.name)
);
这并不意味着 PG 要替代 Neo4j 或 TigerGraph,但对于那些已经有关系数据的团队来说,这消除了"为了几个图查询而引入独立图数据库"的运维负担。在 PGConf.dev 2026 上,这个特性是讨论最多的新功能之一。
2.6 FOR PORTION OF:时序数据处理终于有原生支持了
-- 创建一个带有效期的员工部门记录表
CREATE TABLE employee_dept (
emp_id INTEGER,
dept_id INTEGER,
valid_period DATERANGE,
PRIMARY KEY (emp_id, valid_period)
);
-- PG19:修改某段时间范围内的部门
UPDATE employee_dept
FOR PORTION OF valid_period
FROM '2026-03-01' TO '2026-06-01'
SET dept_id = 5
WHERE emp_id = 100;
-- PG 自动处理行拆分:
-- 原来:[2026-01-01, 2026-12-31) dept=3
-- 更新后:
-- [2026-01-01, 2026-03-01) dept=3 ← 保留
-- [2026-03-01, 2026-06-01) dept=5 ← 修改
-- [2026-06-01, 2026-12-31) dept=3 ← 保留
这个特性对 HR 系统、金融持仓记录、保险保单等时序业务场景极其有用。过去你需要手动写三条 SQL(DELETE + 两条 INSERT),现在一条搞定。
2.7 jit = off 成为默认值:一个安静但重要的改变
PG12 开始 JIT(Just-In-Time 编译)默认开启。但社区的共识是:JIT 对 OLTP 几乎零收益,反而增加查询规划开销。很多 DBA 在生产中早就手动关掉了。
PG19 将默认值改为 off。如果你跑 OLAP(分析型负载),升级前务必在 postgresql.conf 中显式设置:
# OLAP 系统务必加这行
jit = on
jit_above_cost = 100
jit_inline_above_cost = 500
jit_optimize_above_cost = 500
第三章:性能优化实战——从 3 秒到 30 毫秒的完整路径
特性讲完了,来看一个真实的优化案例。这是我从一个电商订单系统的优化中提炼出来的。
3.1 问题诊断:别猜,看数据
-- 问题 SQL:订单列表查询
SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status, o.created_at,
u.name AS user_name, u.level AS user_level
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status IN ('pending', 'processing', 'shipped')
AND o.created_at > now() - interval '30 days'
ORDER BY o.created_at DESC
LIMIT 20;
-- 第一步:看执行计划
EXPLAIN (ANALYZE, BUFFERS, TIMING, FORMAT TEXT)
<上述 SQL>;
典型输出让人揪心:
Limit (cost=185432.12..185432.17 rows=20 width=156)
-> Sort (cost=185432.12..185832.14 rows=160008 width=156)
Sort Key: o.created_at DESC
Sort Method: external merge Disk: 45216kB
-> Hash Join (cost=4521.00..172356.00 rows=160008 width=156)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..158432.00 rows=160008 width=112)
Filter: (status = ANY ('{pending,processing,shipped}'::text[]))
-> Hash (cost=3120.00..3120.00 rows=80000 width=48)
-> Seq Scan on users u (cost=0.00..3120.00 rows=80000 width=48)
Planning Time: 3.421 ms
Execution Time: 3247.832 ms ← 3.2 秒!
问题清单:
- orders 表全表扫描(Seq Scan)
- 排序溢出到磁盘(external merge Disk)
- Hash Join 的 Hash 表太大
3.2 第一轮优化:加索引
-- 创建复合索引:覆盖 WHERE 条件 + 排序列 + SELECT 列
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC)
INCLUDE (order_no, user_id, total_amount);
-- 用户表索引
CREATE INDEX idx_users_id_name_level
ON users(id)
INCLUDE (name, level);
INCLUDE 子句是覆盖索引的关键。它把查询需要的额外列存储在索引中,使得查询不需要回表。
再次测试:
Execution Time: 847.321 ms ← 从 3.2 秒降到 0.85 秒,改善 73%
有进步,但仍然不够好。847 毫秒对于一个 OLTP 查询来说还是太慢。
3.3 第二轮优化:分区 + 部分索引
-- 按月分区(PG12+ 原生分区表)
CREATE TABLE orders (
id BIGSERIAL,
order_no UUID DEFAULT uuidv7(),
user_id INTEGER NOT NULL,
total_amount DECIMAL(10,2),
status VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (created_at);
-- 创建最近 3 个月的分区
CREATE TABLE orders_2026_04
PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE orders_2026_05
PARTITION OF orders
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE orders_2026_06
PARTITION OF orders
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- 部分索引:只索引活跃订单
CREATE INDEX idx_orders_active_status
ON orders(status, created_at DESC)
INCLUDE (order_no, user_id, total_amount)
WHERE status IN ('pending', 'processing', 'shipped');
分区 + 部分索引的效果:
Execution Time: 45.231 ms ← 从 847ms 降到 45ms,改善 94%
3.4 第三轮优化:连接池 + 查询重写
-- 使用 CTE 精简执行计划
WITH recent_orders AS (
SELECT id, order_no, user_id, total_amount, status, created_at
FROM orders
WHERE status = ANY(ARRAY['pending', 'processing', 'shipped'])
AND created_at > now() - interval '7 days' -- 缩小时间窗口
ORDER BY created_at DESC
LIMIT 100 -- 先取候选集
)
SELECT o.*, u.name AS user_name, u.level AS user_level
FROM recent_orders o
JOIN users u ON o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 20;
Execution Time: 28.642 ms ← 最终结果!
3.5 pg_stat_statements:你的慢查询雷达
没有 pg_stat_statements,调优就像盲人摸象:
-- 启用 pg_stat_statements(PG18 建议配置)
-- postgresql.conf:
-- shared_preload_libraries = 'pg_stat_statements'
-- pg_stat_statements.max = 10000
-- pg_stat_statements.track = all
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 找到最耗时的 10 个查询
SELECT
queryid,
LEFT(query, 80) AS query_preview,
calls,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND(mean_exec_time::numeric, 2) AS avg_ms,
ROUND((100.0 * total_exec_time / SUM(total_exec_time) OVER())::numeric, 2) AS pct,
rows,
shared_blks_hit,
shared_blks_read,
ROUND((100.0 * shared_blks_hit /
NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) AS cache_hit_pct
FROM pg_stat_statements
WHERE calls > 100 -- 过滤低频查询
ORDER BY total_exec_time DESC
LIMIT 10;
3.6 Collation 陷阱:排序规则正在偷走你的 CPU
这是一个几乎每个 DBA 都会踩的坑:
-- 查看当前数据库的排序规则
SELECT datname, datcollate, datctype FROM pg_database WHERE datname = current_database();
-- 可能输出:en_US.UTF-8
-- 测试:不同 Collation 下的排序性能
CREATE TABLE test_sort AS
SELECT md5(i::text) AS data FROM generate_series(1, 1000000) i;
-- 使用 ICU Collation(PG10+ 支持,推荐)
CREATE COLLATION icu_undeterministic (
provider = icu,
locale = 'und',
deterministic = false
);
-- 对比测试
\timing on
SELECT * FROM test_sort ORDER BY data COLLATE "en_US.utf8" LIMIT 10;
-- Time: 2341.221 ms
SELECT * FROM test_sort ORDER BY data COLLATE icu_undeterministic LIMIT 10;
-- Time: 612.037 ms ← 快了近 4 倍!
\timing off
PG18 建议:所有新数据库使用 ICU Collation:
CREATE DATABASE new_app
ENCODING 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
ICU_LOCALE = 'und'
ICU_RULES = 'und-x-icu'
TEMPLATE template0;
第四章:生产级部署与配置最佳实践
4.1 PG18 推荐配置(8 核 32GB 内存服务器)
# postgresql.conf - 生产环境推荐配置
# === 连接 ===
listen_addresses = '0.0.0.0'
max_connections = 200
superuser_reserved_connections = 5
# === 内存 ===
shared_buffers = '8GB' # 物理内存的 25%
effective_cache_size = '24GB' # 物理内存的 75%
maintenance_work_mem = '1GB' # VACUUM/CREATE INDEX 等维护操作
work_mem = '32MB' # 单个排序/哈希操作的内存
# 注意:每个查询节点最多用 work_mem × 节点数
hash_mem_multiplier = 1.0 # PG18 新增,控制哈希表内存倍数
# === I/O(PG18 AIO) ===
io_method = 'aio' # 核心!启用异步 I/O
effective_io_concurrency = 32 # SSD 推荐 32,HDD 推荐 2
random_page_cost = 1.1 # SSD 设为 1.0-1.5,HDD 保持 4.0
seq_page_cost = 1.0
wal_sync_method = 'fdatasync'
# === WAL ===
wal_level = 'replica'
max_wal_size = '8GB'
min_wal_size = '2GB'
wal_buffers = '64MB'
checkpoint_timeout = '15min'
checkpoint_completion_target = 0.9
# === Autovacuum ===
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = '30s'
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.025
autovacuum_vacuum_cost_limit = 2000 # SSD 可以调高
autovacuum_vacuum_cost_delay = '2ms'
# === 查询规划 ===
default_statistics_target = 500 # 提升,帮助优化器做更好的决策
enable_partitionwise_join = on # 分区表必须开
enable_partitionwise_aggregate = on # 分区表必须开
jit = off # OLTP 关闭;OLAP 开启
# === 日志 ===
log_min_duration_statement = '500ms' # 记录超过 500ms 的慢查询
log_checkpoints = on
log_autovacuum_min_duration = '1s'
log_lock_waits = on
log_temp_files = '100MB' # 记录使用超过 100MB 临时文件的查询
4.2 Docker 一键部署 PG18
# Dockerfile
FROM postgres:18-alpine
# 启用 AIO(需要较新的内核)
# 确保宿主机的 docker 版本支持 io_uring
COPY ./postgresql.conf /etc/postgresql/postgresql.conf
COPY ./pg_hba.conf /etc/postgresql/pg_hba.conf
# 初始化时执行
COPY ./init.sql /docker-entrypoint-initdb.d/
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
# docker-compose.yml
services:
postgres:
build: .
ports:
- "5432:5432"
environment:
POSTGRES_DB: app_production
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
deploy:
resources:
limits:
cpus: '8'
memory: 32G
sysctls:
- net.core.somaxconn=4096
- vm.swappiness=1
volumes:
pg_data:
4.3 升级路径
PG14 → PG15 → PG16 → PG17 → PG18 → PG19 (Beta)
PG18 的重要变化:
- AIO 子系统:升级后需要重启才能生效。可以先在测试环境验证后再改配置
- 默认生成列行为:从 STORED 改为 VIRTUAL,如果你之前依赖 STORED 的行为,需要显式指定
- pg_dump/pg_restore:大版本升级必须用新版本的 pg_dump 导出,新版本的 pg_restore 导入
推荐使用 pg_upgrade 进行原地升级:
# 1. 停止旧实例
pg_ctl -D /var/lib/postgresql/16/data stop
# 2. 安装 PG18
apt-get install postgresql-18
# 3. 使用 pg_upgrade
/usr/lib/postgresql/18/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/16/data \
--new-datadir=/var/lib/postgresql/18/data \
--old-bindir=/usr/lib/postgresql/16/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--check # 先检查兼容性
# 4. 检查通过,执行升级
/usr/lib/postgresql/18/bin/pg_upgrade \
--old-datadir=/var/lib/postgresql/16/data \
--new-datadir=/var/lib/postgresql/18/data \
--old-bindir=/usr/lib/postgresql/16/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--link # 使用硬链接,速度最快
# 5. 启动新实例
pg_ctl -D /var/lib/postgresql/18/data start
第五章:PGConf.dev 2026 揭示的未来方向
5 月 19-22 日,PGConf.dev 2026 在温哥华举办。作为 PG 开源 30 周年的纪念场,这场大会透露了几个关键的未来方向:
社区最关注的五大议题
TDE(透明数据加密):标题就叫"Stop Debating, Start Implementing",但讨论结果反而说明短期内很难有统一方案。它涉及存储加密、WAL 加密、密钥管理、合规认证等多个层面的联动。
逻辑复制的 DDL 同步:这是被提及最多的话题。目前 PG 的逻辑复制只支持 DML,DDL(如
ALTER TABLE)不会同步到订阅端。社区正在讨论 schema diff、基于 WAL 的 DDL 同步、事件触发器等多种路径。全局索引:Oracle 迁移用户最期待的特性。跨分区的唯一性约束需要解决 VACUUM 扩展性、DDL 重写、跨分区死锁等难题,短期内没有简单答案。
扩展生态的不可变基础设施化:越来越多大规模部署选择把扩展固化到容器镜像中,而非在运行中的数据库里动态安装。这降低了恶意扩展的风险,但改变了扩展的打包和分发方式。
AI 与 PG 的融合:从 AI 辅助 SQL 优化到自然语言查询接口,AI 在 PG 生态中的角色正在快速演进。
总结:这次升级值不值得?
直接答案:值得。而且越早越好。
PG18 的异步 I/O、Skip Scan、UUIDv7 这些特性,不是"锦上添花"的可选功能,而是直接影响到数据库核心性能的基础设施级改进。如果你在跑 PG16 或更早的版本,升级到 PG18 几乎可以确定会带来显著的性能提升——不需要改任何一行应用代码。
PG19 目前还在 Beta 阶段,但 64 位 MultiXact、并行 autovacuum、REPACK CONCURRENTLY 这些运维侧的改进,对于任何严肃的生产环境来说都是"早该有了"的特性。建议在 Beta 阶段就开始在测试环境试用,等正式版发布后尽快规划升级。
PostgreSQL 走过 30 年了。从伯克利大学的一个研究项目,到如今全球最先进的开源关系数据库,它的演进从未像 2026 年这样密集和有野心。如果你还没开始关注 PG18/19,现在正是最好的时机。
参考资源
- PostgreSQL 19 Beta 1 Release: https://www.postgresql.org/about/news/postgresql-19-beta-1-released-3214/
- PGConf.dev 2026: https://2026.pgconf.dev/
- PG18 Release Notes: https://www.postgresql.org/docs/18/release-18.html
- "PostgreSQL 19 Beta: The Four Features You'll Actually Feel" by thebuild.com