PostgreSQL: PostgreSQL性能优化实战

最后更新:2026-08-26

1. 你将学到


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 输出关键字段

SQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 1001;
TEXT 📖 仅展示
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) 执行计划解读流程

100%
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 全表扫描

SQL
-- No index on status, planner chooses Seq Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*) FROM orders WHERE status = 'pending';
TEXT 📖 仅展示
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 精确查找

SQL
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;
TEXT 📖 仅展示
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 中等选择性

SQL
-- Moderate selectivity: planner chooses Bitmap
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id BETWEEN 1000 AND 1100;
TEXT 📖 仅展示
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

SQL
-- 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';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:发现并修复缺失索引

SQL
-- 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

输出:

TEXT 📖 仅展示
CREATE TABLE

5. Concept:pg_stat_statements 慢查询

(1) 启用与配置

BASH
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000
SQL
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 慢查询

SQL
-- 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;

输出:

TEXT 📖 仅展示
  result  
----------
   42.50
(1 row)

▶ 示例:发现 I/O 密集查询

SQL
-- 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;

输出:

TEXT 📖 仅展示
  result  
----------
   42.50
(1 row)

6. Concept:连接池与配置调优

(1) PgBouncer 连接池

模式 说明 适合场景
Session pooling 连接与客户端绑定 需要会话变量/临时表
Transaction pooling 事务结束归还连接 大多数 Web 应用
Statement pooling 语句结束归还连接 无事务的简单查询
BASH
# 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 核心参数

BASH
# 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 核数 并行工作进程

▶ 示例:验证参数生效

SQL
-- 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');

输出:

TEXT 📖 仅展示
ALTER TABLE

7. Concept:Autovacuum 原理与调优

(1) 为什么需要 Autovacuum

PostgreSQL 的 MVCC 机制:UPDATE/DELETE 产生死元组(dead tuples),需要 VACUUM 回收空间,否则表膨胀、查询变慢。

SQL
-- 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

SQL
-- 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)

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:监控 vacuum 进度

SQL
-- 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;

输出:

TEXT 📖 仅展示
 count 
-------
     5
(1 row)

8. 操作:并行查询与监控

▶ 示例:启用并行查询

SQL
-- 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;
TEXT 📖 仅展示
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 监控

SQL
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;

输出:

TEXT 📖 仅展示
 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

▶ 示例:识别与修复性能反模式

SQL
-- 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';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. 综合示例

SQL
-- 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;

❓ 常见问题

Q EXPLAIN 和 EXPLAIN ANALYZE 有什么区别?
A EXPLAIN 只显示预估计划不执行查询,EXPLAIN ANALYZE 实际执行并返回真实耗时和行数。注意 ANALYZE 会执行 INSERT/UPDATE/DELETE,做 DML 分析时建议包裹在事务中后 ROLLBACK。
Q cost 值很大但实际很快,为什么?
A cost 是规划器估算的单位值(非毫秒),受统计信息影响。如果 ANALYZE 统计信息过时,cost 可能不准确。定期 ANALYZE 保持统计新鲜。
Q shared_buffers 设多少合适?
A 一般设为物理内存的 25%,不超过 40%。Linux 上超过 40% 可能与 OS 页缓存冲突。Windows 建议不超过 512MB-1GB。
Q work_mem 设大了会不会 OOM?
A work_mem 是每个排序/哈希操作的内存上限,一个复杂查询可能同时有多个排序操作。设过大确实可能 OOM。建议 32-128MB,监控实际使用调整。
Q autovacuum 跑不过写入速度怎么办?
A 降低 scale_factor(如 0.02)、增大 cost_limit(如 2000)、减少 cost_delay(如 1ms),对大表单独设置。极端情况手动 VACUUM 并行。
Q 并行查询什么场景下会生效?
A 表大小超过 min_parallel_table_scan_size(默认 8MB),查询成本超过并行启动阈值,max_parallel_workers 有空闲。小表或简单查询不会并行。
Q Seq Scan 一定比 Index Scan 差吗?
A 不一定。当需要返回表中大部分行时,Seq Scan 顺序读比 Index Scan 随机读更高效。PG 规划器会自动选择成本更低的方案。

📖 小节


📝 作业

  1. ⭐ 对一张 100 万行的测试表,分别用 EXPLAIN ANALYZE 观察 Seq Scan 和 Index Scan 的 actual time 差异,记录 cost 估算与实际耗时的偏差。

  2. ⭐⭐ 配置 pg_stat_statements,收集 24 小时查询统计,编写一份慢查询报告:按 total_exec_time、mean_exec_time、shared_blks_read 三个维度各列出 Top 5,并给出优化建议。

  3. ⭐⭐⭐ 模拟 Charlie 的场景:创建 5 张表(orders/order_items/users/products/payments),写入测试数据,使用 EXPLAIN ANALYZE 定位 3 个性能问题(缺失索引、work_mem 不足、autovacuum 滞后),逐项修复后用 EXPLAIN ANALYZE 验证每个修复的性能提升倍数。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏