性能问题的根源往往在索引
当应用变慢,第一反应常是“加内存、换 SSD”。但在绝大多数业务系统里,性能瓶颈其实出在 SQL 和索引上:一个没命中索引的全表扫描,能让 32 核服务器也卡得像单核。PostgreSQL 是功能最强大的开源关系数据库,但要榨干它的性能,必须理解索引策略和执行计划。
用 EXPLAIN 看清数据库在做什么
调优的第一步永远是看执行计划。EXPLAIN ANALYZE 不仅显示预估成本,还会真正执行并给出实际耗时。
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;读懂关键字段:
ANALYZE。Limit (cost=0.42..12.50 rows=20 ...) (actual time=0.020..0.045 rows=20 loops=1)
-> Index Scan using orders_user_created_idx on orders (...)
Index Cond: (user_id = 42)
Filter: (status = 'paid')
Rows Removed by Filter: 5上面的计划说明:走了索引,但有 Filter(非索引列过滤)和被过滤掉的行。如果能建对复合索引,连 Filter 都可以省掉。
B-tree 索引与最左前缀
PostgreSQL 默认索引类型是 B-tree,适合等值、范围、排序。复合索引遵循最左前缀原则:查询条件必须从索引最左列开始连续使用。
-- 复合索引
CREATE INDEX idx_orders_user_status_time
ON orders (user_id, status, created_at);
-- 命中:user_id
-- 命中:user_id, status
-- 命中:user_id, status, created_at(完全匹配,最优)
-- 不命中:status, created_at(缺少最左列 user_id)
SELECT * FROM orders
WHERE user_id = 42 AND status = 'paid'
ORDER BY created_at DESC;这条查询能完全利用上面的复合索引:等值条件走索引定位,created_at 既能作为范围/排序用,又能实现 Index Only Scan。索引列顺序的设计原则是:等值条件列在前,范围/排序列在后。
索引不是越多越好
每个索引都会拖慢写入(INSERT/UPDATE/DELETE 要同步维护索引),并占用存储。建立索引前问自己:
-- 查看索引大小和使用情况
SELECT schemaname, relname, indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan AS scans
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;idx_scan 为 0 的索引可能从未被使用,是清理候选。
特殊索引类型
PostgreSQL 的杀手锏是丰富的索引类型:
-- JSONB 字段加 GIN 索引
CREATE INDEX idx_products_attrs ON products USING gin (attrs);
SELECT * FROM products WHERE attrs @> '{"color": "red"}';
-- 只索引活跃用户,省空间
CREATE INDEX idx_orders_active
ON orders (user_id)
WHERE status = 'active';
-- 时序表用 BRIN,体积极小
CREATE INDEX idx_logs_time ON logs USING brin (created_at);避免常见性能反模式
-- 反模式 1:函数包裹列,索引失效
SELECT * FROM users WHERE lower(email) = 'a@b.com';
-- 改用表达式索引
CREATE INDEX idx_users_email_lower ON users (lower(email));
-- 反模式 2:SELECT *,浪费 I/O 还可能阻止 Index Only Scan
SELECT id, name FROM users WHERE user_id = 42;
-- 反模式 3:隐式类型转换
SELECT * FROM orders WHERE order_no = 12345; -- order_no 是 varchar
-- 应传字符串:order_no = '12345'
-- 反模式 4:OFFSET 分页,深度翻页越来越慢
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;
-- 改用游标分页(keyset pagination)
SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 20;游标分页(keyset pagination)是深分页的终极解法:始终基于上一页最后一条记录的索引值定位,复杂度恒定 O(limit)。
连接与子查询优化
-- 子查询改写成 JOIN 往往更快
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE vip = true);
-- 优化为 JOIN
SELECT o.*
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.vip = true;但注意:现代 PostgreSQL 对 IN 子查询的优化已经很好,不要盲目改写,以 EXPLAIN ANALYZE 的实际结果为准。
统计信息与 VACUUM
查询计划依赖统计信息。数据大量变更后,统计信息会失真,导致选错索引。
-- 手动更新统计信息
ANALYZE orders;
-- PostgreSQL 用 MVCC,删除/更新产生“死元组”,需 VACUUM 回收
VACUUM (ANALYZE) orders;
-- 配置 autovacuum 自动维护
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.1,
autovacuum_analyze_scale_factor = 0.05
);大表的 autovacuum 阈值要调小,否则死元组堆积会导致查询变慢、表膨胀。
配置层面的调优
# postgresql.conf 关键参数
shared_buffers = 4GB # 通常为内存的 25%
effective_cache_size = 12GB # 操作系统缓存预估,通常为内存的 50-75%
work_mem = 64MB # 单个排序/哈希的内存,注意并发乘数
maintenance_work_mem = 1GB # VACUUM/CREATE INDEX 的内存
random_page_cost = 1.1 # SSD 上应调低,鼓励走索引random_page_cost 在 SSD 上从默认的 4 调到 1.1,能让规划器更倾向使用索引,这是最常被忽略的一条。
慢查询排查流程
log_min_duration_statement = 200(记录 >200ms 的语句)。pg_stat_statements 找出累计耗时最高的查询。EXPLAIN ANALYZE,看是否 Seq Scan、Sort 落盘。SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;小结
PostgreSQL 性能优化是“看计划、建对索引、写对 SQL、调好配置”的循环。核心要点:用 EXPLAIN ANALYZE 诊断、复合索引遵循最左前缀且等值在前范围在后、善用 GIN/BRIN/部分索引、避免函数包裹列和深分页 OFFSET、保持统计信息与 autovacuum 健康。记住:索引是读优化的杠杆,但每一次新增都要用 EXPLAIN 验证它真的被用上了。养成看执行计划的习惯,比任何调优技巧都重要。
💬 评论区 (0)
暂无评论,快来抢沙发吧!