13 KiB
13 KiB
PostgreSQL 深度调优指南
本文档是 database-tuning-expert 技能的参考资料,涵盖 PostgreSQL 核心调优领域。
1. EXPLAIN ANALYZE 深度解读
1.1 基本用法
-- 完整的执行计划分析(推荐格式)
EXPLAIN (ANALYZE, BUFFERS, COSTS, TIMING, FORMAT TEXT)
SELECT o.id, o.total, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'active' AND o.created_at > '2025-01-01';
ANALYZE: 实际执行查询并返回真实耗时(注意:会真正执行 DML,生产环境用BEGIN; EXPLAIN ANALYZE ...; ROLLBACK;)BUFFERS: 显示缓冲区命中/读取情况COSTS: 显示估算成本(默认开启)TIMING: 显示每个节点的实际耗时
1.2 执行计划节点类型
| 节点类型 | 说明 | 优化建议 |
|---|---|---|
| Seq Scan | 全表顺序扫描 | 表小(< 几百行)可接受;大表应添加索引 |
| Index Scan | 通过索引定位后回表取数据 | 理想的访问方式 |
| Index Only Scan | 仅访问索引,不回表 | 最优,需确保 visibility map 足够新 |
| Bitmap Index Scan | 构建位图后批量回表 | 多条件 OR / 低选择性索引查询常见 |
| Bitmap Heap Scan | 配合 Bitmap Index Scan 回表 | 注意 Recheck Cond 是否过滤大量行 |
| Nested Loop | 对外表每行扫描内表 | 内表需有索引,适合小结果集 |
| Hash Join | 构建哈希表后探测匹配 | 大表等值连接的优选 |
| Merge Join | 两表排序后归并 | 两表已按连接键排序时高效 |
| Sort | 排序操作 | 注意是否溢出到磁盘(Sort Method: external merge) |
| HashAggregate | 哈希分组聚合 | GROUP BY 基数大时使用 |
| GroupAggregate | 排序后分组聚合 | 数据已有序时使用 |
1.3 成本估算与实际执行对比
Seq Scan on orders (cost=0.00..1520.00 rows=500 width=40)
(actual time=0.015..12.340 rows=487 loops=1)
cost=启动成本..总成本: 优化器估算的相对成本rows=500: 优化器估算返回行数actual time=首行耗时..末行耗时(ms): 实际执行时间rows=487: 实际返回行数loops=1: 该节点执行次数
关键排查点: 若 rows 估算值与实际值偏差 > 10 倍,说明统计信息过时,需执行 ANALYZE 更新。
1.4 常见慢查询模式
| 模式 | 特征 | 解决方案 |
|---|---|---|
| 大表全扫描 | Seq Scan + rows 很大 | 添加 WHERE 条件对应索引 |
| 估算偏差 | estimated rows 与 actual rows 差 10x+ | ANALYZE table_name 更新统计 |
| 排序溢出 | Sort Method: external merge Disk | 增大 work_mem 或添加排序索引 |
| 嵌套循环爆炸 | Nested Loop + loops=100000 | 改用 Hash Join 或添加索引 |
| 索引失效 | 有索引但 Seq Scan | 检查隐式类型转换、函数包裹、LIKE '%前缀' |
| 锁等待 | 查询本身快但总耗时长 | 检查 pg_stat_activity 中的 wait_event |
2. 索引类型详解
2.1 B-tree (默认)
最常用的索引类型,支持等值、范围、排序、IS NULL 查询。
-- 创建单列索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 创建联合索引(遵循最左前缀原则)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- 创建唯一索引
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- 创建部分索引(只索引满足条件的行)
CREATE INDEX idx_orders_active ON orders(user_id)
WHERE status = 'active';
-- 创建覆盖索引(INCLUDE 避免回表)
CREATE INDEX idx_orders_cover ON orders(user_id)
INCLUDE (total, created_at);
2.2 Hash 索引
仅支持等值查询,PostgreSQL 10+ 支持 WAL 日志(可靠)。
CREATE INDEX idx_sessions_token ON sessions USING hash(token);
-- 适合:token 精确查找,不需要范围查询
2.3 GIN (Generalized Inverted Index)
适合多值类型:全文搜索、JSONB、数组。
-- JSONB 字段索引
CREATE INDEX idx_metadata_gin ON products USING gin(metadata);
-- 查询:SELECT * FROM products WHERE metadata @> '{"color": "red"}';
-- 全文搜索索引
CREATE INDEX idx_articles_fts ON articles USING gin(to_tsvector('chinese', title || ' ' || content));
-- 数组字段索引
CREATE INDEX idx_tags_gin ON posts USING gin(tags);
-- 查询:SELECT * FROM posts WHERE tags @> ARRAY['postgresql'];
2.4 GiST (Generalized Search Tree)
适合几何类型、范围类型、全文搜索(支持模糊匹配权重)。
-- PostGIS 空间索引
CREATE INDEX idx_locations_geom ON locations USING gist(geom);
-- 范围类型索引
CREATE INDEX idx_events_during ON events USING gist(during);
-- 查询:SELECT * FROM events WHERE during && '[2025-01-01, 2025-06-01)';
-- pg_trgm 模糊搜索
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_users_name_trgm ON users USING gist(name gist_trgm_ops);
-- 查询:SELECT * FROM users WHERE name % '张三';
2.5 BRIN (Block Range Index)
极小的索引体积,适合物理有序的大表(如时序数据)。
-- 时序数据时间戳索引
CREATE INDEX idx_logs_created_brin ON logs USING brin(created_at)
WITH (pages_per_range = 128);
-- 适合场景:数据按 created_at 自然递增写入
-- 不适合:随机插入或频繁更新的表
2.6 索引维护命令
-- 查看索引使用情况
SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- 查看索引大小
SELECT pg_size_pretty(pg_relation_size('idx_orders_user_id'));
-- 重建索引(不锁表,推荐)
REINDEX INDEX CONCURRENTLY idx_orders_user_id;
-- 删除未使用的索引(idx_scan = 0 持续数周)
DROP INDEX CONCURRENTLY idx_unused;
3. 分区表
3.1 Range 分区(最常用)
-- 创建分区父表
CREATE TABLE logs (
id bigserial,
message text,
created_at timestamptz NOT NULL
) PARTITION BY RANGE (created_at);
-- 创建月度分区
CREATE TABLE logs_2025_01 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE logs_2025_02 PARTITION OF logs
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- 创建默认分区(兜底)
CREATE TABLE logs_default PARTITION OF logs DEFAULT;
3.2 List 分区
CREATE TABLE orders (
id bigserial,
region text NOT NULL,
total numeric
) PARTITION BY LIST (region);
CREATE TABLE orders_cn PARTITION OF orders FOR VALUES IN ('cn', 'hk', 'tw');
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('us', 'ca');
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('de', 'fr', 'uk');
3.3 Hash 分区
CREATE TABLE sessions (
id bigserial,
user_id bigint NOT NULL,
data jsonb
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);
3.4 分区裁剪验证
-- 确认分区裁剪生效(Constraint Exclusion / Partition Pruning)
SET enable_partition_pruning = on; -- 默认开启
EXPLAIN SELECT * FROM logs WHERE created_at = '2025-03-15';
-- 应只扫描 logs_2025_03,不扫描其他分区
3.5 自动分区管理(pg_partman)
-- 安装 pg_partman 扩展
CREATE EXTENSION pg_partman;
-- 配置自动分区
SELECT create_parent(
p_parent_table := 'public.logs',
p_control := 'created_at',
p_type := 'native',
p_interval := '1 month',
p_premake := 3 -- 预创建未来 3 个分区
);
-- 定期维护(cron 每天执行)
SELECT run_maintenance();
4. VACUUM 与 ANALYZE
4.1 VACUUM 基础
PostgreSQL 使用 MVCC(多版本并发控制),UPDATE/DELETE 不会立即物理删除旧行,而是标记为"死元组"(dead tuples)。VACUUM 负责回收这些空间。
-- 手动 VACUUM(不锁表,可并发读写)
VACUUM orders;
-- VACUUM FULL(重写整表,会锁表,慎用)
VACUUM FULL orders;
-- VACUUM + ANALYZE(清理 + 更新统计)
VACUUM ANALYZE orders;
4.2 autovacuum 配置优化
# postgresql.conf 关键参数
autovacuum = on # 必须开启
autovacuum_max_workers = 3 # 并发 worker 数
autovacuum_naptime = 60 # 检查间隔(秒)
# 触发阈值
autovacuum_vacuum_threshold = 50 # 基础行数
autovacuum_vacuum_scale_factor = 0.1 # 表大小比例(10%变更触发)
# 实际触发 = threshold + scale_factor * 表行数
# 大表单独调优
ALTER TABLE huge_table SET (
autovacuum_vacuum_scale_factor = 0.01, # 1% 变更就触发
autovacuum_vacuum_threshold = 1000
);
4.3 监控死元组
-- 查看各表死元组情况
SELECT relname, n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS dead_ratio,
last_vacuum, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
5. PgBouncer 连接池
5.1 三种 pool_mode 对比
| 模式 | 连接复用粒度 | 兼容性 | 推荐场景 |
|---|---|---|---|
| session | 会话结束才释放 | 完全兼容 | 需要 PREPARE/SET/临时表 |
| transaction | 事务结束即释放 | 大部分兼容 | Web 应用(推荐) |
| statement | 每条语句后释放 | 只支持 autocommit | 简单查询负载 |
5.2 完整配置模板
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
server_lifetime = 3600
server_connect_timeout = 15
query_timeout = 120
query_wait_timeout = 60
log_connections = 0
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60
5.3 监控命令
-- 连接到 PgBouncer 管理控制台
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
-- 查看连接池状态
SHOW POOLS;
-- 查看活跃客户端
SHOW CLIENTS;
-- 查看服务端连接
SHOW SERVERS;
-- 查看统计信息
SHOW STATS;
6. 内存参数调优
6.1 核心参数
| 参数 | 建议值 | 说明 |
|---|---|---|
shared_buffers |
物理内存的 25% | 数据页缓存,不宜超过 8GB(OS cache 更高效) |
work_mem |
32MB-256MB | 排序/哈希操作内存,按连接数×并发估算总量 |
effective_cache_size |
物理内存的 50%-75% | 告诉优化器可用缓存量(不分配实际内存) |
maintenance_work_mem |
512MB-2GB | VACUUM/CREATE INDEX 时使用 |
wal_buffers |
64MB | WAL 写入缓冲 |
6.2 典型服务器配置示例
# 16GB 内存服务器
shared_buffers = 4GB
work_mem = 64MB
effective_cache_size = 12GB
maintenance_work_mem = 1GB
wal_buffers = 64MB
# 连接相关
max_connections = 200
6.3 work_mem 估算
总 work_mem 消耗 ≈ work_mem × max_connections × 每查询排序/哈希操作数
# 例:64MB × 200 连接 × 2 操作 = 25.6GB(注意不要超过可用内存)
7. 慢查询日志
7.1 配置慢查询记录
# postgresql.conf
log_min_duration_statement = 200 # 记录超过 200ms 的查询
log_statement = 'none' # 不记录所有语句(避免日志爆炸)
log_line_prefix = '%t [%p] %u@%d ' # 时间 + PID + 用户 + 数据库
# 自动解释慢查询
auto_explain.log_min_duration = 500 # 超过 500ms 自动记录执行计划
auto_explain.log_analyze = on
auto_explain.log_buffers = on
7.2 pg_stat_statements 扩展
-- 启用扩展
CREATE EXTENSION pg_stat_statements;
-- 查看 Top 10 慢查询
SELECT query,
calls,
round(total_exec_time::numeric, 2) AS total_ms,
round(mean_exec_time::numeric, 2) AS avg_ms,
rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 查看 Top 10 调用最频繁的查询
SELECT query, calls, rows,
round(total_exec_time::numeric, 2) AS total_ms
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;
-- 重置统计(定期执行,如每周)
SELECT pg_stat_statements_reset();
7.3 实时排查活跃查询
-- 查看当前正在执行的查询
SELECT pid, now() - pg_stat_activity.query_start AS duration,
state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state = 'active' AND pid <> pg_backend_pid()
ORDER BY duration DESC;
-- 终止指定查询(优雅取消)
SELECT pg_cancel_backend(pid);
-- 强制终止连接(慎用)
SELECT pg_terminate_backend(pid);