Skip to content

PostgreSQL 索引设计与优化

索引设计原则

索引是提高 PostgreSQL 查询性能的重要手段,但不合理的索引设计可能会导致性能下降和资源浪费。索引设计需要遵循以下原则:

1. 基于查询模式设计索引

索引应该基于实际的查询模式设计,优先为频繁使用的查询条件创建索引。

查询分析方法

  • 使用 pg_stat_statements 查看最频繁的查询
  • 分析慢查询日志,识别需要优化的查询
  • 与开发团队沟通,了解业务查询模式

示例

sql
-- 查看最频繁的查询
SELECT substring(query, 1, 100) AS query, calls, total_exec_time
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 20;

2. 选择合适的索引类型

根据查询模式和数据特征选择合适的索引类型:

  • B-tree:适用于等值查询、范围查询、排序和分组
  • Hash:适用于简单等值查询
  • GiST:适用于空间数据、全文搜索、范围类型
  • GIN:适用于多值数据类型,如数组、JSONB
  • BRIN:适用于大表中具有物理顺序相关性的列

3. 考虑索引选择性

索引选择性是指索引中唯一值的比例,选择性越高,索引的效果越好。

选择性计算公式

选择性 = 唯一值数量 / 总行数

选择性分析

sql
-- 计算列的选择性
SELECT count(DISTINCT column_name) / count(*)::float AS selectivity
FROM table_name;

4. 复合索引设计

复合索引是包含多个列的索引,设计时需要考虑以下原则:

  • 最左前缀原则:查询条件必须包含索引的最左列才能使用索引
  • 选择性优先:将选择性高的列放在前面
  • 使用频率优先:将经常一起查询的列放在一起
  • 顺序敏感:索引列的顺序会影响查询的性能

5. 避免过度索引

过度索引会导致:

  • 增加写入开销
  • 占用更多存储空间
  • 增加查询优化器的负担
  • 降低查询计划质量

索引维护建议

  • 定期审查索引使用情况
  • 删除未使用或很少使用的索引
  • 合并冗余索引

复合索引设计

1. 最左前缀原则

复合索引遵循最左前缀原则,查询条件必须包含索引的最左列才能使用索引。

示例

sql
-- 创建复合索引
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date, status);

-- 可以使用索引的查询
SELECT * FROM orders WHERE user_id = 123;
SELECT * FROM orders WHERE user_id = 123 AND order_date > '2024-01-01';
SELECT * FROM orders WHERE user_id = 123 AND order_date > '2024-01-01' AND status = 'completed';

-- 无法使用索引的查询
SELECT * FROM orders WHERE order_date > '2024-01-01';
SELECT * FROM orders WHERE status = 'completed';

2. 复合索引的列顺序

复合索引的列顺序应该基于查询模式和数据特征:

示例场景

  • 查询模式:SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY order_date DESC
  • 数据特征:user_id 选择性高,status 选择性低

优化的复合索引

sql
CREATE INDEX idx_orders_user_status_date ON orders (user_id, status, order_date DESC);

3. 复合索引的覆盖查询

覆盖查询是指查询所需的所有列都包含在索引中,不需要回表查询。覆盖查询可以显著提高查询性能。

示例

sql
-- 创建覆盖索引
CREATE INDEX idx_users_email_name ON users (email) INCLUDE (name);

-- 覆盖查询,不需要回表
SELECT name FROM users WHERE email = 'test@example.com';

索引选择性分析

1. 选择性计算

选择性是衡量索引质量的重要指标,选择性越高,索引的效果越好。

示例

sql
-- 计算列的选择性
SELECT
    column_name,
    count(DISTINCT column_name) AS distinct_values,
    count(*) AS total_rows,
    round(count(DISTINCT column_name)::float / count(*)::float, 4) AS selectivity
FROM information_schema.columns
JOIN pg_stat_user_tables ON table_name = relname
WHERE table_schema = 'public'
GROUP BY column_name, table_name
ORDER BY selectivity DESC;

2. 选择性与索引效果

选择性范围索引效果建议
> 0.1优秀适合创建索引
0.01 - 0.1良好适合创建索引
0.001 - 0.01一般考虑创建索引
< 0.001较差不建议创建索引

3. 低选择性列的索引设计

对于低选择性列,可以考虑以下优化方法:

  • 复合索引:与其他列组合创建复合索引
  • 部分索引:只对满足特定条件的行创建索引
  • 位图索引:对于低选择性列,位图索引可能比 B-tree 索引更高效

示例

sql
-- 部分索引
CREATE INDEX idx_orders_pending ON orders (order_date)
WHERE status = 'pending';

索引维护

1. 索引使用监控

定期监控索引的使用情况,识别未使用或很少使用的索引。

监控查询

sql
-- 查看索引使用情况
SELECT
    schemaname,
    relname AS table_name,
    indexrelname AS index_name,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan;

-- 查看未使用的索引
SELECT
    schemaname,
    relname AS table_name,
    indexrelname AS index_name,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
    AND pg_relation_size(indexrelid) > 1024 * 1024  -- 只显示大于 1MB 的索引
ORDER BY pg_relation_size(indexrelid) DESC;

2. 索引膨胀管理

索引膨胀是指索引中包含大量未使用的空间,导致索引体积增大,查询性能下降。

检查索引膨胀

sql
-- 安装 pgstattuple 扩展
CREATE EXTENSION IF NOT EXISTS pgstattuple;

-- 检查索引膨胀
SELECT
    indexrelname AS index_name,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
    pgstattuple(indexrelid) AS tuple_stats
FROM pg_indexes
WHERE schemaname = 'public';

重建索引

sql
-- 重建单个索引
REINDEX INDEX idx_users_email;

-- 重建表的所有索引
REINDEX TABLE users;

-- 在线重建索引(不阻塞写入)
REINDEX INDEX CONCURRENTLY idx_users_email;

-- 重建数据库的所有索引
REINDEX DATABASE app_db;

3. 定期分析表

定期执行 ANALYZE 命令更新表的统计信息,帮助优化器选择更好的执行计划。

分析命令

sql
-- 分析单个表
ANALYZE users;

-- 分析所有表
ANALYZE;

-- 使用 VERBOSE 选项查看分析进度
ANALYZE VERBOSE users;

执行计划分析

1. 使用 EXPLAIN 分析执行计划

EXPLAIN 命令可以显示查询的执行计划,帮助识别索引使用情况和性能瓶颈。

基本用法

sql
-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';

-- 查看执行计划并实际执行查询
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';

-- 查看详细的执行计划
EXPLAIN (ANALYZE, VERBOSE, COSTS, BUFFERS, TIMING)
SELECT * FROM users WHERE email = 'test@example.com';

2. 执行计划解读

执行计划包含以下关键信息:

  • 节点类型:如 Seq Scan(顺序扫描)、Index Scan(索引扫描)、Bitmap Index Scan(位图索引扫描)
  • 成本估算:启动成本和总成本
  • 行估算:预计返回的行数
  • 实际执行时间EXPLAIN ANALYZE 显示的实际执行时间
  • 缓冲区使用情况:共享缓冲区、本地缓冲区和临时缓冲区的使用情况

执行计划示例

Index Scan using idx_users_email on users  (cost=0.29..8.30 rows=1 width=100) (actual time=0.020..0.021 rows=1 loops=1)
  Index Cond: (email = 'test@example.com'::text)
  Buffers: shared hit=2
Planning Time: 0.058 ms
Execution Time: 0.037 ms

3. 识别索引问题

从执行计划中可以识别以下索引问题:

  • 顺序扫描:当应该使用索引时却使用了顺序扫描
  • 位图索引扫描:可能表示索引选择性不高
  • 嵌套循环连接:可能需要优化连接顺序或使用更高效的连接方法
  • 大量缓冲区读取:可能表示索引不够高效或内存不足

索引优化案例

1. 案例:复合索引优化

问题:查询 SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY order_date DESC 执行缓慢。

分析

  • 表中有 user_idstatus 的单独索引
  • 执行计划显示使用了 Seq Scan

解决方案

sql
-- 创建复合索引
CREATE INDEX idx_orders_user_status_date ON orders (user_id, status, order_date DESC);

优化效果

  • 查询使用 Index Scan 替代了 Seq Scan
  • 查询时间从 100ms 减少到 1ms

2. 案例:部分索引优化

问题:查询 SELECT * FROM orders WHERE status = 'pending' AND order_date > ? 执行缓慢。

分析

  • status 列选择性低
  • 只有 10% 的行状态为 'pending'

解决方案

sql
-- 创建部分索引
CREATE INDEX idx_orders_pending_date ON orders (order_date)
WHERE status = 'pending';

优化效果

  • 索引体积减少了 90%
  • 查询时间从 50ms 减少到 2ms

3. 案例:覆盖索引优化

问题:查询 SELECT name, email FROM users WHERE email LIKE '%@example.com' 执行缓慢。

分析

  • 执行计划显示使用了 Index Scan 但需要回表查询

解决方案

sql
-- 创建覆盖索引
CREATE INDEX idx_users_email_name ON users (email) INCLUDE (name);

优化效果

  • 查询使用 Index Only Scan 替代了 Index Scan
  • 不需要回表查询,减少了 I/O 操作
  • 查询时间从 20ms 减少到 5ms

最佳实践

1. 索引设计

  • 基于实际查询模式设计索引
  • 选择合适的索引类型
  • 考虑索引选择性
  • 合理设计复合索引
  • 避免过度索引

2. 索引维护

  • 定期监控索引使用情况
  • 重建膨胀的索引
  • 定期分析表,更新统计信息
  • 删除未使用或很少使用的索引

3. 执行计划分析

  • 定期分析执行计划
  • 识别索引问题并及时优化
  • 与开发团队合作,优化查询语句

4. 开发规范

  • 为频繁使用的查询条件创建索引
  • 避免在索引列上使用函数或表达式
  • 合理使用复合索引
  • 考虑索引的写入开销

常见问题(FAQ)

Q1: 如何确定需要创建索引的列?

A1: 可以通过以下方法确定需要创建索引的列:

  1. 分析慢查询日志,识别频繁执行的查询
  2. 使用 pg_stat_statements 查看最消耗资源的查询
  3. 与开发团队沟通,了解业务查询模式
  4. 检查执行计划,识别顺序扫描的查询
  5. 计算列的选择性,选择性高的列更适合创建索引

Q2: 复合索引的列顺序如何确定?

A2: 复合索引的列顺序应该基于以下原则:

  1. 选择性优先:将选择性高的列放在前面
  2. 使用频率优先:将经常一起查询的列放在一起
  3. 范围查询后置:将范围查询的列放在后面
  4. 排序和分组列放在最后:将用于排序和分组的列放在复合索引的最后

Q3: 如何处理索引膨胀问题?

A3: 处理索引膨胀问题的方法包括:

  1. 重建索引:使用 REINDEX 命令重建索引
  2. 在线重建:使用 REINDEX CONCURRENTLY 在线重建索引,不阻塞写入
  3. 使用 pg_repack:pg_repack 是一个第三方扩展,可以在线重建表和索引,减少膨胀
  4. 调整 autovacuum 设置:优化 autovacuum 参数,及时清理死元组

Q4: 为什么执行计划显示不使用索引?

A4: 执行计划显示不使用索引的原因可能包括:

  1. 表很小,顺序扫描比索引扫描更高效
  2. 索引选择性低,索引扫描的成本高于顺序扫描
  3. 查询条件中没有使用索引的最左列
  4. 查询条件中在索引列上使用了函数或表达式
  5. 统计信息过时,优化器无法准确估算成本
  6. 查询需要返回表的大部分行

Q5: 如何优化 ORDER BY 查询?

A5: 优化 ORDER BY 查询的方法包括:

  1. 创建包含排序列的索引
  2. 对于复合索引,将排序列放在最后
  3. 使用索引的排序方向与查询的排序方向一致
  4. 考虑使用覆盖索引,避免回表查询
  5. 对于大表,可以考虑分区表

Q6: 索引对写入性能有影响吗?

A6: 是的,索引会影响写入性能。每次插入、更新或删除操作都会导致索引更新,增加写入开销。索引越多,写入开销越大。因此,在设计索引时需要权衡查询性能和写入性能,避免创建过多不必要的索引。