bookworm-smart-assistant/skills/database-tuning-expert/references/postgresql-tuning.md

13 KiB
Raw Blame History

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% 数据页缓存,不宜超过 8GBOS 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);