7 个 PostgreSQL 性能调优实战技巧:索引策略、查询优化、主从复制全解析

阿里云上云采购季 2核2G云服务器99元/年 续费同价

从索引设计到查询优化,从配置调优到主从复制,手把手提升 PostgreSQL 性能。


为什么 PostgreSQL 性能会变差?

随着数据量增长,常见的性能瓶颈:

  • 缺少索引或索引设计不合理 → 全表扫描
  • 查询语句写得差 → 大量无用 IO
  • 配置参数沿用默认值 → 内存/并发配置不匹配
  • 没有主从分离 → 读写互相影响

技巧1:索引策略——不是建得越多越好

1.1 查看缺失索引

-- 找出执行最慢的查询
SELECT 
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- 查看表上的索引使用情况
SELECT 
    schemaname,
    relname AS table_name,
    indexrelname AS index_name,
    idx_scan AS index_scans,
    seq_scan AS sequential_scans
FROM pg_stat_user_tables t
JOIN pg_stat_user_indexes i ON t.relid = i.relid
WHERE seq_scan > idx_scan * 10  -- 顺序扫描远多于索引扫描
ORDER BY seq_scan DESC;

1.2 创建合适的索引

-- ❌ 错误:对低区分度字段建索引(如性别)
CREATE INDEX idx_gender ON users(gender);  -- 只有2个值,索引效果不佳

-- ✅ 正确:对高区分度字段建索引
CREATE INDEX idx_email ON users(email);  -- email 唯一,区分度极高

-- ✅ 复合索引(注意字段顺序:区分度高的放前面)
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);

-- ✅ 部分索引(只索引部分数据,节省空间)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';

-- ✅ GIN 索引(用于 JSONB / 数组字段)
CREATE INDEX idx_metadata ON products USING GIN(metadata);

1.3 索引使用原则

场景 推荐索引类型
精确匹配(=) B-tree(默认)
范围查询(>, <, BETWEEN) B-tree
模糊查询(LIKE ‘%xxx%’) 不支持索引,考虑全文搜索
模糊查询(LIKE ‘xxx%’) B-tree
包含查询(JSONB) GIN
地理位置 GiST / SP-GiST

技巧2:查询优化——EXPLAIN 是你的好朋友

2.1 读懂执行计划

-- 开启 BUFFERS 可以看到 IO 情况
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE user_id = 12345;

/*
Seq Scan on orders  (cost=0.00..153.00 rows=50 width=88) (actual time=0.020..1.200 rows=48 loops=1)
  Filter: (user_id = 12345)
  Rows Removed by Filter: 99952
  Buffers: shared hit=128
*/
-- ↑ 问题:Seq Scan(全表扫描),扫描了99952行才找到48行
-- 解决:给 user_id 建索引

2.2 常见慢查询优化

-- ❌ 慢查询:使用函数导致索引失效
SELECT * FROM users WHERE DATE(created_at) = '2024-01-01';
-- DATE() 函数让索引失效,全表扫描

-- ✅ 优化:用范围查询
SELECT * FROM users 
WHERE created_at >= '2024-01-01' 
  AND created_at < '2024-01-02';

-- ❌ 慢查询:SELECT * 返回所有字段
SELECT * FROM orders WHERE user_id = 123;

-- ✅ 优化:只查需要的字段
SELECT id, status, total_amount FROM orders WHERE user_id = 123;

-- ❌ 慢查询:NOT IN 子查询
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM banned_users);

-- ✅ 优化:用 NOT EXISTS 或 LEFT JOIN
SELECT u.* FROM users u
LEFT JOIN banned_users b ON u.id = b.user_id
WHERE b.user_id IS NULL;

技巧3:配置调优——不要只用默认值

3.1 关键参数(postgresql.conf)

# 内存配置(64GB 服务器示例)
shared_buffers = 16GB          # 物理内存的 25%
effective_cache_size = 48GB     # 物理内存的 75%
work_mem = 64MB                 # 每个操作的内存,调大可加速排序/哈希
maintenance_work_mem = 2GB      # VACUUM / CREATE INDEX 时的内存

# WAL 配置(影响写入性能)
wal_buffers = 64MB              # WAL 缓冲区
checkpoint_timeout = 30min      # 检查点间隔,调大减少 IO
checkpoint_completion_target = 0.9  # 分散 IO 压力

# 并发配置
max_connections = 200           # 根据连接池调整,不要设太大

3.2 用 pgTune 自动计算参数

# 在线工具:https://pgtune.leopard.in.ua/
# 或本地运行:
docker run --rm pgtuned \
  -c "db_type=web" \
  -c "total_memory=64GB" \
  -c "cpus=16" \
  -c "connections=200"

技巧4:VACUUM 和 ANALYZE——维护表健康

-- 查看表膨胀情况
SELECT 
    schemaname,
    relname AS table_name,
    n_dead_tup,
    n_live_tup,
    round(n_dead_tup * 100.0 / (n_live_tup + n_dead_tup), 2) AS dead_percent
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_percent DESC;

-- 手动 VACUUM(回收死元组空间)
VACUUM (VERBOSE, ANALYZE) your_table;

-- 设置自动 VACUUM(在 postgresql.conf)
autovacuum_max_workers = 6       # 同时运行的 autovacuum 进程数
autovacuum_vacuum_threshold = 100  # 触发 VACUUM 的最小更新行数

技巧5:分区表——应对亿级数据

-- 创建分区主表(按时间分区)
CREATE TABLE orders (
    id BIGSERIAL,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2),
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

-- 创建分区(每月一个分区)
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- 查询时只扫描相关分区(分区裁剪)
SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31';
-- ↑ 只扫描 orders_2024_01 分区,其他分区不扫描

技巧6:主从复制——读写分离

6.1 配置主库(Primary)

# postgresql.conf(主库)
wal_level = replica              # 开启 WAL 复制
max_wal_senders = 10           # 最大复制连接数
wal_keep_size = 1GB            # 保留 WAL 文件大小
-- 创建复制用户
CREATE USER replicator REPLICATION LOGIN PASSWORD 'your_password';

-- pg_hba.conf 允许从库连接
# TYPE  DATABASE  USER         ADDRESS         METHOD
  host   replication  replicator  192.168.1.0/24   md5

6.2 配置从库(Replica)

# 从主库基础备份
pg_basebackup -h primary-host -D /var/lib/postgresql/data \
  -U replicator -P -v -R

# 启动从库(会自动以只读模式运行)
pg_ctl start -D /var/lib/postgresql/data
-- 验证复制状态(在主库执行)
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn 
FROM pg_stat_replication;

-- 在应用中实现读写分离
-- 写操作 → 主库
-- 读操作 → 从库(用连接池配置)

技巧7:连接池——减少连接开销

PostgreSQL 每个连接消耗约 10MB 内存,连接数过多会严重影响性能。

# 使用 PgBouncer 或 SQLAlchemy 连接池
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "postgresql://user:pass@localhost/mydb",
    poolclass=QueuePool,
    pool_size=20,          # 连接池大小
    max_overflow=10,       # 超出 pool_size 后最多创建的连接数
    pool_recycle=3600,     # 连接回收时间(秒)
)

性能调优检查清单

□ 是否存在全表扫描的大表?(用 pg_stat_statements 查)
□ 索引是否合理?(高区分度字段有索引,低区分度字段无冗余索引)
□ 是否启用了 autovacuum?
□ shared_buffers / effective_cache_size 是否按服务器内存调整?
□ 是否有分区表应对大表?
□ 是否配置了主从复制实现读写分离?
□ 应用是否使用了连接池?

总结

优化层次 具体措施 预期效果
SQL 层 优化慢查询、避免 SELECT * 减少 50-90% 响应时间
索引层 合理建索引、删除冗余索引 查询提速 10-100 倍
配置层 调整 postgresql.conf 提升并发处理能力
架构层 分区表 + 主从复制 支撑亿级数据

记住:先测瓶颈,再优化。用 EXPLAIN ANALYZE 和 pg_stat_statements 找到真问题。


👤 作者简介

一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~

博客:y1cloud.cn

发表评论