
为什么你的 PostgreSQL 越来越慢?
很多站长在网站初期选择 PostgreSQL(一种开源关系型数据库)作为数据存储方案,初期运行流畅,但随着数据量增长到几十万甚至上百万行,查询响应时间从毫秒级飙升到秒级,用户反馈页面加载卡顿。这类问题的根源往往不是硬件不够,而是索引设计不合理、连接池配置缺失、查询语句存在性能瓶颈。
本文将从三个核心维度——索引优化、连接池管理、查询调优——出发,结合实际可执行的配置和命令,帮助你系统性地解决 PostgreSQL 性能问题。无论你是在 VPS(虚拟专用服务器) 上跑小型项目,还是在独立服务器上承载企业级应用,这些方法都适用。
索引优化:让查询速度提升 10 倍的关键
索引是 PostgreSQL 性能调优的第一道门槛。没有索引的查询,数据库需要逐行扫描整张表(全表扫描),当表数据超过 10 万行时,这种操作的耗时会急剧增加。
B-tree 索引:默认选择的正确用法
PostgreSQL 默认使用 B-tree 索引,适合等值查询(=)和范围查询(BETWEEN、>、<)。创建索引的基本语法:
CREATE INDEX idx_users_email ON users (email);
但很多人忽略了一个关键点:复合索引的列顺序决定查询效率。如果你的查询条件经常是 WHERE status = 'active' AND created_at > '2026-01-01',那么索引应该定义为:
CREATE INDEX idx_users_status_created ON users (status, created_at);
把等值条件的列放在前面,范围条件的列放在后面,这样 PostgreSQL 可以先通过 status 快速定位到一个子集,再在子集内用 created_at 做范围筛选。
部分索引:只索引你需要的数据
如果你的查询经常只针对表中的一小部分数据(比如只查活跃用户、未处理订单),部分索引(Partial Index)可以大幅减少索引体积和维护成本:
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';
这个索引只包含 status = 'pending' 的行,相比全表索引,体积可能缩小 90% 以上,写入性能开销也相应降低。
覆盖索引:让查询只读索引不读表
当查询的所有列都包含在索引中时,PostgreSQL 可以直接从索引返回结果,不需要回表读取数据文件,这就是覆盖索引(Covering Index):
CREATE INDEX idx_products_covering ON products (category_id, price)
INCLUDE (name, stock);
查询 SELECT name, stock FROM products WHERE category_id = 5 AND price < 100 时,数据库只扫描索引就能拿到所有数据,IO 开销降到最低。

关于索引设计,我们的经验是:先用 EXPLAIN ANALYZE 确认慢查询的执行计划,再针对性地创建索引,而不是盲目给每列都加索引。索引不是越多越好——每增加一个索引,写入操作(INSERT/UPDATE/DELETE)的性能就会下降,因为数据库需要同时维护表数据和索引结构。更多服务器性能优化思路,可以参考 服务器优化指南。
连接池配置:避免数据库被连接数压垮
PostgreSQL 的连接管理是另一个常见的性能瓶颈。每个数据库连接在 PostgreSQL 中对应一个独立的操作系统进程,创建和销毁进程的开销不小。当并发连接数从 20 涨到 200 时,你会发现 CPU 使用率飙升,但实际查询吞吐量反而下降。
为什么需要连接池?
直接用应用代码管理数据库连接,常见的问题是:每个请求创建一个连接,请求结束后关闭连接。在高并发场景下,数据库会花大量时间在”创建连接→销毁连接”的循环中,而不是执行真正的查询。连接池的作用就是在应用和数据库之间维护一组复用的连接,避免反复创建和销毁。
PgBouncer 实战配置
PgBouncer 是 PostgreSQL 生态中最常用的连接池工具。以下是一个适合中小型应用的配置:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 20
max_client_conn = 200
min_pool_size = 5
reserve_pool_size = 3
关键参数解读:
pool_mode = transaction:每个事务结束后连接归还池中,比session模式更高效,适合大多数 Web 应用default_pool_size = 20:每个用户-数据库对维护 20 个后端连接,足够应对日常并发max_client_conn = 200:前端最多接受 200 个客户端连接,但后端实际只用 20 个连接服务它们

配置完成后,应用代码中把数据库连接地址从 5432 改成 6432 即可。对于部署在 独立服务器 上的高并发应用,连接池几乎是必选项。
查询优化:用 EXPLAIN 找到真正的瓶颈
索引和连接池解决的是”基础设施”层面的问题,但很多时候慢查询的根源在于 SQL 语句本身写得不够高效。PostgreSQL 提供了 EXPLAIN ANALYZE 工具,可以让你看到每条查询的真实执行计划。
读懂 EXPLAIN ANALYZE 输出
运行以下命令:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 12345 ORDER BY created_at DESC LIMIT 10;
输出中需要关注几个关键指标:
Seq Scan(顺序扫描):表示全表扫描,如果出现在大表上,通常意味着缺少合适的索引Index Scan(索引扫描):使用了索引,这是理想状态actual time:实际执行时间,第一个数字是首行返回时间,第二个是全部返回时间rows:估计扫描行数 vs 实际扫描行数,如果差距大,说明统计信息过时
常见的查询反模式
在实际项目中,以下几种写法最容易导致性能问题:
在 WHERE 子句中对列做函数运算
-- 慢:无法使用索引
SELECT * FROM users WHERE YEAR(created_at) = 2026;
-- 快:利用索引
SELECT * FROM users WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
SELECT 取所有列
-- 慢:传输大量不需要的数据
SELECT * FROM products WHERE category_id = 5;
-- 快:只取需要的列
SELECT id, name, price FROM products WHERE category_id = 5;
N+1 查询问题
应用代码中循环执行单条查询,比如先查出 100 个用户 ID,再逐个查每个用户的订单数量。正确做法是用 JOIN 或子查询一次性完成:
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
GROUP BY u.id, u.name;

这些查询优化技巧配合前面的索引设计,通常能让查询性能提升 5-50 倍。如果你的应用部署在云服务器(一种按需分配计算资源的托管服务)上,查询优化还能直接降低 CPU 和内存消耗,节省运维成本。更多实用的网站性能优化方法,可以参考 网站优化专栏。
监控与日常维护:让性能持续在线
调优不是一次性工作,PostgreSQL 的性能会随着数据增长和业务变化而退化。建立一套持续监控和定期维护的机制,才能让数据库长期保持健康状态。
pg_stat_statements:找到最慢的查询
启用 pg_stat_statements 扩展后,PostgreSQL 会自动记录所有查询的执行统计。首先在 postgresql.conf 中启用:
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
重启数据库后,执行以下查询找出执行时间最长的 Top 10 慢查询:
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
这个结果会告诉你哪些查询消耗了最多的时间,优先优化这些查询的收益最大。
VACUUM 和 ANALYZE:保持统计信息准确
PostgreSQL 使用 MVCC(多版本并发控制)机制,每次 UPDATE 或 DELETE 操作不会立即释放旧数据占用的空间,而是标记为”可回收”。VACUUM 命令负责清理这些死元组,释放空间供新数据使用。
-- 手动清理单张表
VACUUM ANALYZE orders;
-- 查看表的死元组比例
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup::numeric / GREATEST(n_live_tup, 1) * 100, 2) AS dead_ratio
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
建议配置 autovacuum 参数,让数据库自动执行清理:
autovacuum = on
autovacuum_vacuum_scale_factor = 0.1
autovacuum_analyze_scale_factor = 0.05
这两个参数的含义是:当表中死元组比例超过 10% 时自动触发 VACUUM,当数据变化比例超过 5% 时自动触发 ANALYZE 更新统计信息。
连接数监控
定期检查当前连接数是否接近上限:
SELECT count(*) AS current_connections,
setting AS max_connections
FROM pg_stat_activity, pg_settings
WHERE pg_settings.name = 'max_connections'
GROUP BY setting;
如果当前连接数长期超过最大连接数的 80%,说明要么需要调大 max_connections,要么需要优化连接池配置。

总结与下一步行动建议
PostgreSQL 性能调优是一个系统工程,涉及索引设计、连接池管理、查询优化和持续监控四个层面。如果你目前只做一件事,建议从以下步骤开始:
- 先找到瓶颈:启用
pg_stat_statements,定位 Top 5 慢查询 - 针对性优化:对慢查询运行
EXPLAIN ANALYZE,检查是否缺少索引、是否存在全表扫描 - 配置连接池:如果你的应用并发超过 50,立即部署 PgBouncer
- 建立监控:定期检查死元组比例和连接数使用率,提前发现性能退化
如果你需要一台稳定可靠的数据库服务器来运行 PostgreSQL,可以考虑 Hostease 的 VPS 主机方案,支持 SSD 存储和灵活的资源配置,适合中小型项目的数据库部署需求。