PostgreSQL: PostgreSQL性能优化实战
最后更新:2026-08-26
1. 你将学到
- 深入阅读 EXPLAIN ANALYZE 执行计划
- 理解 Seq Scan / Index Scan / Bitmap Scan / Nest Loop / Hash Join / Merge Join
- 使用 pg_stat_statements 定位慢查询
- 配置连接池(PgBouncer)
- 调优关键参数:shared_buffers、work_mem、effective_cache_size
- 理解并调优 autovacuum
2. 故事
Charlie 接手了一个电商系统,首页加载 8 秒,月度报表超时。他用 EXPLAIN ANALYZE 逐条分析,发现三大问题:(1) orders 表缺少索引导致 Seq Scan;(2) work_mem 只有 4MB,复杂排序溢出到磁盘;(3) autovacuum 跑不过写入速度,表膨胀严重。逐项优化后:加索引让查询走 Index Scan,work_mem 调到 64MB 排序全内存,autovacuum 频率翻倍控制膨胀——整体性能提升 10 倍,首页 0.8 秒加载。
3. Concept:EXPLAIN ANALYZE 深度解读
(1) 执行计划核心操作符
| 操作符 | 含义 | 适合场景 |
|---|---|---|
| Seq Scan | 全表顺序扫描 | 小表、无可用索引、大量行需返回 |
| Index Scan | B-Tree 索引扫描 | 高选择性查询(返回 < 5% 行) |
| Bitmap Heap Scan | 位图堆扫描 | 中等选择性(5%-15%),先收集 TID 再取行 |
| Bitmap Index Scan | 位图索引扫描 | 配合 Bitmap Heap Scan |
| Nest Loop | 嵌套循环连接 | 外表小 + 内表有索引 |
| Hash Join | 哈希连接 | 等值连接,内表可放入内存 |
| Merge Join | 归并连接 | 两表已排序,等值连接 |
(2) EXPLAIN 输出关键字段
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 1001;
Index Scan using idx_orders_user_id on orders (cost=0.42..8.44 rows=1 width=72) (actual time=0.015..0.016 rows=1 loops=1)
Index Cond: (user_id = 1001)
Buffers: shared hit=4
Planning Time: 0.085 ms
Execution Time: 0.032 ms
| 字段 | 含义 |
|---|---|
| cost=X..Y | 启动成本..总成本(预估) |
| rows=N | 预估返回行数 |
| actual time | 实际耗时(毫秒) |
| rows (actual) | 实际返回行数 |
| loops | 执行次数 |
| Buffers: shared hit | 命中共享缓冲区次数 |
| Planning Time | 规划耗时 |
| Execution Time | 执行耗时 |
(3) 执行计划解读流程
flowchart TD
A["EXPLAIN ANALYZE output"] --> B{"Top node type?"}
B -->|"Seq Scan"| C{"Rows vs estimated?"}
C -->|"Estimate off"| D["RUN ANALYZE<br/>Update statistics"]
C -->|"Estimate OK"| E{"Filter selectivity?"}
E -->|"Low (< 5%)"| F["Add index on filter column"]
E -->|"High (> 15%)"| G["Seq Scan is OK"]
B -->|"Index Scan"| H["✅ Good for low selectivity"]
B -->|"Hash Join"| I{"Hash table spill?"}
I -->|"Yes (work_mem low)"| J["Increase work_mem"]
I -->|"No"| K["✅ Good"]
B -->|"Nest Loop"| L{"Outer rows × inner cost?"}
L -->|"Too high"| M["Consider Hash Join<br/>or add inner index"]
L -->|"Reasonable"| N["✅ Good"]
4. 操作:扫描类型对比
▶ 示例:Seq Scan 全表扫描
-- No index on status, planner chooses Seq Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*) FROM orders WHERE status = 'pending';
Aggregate (actual time=45.123..45.124 rows=1 loops=1)
-> Seq Scan on orders (actual time=0.012..42.890 rows=50000 loops=1)
Filter: (status = 'pending'::text)
Rows Removed by Filter: 950000
▶ 示例:Index Scan 精确查找
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- High selectivity: planner uses Index Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id = 1001;
Index Scan using idx_orders_user_id on orders (actual time=0.015..0.018 rows=3 loops=1)
Index Cond: (user_id = 1001)
▶ 示例:Bitmap Scan 中等选择性
-- Moderate selectivity: planner chooses Bitmap
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id BETWEEN 1000 AND 1100;
Bitmap Heap Scan on orders (actual time=0.523..2.145 rows=523 loops=1)
Recheck Cond: (user_id >= 1000 AND user_id <= 1100)
-> Bitmap Index Scan on idx_orders_user_id (actual time=0.412..0.412 rows=523 loops=1)
Index Cond: (user_id >= 1000 AND user_id <= 1100)
| 扫描类型 | 选择性 | I/O 模式 | 何时最优 |
|---|---|---|---|
| Seq Scan | 全表或 > 15% | 顺序读 | 小表 / 大量返回 |
| Index Scan | < 5% | 随机读 | 精确查找 |
| Bitmap Scan | 5%-15% | 先随机后顺序 | 范围 + 排序 |
▶ 示例:Nest Loop vs Hash Join
-- Small outer table + indexed inner = Nest Loop
EXPLAIN (COSTS OFF)
SELECT o.id, u.name
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 1001;
-- Large outer + unsorted inner = Hash Join
EXPLAIN (COSTS OFF)
SELECT o.id, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date > '2024-01-01';
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ 示例:发现并修复缺失索引
-- Slow query: Seq Scan on large table
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '7 days';
-- Seq Scan, cost=0.00..15432.00, actual time=120ms
-- Add index
CREATE INDEX idx_orders_created_at ON orders (created_at);
-- Re-analyze for accurate statistics
ANALYZE orders;
-- Same query now uses Index Scan
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '7 days';
-- Index Scan, cost=0.42..890.00, actual time=2ms
输出:
CREATE TABLE
5. Concept:pg_stat_statements 慢查询
(1) 启用与配置
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
(2) 关键指标解读
| 列名 | 含义 | 优化方向 |
|---|---|---|
| total_exec_time | 总执行时间 | 优化最耗时的查询 |
| mean_exec_time | 平均执行时间 | 单次慢查询 |
| calls | 调用次数 | 高频查询优先优化 |
| rows | 总返回行数 | 评估是否返回过多 |
| shared_blks_hit | 缓冲区命中 | 命中率低 → 增 shared_buffers |
| shared_blks_read | 磁盘读取 | 高磁盘读 → 加索引/增缓存 |
▶ 示例:定位 Top N 慢查询
-- Top 10 by total time
SELECT left(query, 80) AS query_preview,
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 1) AS avg_ms,
rows,
round((100.0 * shared_blks_hit /
nullif(shared_blks_hit + shared_blks_read, 0))::numeric, 1) AS cache_hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
输出:
result
----------
42.50
(1 row)
▶ 示例:发现 I/O 密集查询
-- Queries with most disk reads
SELECT left(query, 80) AS query_preview,
shared_blks_read AS disk_reads,
shared_blks_hit AS cache_hits,
calls,
round(mean_exec_time::numeric, 1) AS avg_ms
FROM pg_stat_statements
WHERE shared_blks_read > 1000
ORDER BY shared_blks_read DESC
LIMIT 10;
输出:
result
----------
42.50
(1 row)
6. Concept:连接池与配置调优
(1) PgBouncer 连接池
| 模式 | 说明 | 适合场景 |
|---|---|---|
| Session pooling | 连接与客户端绑定 | 需要会话变量/临时表 |
| Transaction pooling | 事务结束归还连接 | 大多数 Web 应用 |
| Statement pooling | 语句结束归还连接 | 无事务的简单查询 |
# pgbouncer.ini
[databases]
shop_db = host=127.0.0.1 port=5432 dbname=shop
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
| 参数 | 推荐值 | 说明 |
|---|---|---|
| max_client_conn | 500-1000 | 最大客户端连接数 |
| default_pool_size | 20-50 | 每数据库/用户连接池大小 |
| reserve_pool_size | 5-10 | 突发流量缓冲池 |
| pool_mode | transaction | Web 应用推荐 |
(2) PostgreSQL 核心参数
# postgresql.conf tuning for 16GB RAM server
shared_buffers = 4GB # 25% of RAM
work_mem = 64MB # per-sort-operation memory
effective_cache_size = 12GB # 75% of RAM
maintenance_work_mem = 1GB # for VACUUM, CREATE INDEX
effective_io_concurrency = 200 # SSD; 2 for HDD
random_page_cost = 1.1 # SSD; 4.0 for HDD
| 参数 | 默认值 | 推荐值(16GB RAM) | 说明 |
|---|---|---|---|
| shared_buffers | 128MB | 25% RAM | 共享缓冲区 |
| work_mem | 4MB | 32-128MB | 排序/哈希内存 |
| effective_cache_size | 4GB | 75% RAM | 规划器缓存估计 |
| maintenance_work_mem | 64MB | 512MB-1GB | 维护操作内存 |
| max_parallel_workers | 8 | CPU 核数 | 并行工作进程 |
▶ 示例:验证参数生效
-- Check current settings
SHOW shared_buffers;
SHOW work_mem;
SHOW effective_cache_size;
-- Check at runtime (no restart needed for some)
ALTER SYSTEM SET work_mem = '64MB';
SELECT pg_reload_conf();
-- Must-restart parameters
SELECT name, setting, boot_val, context
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'effective_cache_size');
输出:
ALTER TABLE
7. Concept:Autovacuum 原理与调优
(1) 为什么需要 Autovacuum
PostgreSQL 的 MVCC 机制:UPDATE/DELETE 产生死元组(dead tuples),需要 VACUUM 回收空间,否则表膨胀、查询变慢。
-- Check dead tuple ratio
SELECT relname,
n_live_tup,
n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
(2) Autovacuum 关键参数
| 参数 | 默认值 | 调优建议 |
|---|---|---|
| autovacuum | on | 必须开启 |
| autovacuum_vacuum_threshold | 50 | 触发 vacuum 的基础死元组数 |
| autovacuum_vacuum_scale_factor | 0.2 | 死元组占行数 20% 触发 |
| autovacuum_analyze_scale_factor | 0.1 | 变化 10% 触发 analyze |
| autovacuum_vacuum_cost_delay | 2ms | vacuum 节流延迟 |
| autovacuum_vacuum_cost_limit | 200 | vacuum 每轮 I/O 限制 |
▶ 示例:针对大表调优 autovacuum
-- For high-write tables, lower scale factor for more frequent vacuum
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_delay = '1ms',
autovacuum_vacuum_cost_limit = 1000
);
-- For orders with 10M rows, vacuum triggers at:
-- 10M × 0.05 = 500K dead tuples (vs default 2M)
输出:
-- SQL 语句执行成功
▶ 示例:监控 vacuum 进度
-- PG 12+: track vacuum progress
SELECT pid,
relid::regclass AS table_name,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
index_vacuum_count
FROM pg_stat_progress_vacuum;
输出:
count
-------
5
(1 row)
8. 操作:并行查询与监控
▶ 示例:启用并行查询
-- Enable parallel query (PG 10+)
SET max_parallel_workers_per_gather = 4;
SET max_parallel_workers = 8;
SET parallel_tuple_cost = 0.001;
SET min_parallel_table_scan_size = '8MB';
-- Verify parallel plan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*), category
FROM products
GROUP BY category;
Finalize Aggregate (actual time=12.3..12.4 rows=5 loops=1)
-> Gather (actual time=12.1..12.3 rows=15 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (actual time=8.5..8.6 rows=5 loops=3)
-> Parallel Seq Scan on products (actual time=0.2..6.1 rows=33333 loops=3)
▶ 示例:pg_stat_user_tables 监控
SELECT relname AS table_name,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
round(100.0 * idx_scan / nullif(idx_scan + seq_scan, 0), 2) AS idx_scan_pct,
n_live_tup,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC;
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| 监控指标 | 告警阈值 | 优化动作 |
|---|---|---|
| seq_tup_read >> idx_tup_fetch | seq_scan 高 | 加索引 |
| dead_pct > 20% | 膨胀严重 | 调优 autovacuum |
| cache_hit_pct < 95% | 缓冲不足 | 增 shared_buffers |
| last_autovacuum > 7 days | vacuum 不及时 | 降低 scale_factor |
▶ 示例:识别与修复性能反模式
-- Anti-pattern 1: OR condition preventing index use
-- BAD
SELECT * FROM orders WHERE user_id = 1001 OR status = 'pending';
-- GOOD: use UNION ALL
SELECT * FROM orders WHERE user_id = 1001
UNION ALL
SELECT * FROM orders WHERE status = 'pending' AND user_id != 1001;
-- Anti-pattern 2: function wrapping indexed column
-- BAD
SELECT * FROM orders WHERE lower(status) = 'pending';
-- GOOD
SELECT * FROM orders WHERE status = lower('PENDING');
-- Anti-pattern 3: LIKE with leading wildcard
-- BAD
SELECT * FROM products WHERE name LIKE '%phone%';
-- GOOD: pg_trgm or full-text search
SELECT * FROM products WHERE name % 'phone';
输出:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. 综合示例
-- Full performance optimization workflow for Charlie's e-commerce system
-- Step 1: Identify slow queries
SELECT left(query, 60) AS q, calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 1) AS avg_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 5;
-- Step 2: Analyze worst query
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total_amount, u.name, p.name AS product_name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31'
AND o.status = 'completed';
-- Step 3: Add missing indexes
CREATE INDEX idx_orders_date_status ON orders (order_date, status);
CREATE INDEX idx_order_items_order_id ON order_items (order_id);
CREATE INDEX idx_order_items_product_id ON order_items (product_id);
-- Step 4: Update statistics
ANALYZE orders;
ANALYZE order_items;
ANALYZE products;
-- Step 5: Tune autovacuum for high-write tables
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_limit = 1000
);
-- Step 6: Increase work_mem for complex sorts
ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM SET effective_cache_size = '12GB';
SELECT pg_reload_conf();
-- Step 7: Verify improvement
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31'
AND o.status = 'completed';
-- Step 8: Monitor table health
SELECT relname,
n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_items', 'products')
ORDER BY n_dead_tup DESC;
❓ 常见问题
📖 小节
- EXPLAIN ANALYZE 是性能优化的第一工具,读懂操作符和成本是关键
- Seq Scan/Index Scan/Bitmap Scan 适合不同选择性场景
- pg_stat_statements 定位慢查询:关注 total_time、calls、cache_hit
- PgBouncer transaction 模式是 Web 应用连接池标配
- shared_buffers 25% RAM、work_mem 按查询复杂度调、effective_cache_size 75% RAM
- Autovacuum 对高写入表需降低 scale_factor、提高 cost_limit
- 避免 OR 条件、函数包裹索引列、LIKE 前缀通配符等常见反模式
📝 作业
-
⭐ 对一张 100 万行的测试表,分别用 EXPLAIN ANALYZE 观察 Seq Scan 和 Index Scan 的 actual time 差异,记录 cost 估算与实际耗时的偏差。
-
⭐⭐ 配置 pg_stat_statements,收集 24 小时查询统计,编写一份慢查询报告:按 total_exec_time、mean_exec_time、shared_blks_read 三个维度各列出 Top 5,并给出优化建议。
-
⭐⭐⭐ 模拟 Charlie 的场景:创建 5 张表(orders/order_items/users/products/payments),写入测试数据,使用 EXPLAIN ANALYZE 定位 3 个性能问题(缺失索引、work_mem 不足、autovacuum 滞后),逐项修复后用 EXPLAIN ANALYZE 验证每个修复的性能提升倍数。