在 PostgreSQL 中,索引是提升查询性能最有效的手段之一。然而,“盲目建索引”不仅无法提升性能,反而会拖慢写入速度、浪费存储空间、增加维护成本。优秀的索引设计需要结合数据分布、查询模式、业务场景进行系统性思考。

本文将从 索引类型选择、列顺序设计、复合索引策略、部分索引应用、统计信息管理、反模式识别 六大维度,深入剖析 PostgreSQL 索引设计的核心原则,并提供可落地的最佳实践。
适用场景:
=)>, <, BETWEEN)ORDER BY)LIKE 'abc%')内部结构:
创建语法:
CREATE INDEX idx_orders_user_id ON orders(user_id);
注意:PostgreSQL 的 B-tree 索引默认不存储 NULL 值(但可通过
IS NOT NULL条件使用部分索引覆盖)。
适用场景:
=)优势:
局限:
ORDER BYDISTINCT 优化创建语法:
CREATE INDEX idx_users_email_hash ON users USING HASH(email);
建议:除非明确测试证明 Hash 更优,否则优先使用 B-tree。
适用场景:
array @> ARRAY[1])data @> '{"key": "value"}')tsvector @@ tsquery)pg_trgm 模糊匹配(name LIKE '%alice%')特点:
创建示例:
-- JSONB 索引CREATE INDEX idx_products_attrs_gin ON products USING GIN(attributes);-- 全文检索CREATE INDEX idx_articles_fts ON articles USING GIN(to_tsvector('english', content));-- 模糊搜索(需 pg_trgm 扩展)CREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX idx_users_name_trgm ON users USING GIN(name gin_trgm_ops);适用场景:
ltree(树形路径)与 GIN 对比:
创建示例:
-- 全文检索(GiST 版本)CREATE INDEX idx_articles_fts_gist ON articles USING GiST(to_tsvector('english', content));-- ltree 路径索引CREATE INDEX idx_categories_path ON categories USING GiST(path);适用场景:
created_at > '2026-01-01')原理:
优势:
创建示例:
-- 时间序列表CREATE INDEX idx_logs_created_brin ON logs USING BRIN(created_at);
建议:日志、监控、IoT 数据等场景首选 BRIN。
索引不是为表设计的,而是为查询语句设计的。
通过以下方式识别关键查询:
pg_stat_statements 扩展-- 启用 pg_stat_statementsCREATE EXTENSION pg_stat_statements;-- 查看最耗时的查询SELECT query, calls, total_exec_time, rowsFROM pg_stat_statementsORDER BY total_exec_time DESCLIMIT 10;
原则:索引应覆盖 WHERE 子句中的过滤条件。
-- 查询:SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';-- 推荐索引:CREATE INDEX idx_orders_user_status ON orders(user_id, status);
注意:若
status只有少数几个值(如 ‘paid’, ‘pending’),将其放在复合索引第二位可提升选择性。
问题:索引扫描后仍需回表(Heap Fetch)获取其他列,增加 I/O。
解决方案:使用 INCLUDE 子句(PostgreSQL 11+)将非过滤列加入索引。
-- 查询:SELECT order_id, total FROM orders WHERE user_id = 123;-- 普通索引:CREATE INDEX idx_orders_user_id ON orders(user_id); -- 需回表取 total-- 覆盖索引:CREATE INDEX idx_orders_user_id_covering ON orders(user_id) INCLUDE (total);-- 执行计划:Index Only Scan(无需回表)
优势:
- 减少 I/O
- 提升缓存命中率
- 适用于只读或低频更新列
复合索引的性能高度依赖列的顺序。遵循 “等值列在前,范围列在后” 原则。
B-tree 复合索引 (a, b, c) 可用于:
WHERE a = ?WHERE a = ? AND b = ?WHERE a = ? AND b = ? AND c = ?WHERE a = ? AND b = ? ORDER BY c但不能用于:
WHERE b = ?WHERE c = ?WHERE b = ? AND c = ?查询条件中有哪些列?├─ 全是等值(=) → 任意顺序(建议高选择性列在前)├─ 含范围(>, <, BETWEEN) → 等值列在前,范围列在后└─ 含排序(ORDER BY) → 将排序列放在最后(若前面是等值)
-- 查询:WHERE user_id = 123 AND created_at > '2026-01-01'-- 正确顺序:(user_id, created_at)CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);-- 错误顺序:(created_at, user_id) → created_at 范围扫描后仍需过滤 user_id
-- 查询:WHERE status = 'paid' ORDER BY created_at DESC LIMIT 10-- 推荐索引:(status, created_at DESC)CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);-- 可实现 Index Scan + Limit,避免 Sort
注意:PostgreSQL 11+ 支持
NULLS FIRST/LAST和降序索引,可精确匹配ORDER BY。
当查询只关注数据子集时,部分索引可大幅减小索引体积并提升效率。
status = 'active')created_at > current_date - interval '30 days')email IS NOT NULL)-- 场景:90% 的订单是 'completed',但常查 'pending'CREATE INDEX idx_orders_pending ON orders(user_id)WHERE status = 'pending';-- 查询必须包含相同条件才能使用索引SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
警告:查询条件必须完全匹配部分索引的
WHERE子句,否则无法使用。
每个索引都有代价:
-- 查看从未使用的索引SELECT schemaname, tablename, indexname, idx_scanFROM pg_stat_user_indexesWHERE idx_scan = 0ORDER BY schemaname, tablename;-- 查看低效索引(扫描次数远低于表大小)SELECT schemaname, tablename, indexname, idx_scan, pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_sizeFROM pg_stat_user_indexesWHERE idx_scan < 100 -- 阈值根据业务调整ORDER BY pg_relation_size(indexname::regclass) DESC;
常见冗余情况:
建议:定期审计索引使用情况,删除无用索引。
索引能否被使用,最终由查询优化器决定。而优化器依赖统计信息和成本参数。
-- 手动更新统计信息(大批量导入后执行)ANALYZE table_name;-- 调整自动分析阈值ALTER TABLE orders SET ( autovacuum_analyze_scale_factor = 0.05, -- 默认 0.1 autovacuum_analyze_threshold = 500 -- 默认 50);
-- SSD 随机读接近顺序读,降低 random_page_costSET random_page_cost = 1.1; -- 默认 4.0(机械盘)-- 若内存充足,可降低 cpu_tuple_costSET cpu_tuple_cost = 0.005; -- 默认 0.01
建议:在 SSD 服务器上,将
random_page_cost设为 1.1~1.3。
-- 性别只有 'M'/'F',索引几乎无效CREATE INDEX idx_users_gender ON users(gender);
判断标准:n_distinct / 表行数 < 0.01(即唯一值占比 < 1%)
orders.user_id 常用于查询,应建索引;但 order_items.order_id 作为主表关联,若不单独查询,可不建IS NULL,需单独建部分索引:CREATE INDEX idx_users_phone_null ON users((1)) WHERE phone IS NULL;
-- 索引CREATE INDEX idx_users_upper_email ON users(UPPER(email));-- 查询必须完全一致SELECT * FROM users WHERE UPPER(email) = '[email protected]'; -- ✅SELECT * FROM users WHERE UPPER(email) = lower('[email protected]'); -- ❌ 不匹配
有时,改写查询比建索引更有效。
-- 原查询(可能无法使用索引)SELECT * FROM users WHERE email = 'a' OR name = 'Alice';-- 优化后(每个分支独立使用索引)SELECT * FROM users WHERE email = 'a'UNIONSELECT * FROM users WHERE name = 'Alice';
-- 原查询SELECT * FROM logs WHERE DATE(created_at) = '2026-01-25';-- 优化后(使用范围)SELECT * FROM logs WHERE created_at >= '2026-01-25' AND created_at < '2026-01-26';
-- 原查询(需回表)SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;-- 若 orders 表很大,可建覆盖索引CREATE INDEX idx_orders_user_covering ON orders(user_id) INCLUDE (order_id);-- 执行计划:Index Only Scan + GroupAggregate
在创建索引前,问自己以下问题:
遵循这些原则,你将能设计出高效、精简、可维护的索引体系,在查询性能与写入成本之间取得最佳平衡。记住:好的索引不是越多越好,而是恰到好处。