前置阅读:建议先阅读 PostgreSQL 性能调优。
关键概念:PostgreSQL 使用基于成本的查询优化器(CBO),EXPLAIN 输出的是优化器估算的成本(非时间单位),EXPLAIN ANALYZE 则执行查询并输出实际时间和行数。
² EXPLAIN 输出深度解读
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.created_at > '2024-01-01' AND u.status = 'active' GROUP BY u.id, u.name HAVING COUNT(o.id) > 5 ORDER BY order_count DESC LIMIT 20;典型的执行计划节点解读:
Limit (cost=1234.56..1234.76 rows=20 width=25) (actual time=45.23..45.89 rows=20 loops=1) -> Sort (cost=1234.56..1289.12 rows=21824 width=25) (actual time=45.21..45.45 rows=20 loops=1) Sort Key: (count(o.id)) DESC Sort Method: top-N heapsort Memory: 30kB -> GroupAggregate (cost=0.42..890.34 rows=21824 width=25) (actual time=12.34..42.18 rows=1847 loops=1) Group Key: u.id, u.name -> Nested Loop Left Join (cost=0.42..456.78 rows=21824 width=21) (actual time=0.12..28.45 rows=50020 loops=1) -> Index Scan using idx_users_created_at on users u (cost=0.42..234.56 rows=21824 width=17) (actual time=0.08..5.23 rows=1847 loops=1) Index Cond: (created_at > '2024-01-01') Filter: (status = 'active') Rows Removed by Filter: 1234 -> Index Scan using idx_orders_user_id on orders o (cost=0.00..0.01 rows=2 width=4) (actual time=0.01..0.01 rows=27 loops=1847)关键指标解读:
指标 含义 警报阈值 cost=...优化器估算的单位成本 - actual time=...实际执行时间(ms) 单次 > 100ms rows=...估算/实际返回行数 估算偏差 > 10x loops=...该节点执行次数 Nested Loop 内层过高 Buffers: shared read=...共享缓冲区读取块数 大量 read= 缺索引 常见问题信号:
信号 含义 修复方向 Seq Scan大表全表扫描 加索引、VACUUM ANALYZE Rows Removed by Filter高索引后大量过滤 复合索引覆盖过滤条件 Sort Method: external merge内存不足外排 增 work_mem、加排序索引 Nested Loop驱动表大错误的连接策略 分析统计信息、强制 Hash Join ³ 索引策略矩阵
索引类型 适用场景 不支持 示例 B-Tree 等值、范围、排序、LIKE ‘prefix%’ 全文搜索 WHERE id = 1Hash 仅等值查询(不排序) 范围、排序 WHERE hash_field = 'abc'GiST 地理空间、范围类型、模糊搜索 - PostGIS 几何查询 GIN 数组、JSONB、全文搜索 范围查询 WHERE tags @> ARRAY['ai']BRIN 大块有序数据(时序) 随机分布 月份分区日志表 -- B-Tree 复合索引:列顺序关键(最左前缀) CREATE INDEX idx_users_status_created ON users (status, created_at DESC); -- 支持:WHERE status = 'active' ORDER BY created_at DESC -- 不支持:WHERE created_at > '2024-01-01'(status 不在条件中) -- GIN 索引:JSONB 搜索 CREATE INDEX idx_products_attrs ON products USING GIN (attributes); -- 支持:WHERE attributes @> '{"color": "blue"}' -- 部分索引:仅索引热点数据 CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending'; -- 体积更小,查询 WHERE status = 'pending' 时自动使用 -- 覆盖索引:包含查询所需全部列,避免回表 CREATE INDEX idx_users_covering ON users (status, created_at) INCLUDE (name, email); -- Query: SELECT name, email FROM users WHERE status = 'active' ORDER BY created_at⁴ 慢查询治理流程
-- 1. 启用 pg_stat_statements CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- 2. 发现 Top 慢查询(按总时间排序) SELECT substring(query, 1, 80) as query_preview, calls, round(total_exec_time::numeric, 2) as total_ms, round(mean_exec_time::numeric, 2) as avg_ms, round(stddev_exec_time::numeric, 2) as stddev_ms, rows as total_rows, 100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20; -- 3. 重置统计(调优后) SELECT pg_stat_statements_reset();诊断 → 修复 决策树:
慢查询发现 │ ├─ 行数估算偏差大? → ANALYZE table_name(更新统计信息) │ ├─ 使用 Seq Scan? → 检查 WHERE 条件列是否有索引 │ ├─ 单列查询 → B-Tree 单列索引 │ ├─ 多列查询 → 复合索引(最左匹配) │ └─ 过滤条件恒定 → 部分索引 │ ├─ 回表次数高? → 覆盖索引 (INCLUDE) / 减少 SELECT * │ ├─ 排序用外部磁盘? → 增 work_mem / 添加排序列索引 │ ├─ Join 策略不佳? → 调整 enable_nestloop / analyze │ └─ 查询本身复杂? → 物化视图 / 结果缓存 / 业务拆分⁵ 真实生产案例
案例 1:时间范围 + 状态查询
-- 原始查询(800ms) SELECT * FROM events WHERE event_type = 'error' AND created_at > NOW() - INTERVAL '7 days' ORDER BY created_at DESC LIMIT 100; -- 问题:单列索引 idx_events_created_at 不覆盖 event_type 过滤 -- 修复:复合索引(排序列在后) CREATE INDEX idx_events_type_created ON events (event_type, created_at DESC); -- 结果:12ms(提升 66x)案例 2:JSONB 字段搜索
-- 原始查询(2s+) SELECT * FROM documents WHERE metadata->>'department' = 'engineering'; -- 问题:->> 操作导致全表扫描 -- 修复:GIN 索引 + 表达式索引 CREATE INDEX idx_documents_metadata_department ON documents ((metadata->>'department')); -- 结果:5ms(提升 400x)案例 3:分页深翻页性能下降
-- 原始 LIMIT/OFFSET 深翻页(OFFSET 1000000 耗时 5s) SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 1000000; -- 修复:键集分页(Keyset Pagination) SELECT * FROM orders WHERE created_at < '2024-03-01T12:00:00Z' -- 上一页最后一条的时间 ORDER BY created_at DESC LIMIT 20; -- 结果:恒定 < 10ms⁶ 配置调优速查
# postgresql.conf # 内存配置(总 RAM = 32GB) shared_buffers = 8GB # 25% RAM effective_cache_size = 24GB # 75% RAM(估算 OS 缓存) work_mem = 64MB # 复杂排序/Hash 每操作 maintenance_work_mem = 1GB # VACUUM/CREATE INDEX # 查询优化器 random_page_cost = 1.1 # SSD 上接近 seq_page_cost effective_io_concurrency = 200 # SSD 并发读取 default_statistics_target = 100 # 提升统计精度(更慢 ANALYZE) # 日志慢查询 log_min_duration_statement = 100 # 记录 > 100ms 的查询 log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
延伸阅读:
- PostgreSQL 性能调优 — 参数调优与连接池
- PostgreSQL 高可用架构 — 读写分离对查询优化的影响
- PostgreSQL 扩展与扩展性指南 — pg_stat_statements 与自动调优工具
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。