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

459 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PostgreSQL 深度调优指南
> 本文档是 database-tuning-expert 技能的参考资料,涵盖 PostgreSQL 核心调优领域。
---
## 1. EXPLAIN ANALYZE 深度解读
### 1.1 基本用法
```sql
-- 完整的执行计划分析(推荐格式)
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 查询。
```sql
-- 创建单列索引
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 日志(可靠)。
```sql
CREATE INDEX idx_sessions_token ON sessions USING hash(token);
-- 适合token 精确查找,不需要范围查询
```
### 2.3 GIN (Generalized Inverted Index)
适合多值类型全文搜索、JSONB、数组。
```sql
-- 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)
适合几何类型、范围类型、全文搜索(支持模糊匹配权重)。
```sql
-- 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)
极小的索引体积,适合物理有序的大表(如时序数据)。
```sql
-- 时序数据时间戳索引
CREATE INDEX idx_logs_created_brin ON logs USING brin(created_at)
WITH (pages_per_range = 128);
-- 适合场景:数据按 created_at 自然递增写入
-- 不适合:随机插入或频繁更新的表
```
### 2.6 索引维护命令
```sql
-- 查看索引使用情况
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 分区(最常用)
```sql
-- 创建分区父表
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 分区
```sql
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 分区
```sql
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 分区裁剪验证
```sql
-- 确认分区裁剪生效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
```sql
-- 安装 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 负责回收这些空间。
```sql
-- 手动 VACUUM不锁表可并发读写
VACUUM orders;
-- VACUUM FULL重写整表会锁表慎用
VACUUM FULL orders;
-- VACUUM + ANALYZE清理 + 更新统计)
VACUUM ANALYZE orders;
```
### 4.2 autovacuum 配置优化
```ini
# 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 监控死元组
```sql
-- 查看各表死元组情况
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 完整配置模板
```ini
[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 监控命令
```sql
-- 连接到 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 典型服务器配置示例
```ini
# 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 配置慢查询记录
```ini
# 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 扩展
```sql
-- 启用扩展
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 实时排查活跃查询
```sql
-- 查看当前正在执行的查询
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);
```