在现代应用开发中,PostgreSQL 凭借其开源、功能全面、扩展性强等特点,已经成为不少业务场景的主力数据库。不过,一个常见的问题是:明明建了索引,查询却慢得让人头疼。原因说到底就一个——索引“失效”了。当然,从严格意义上讲,PostgreSQL 的索引并不会真的物理失效,而是查询优化器在执行计划
在现代应用开发中,PostgreSQL 凭借其开源、功能全面、扩展性强等特点,已经成为不少业务场景的主力数据库。不过,一个常见的问题是:明明建了索引,查询却慢得让人头疼。原因说到底就一个——索引“失效”了。当然,从严格意义上讲,PostgreSQL 的索引并不会真的物理失效,而是查询优化器在执行计划中没有选择走索引,转而选择了更慢的全表扫描(Seq Scan)。这种情况通常与查询写法、数据分布、统计信息或索引类型不匹配有关。
这篇文章整理了几个实战中摸爬滚打总结出来的技巧,希望能帮开发者和 DBA 在系统设计阶段就考虑到索引的有效性,真正规避那些“坑”。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
先说说最常见的坑:在索引列上直接套函数或者做计算。比如 UPPER()、TO_CHAR(),或者 col + 1 这类操作。PostgreSQL 的 B-tree 索引存的是原始值,不是计算后的值,所以它没法直接用。
错误示例:
-- 假设 users 表有 email 列,并在 email 上建了普通索引 CREATE INDEX idx_users_email ON users(email); -- 查询时使用 UPPER 函数 SELECT * FROM users WHERE UPPER(email) = 'USER@EXAMPLE.COM';
这个查询,即使 email 有索引,优化器也不会用。因为索引里存的是 'user@example.com',不是 'USER@EXAMPLE.COM'。
正确做法是创建一个函数索引:
-- 创建基于 UPPER(email) 的函数索引 CREATE INDEX idx_users_email_upper ON users(UPPER(email)); -- 现在查询可以命中索引 SELECT * FROM users WHERE UPPER(email) = 'USER@EXAMPLE.COM';
在 Ja va 代码里(Spring Data JPA),可以这样写:
// UserRepository.ja va public interface UserRepository extends JpaRepository{ // 使用 @Query 注解显式调用函数索引 @Query("SELECT u FROM User u WHERE UPPER(u.email) = UPPER(:email)") Optional findByEmailIgnoreCase(@Param("email") String email); }
验证方法很简单:跑一个 EXPLAIN (ANALYZE, BUFFERS),看看有没有出现 Index Scan using idx_users_email_upper,如果有,就对了。
EXPLAIN (ANalyze, BUFFERS) SELECT * FROM users WHERE UPPER(email) = 'USER@EXAMPLE.COM';
模糊匹配是查询中的高频场景,但它对索引的影响很直接:
LIKE 'abc%':可以使用 B-tree 索引(前缀匹配)LIKE '%abc' 或 LIKE '%abc%':无法使用 B-tree 索引,默默触发全表扫描来看个例子:
-- 在 product_name 上有索引 CREATE INDEX idx_products_name ON products(product_name); -- 能用索引 SELECT * FROM products WHERE product_name LIKE 'iPhone%'; -- 不能用索引 SELECT * FROM products WHERE product_name LIKE '%Phone';
要解决这个问题,有几个思路:
pg_trgm 扩展 + GIN/GiST 索引:支持任意位置的模糊匹配。-- 启用 pg_trgm 扩展 CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 创建 GIN 索引(适合高并发读) CREATE INDEX idx_products_name_trgm ON products USING GIN (product_name gin_trgm_ops); -- 现在以下查询也能走索引 SELECT * FROM products WHERE product_name LIKE '%Phone%';
用 MyBatis 实现的话:
不过要提醒一句:
pg_trgm索引的体积比较大,对写入性能有影响,建议只在必要的字段上使用。
渲染错误: Mermaid 渲染失败: Parsing failed: unexpected character: ->“<- at offset: 32, skipped 5 characters. unexpected character: ->(<- at offset: 38, skipped 7 characters. unexpected character: ->:<- at offset: 46, skipped 1 characters. unexpected character: ->“<- at offset: 55, skipped 5 characters. unexpected character: ->(<- at offset: 61, skipped 7 characters. unexpected character: ->:<- at offset: 69, skipped 1 characters. unexpected character: ->“<- at offset: 78, skipped 5 characters. unexpected character: ->(<- at offset: 84, skipped 8 characters. unexpected character: ->:<- at offset: 93, skipped 1 characters. Expecting token of type 'EOF' but found `45`. Expecting token of type 'EOF' but found `30`. Expecting token of type 'EOF' but found `25`.
查询条件里的常量数据类型与索引列不一致时,PostgreSQL 会尝试隐式类型转换。有时候它能处理好,但一旦操作符重载或自定义类型出现,优化器可能直接放弃索引。
典型场景:
-- user_id 是 BIGINT 类型,有索引 CREATE INDEX idx_orders_user_id ON orders(user_id); -- 错误:传入字符串 '123' SELECT * FROM orders WHERE user_id = '123'; -- 隐式转换为 text → bigint -- 正确:传入数字 123 SELECT * FROM orders WHERE user_id = 123;
Ja va 里用 JDBC 时尤其要注意:
// 错误:使用字符串参数 String sql = "SELECT * FROM orders WHERE user_id = "; PreparedStatement stmt = connection.prepareStatement(sql); stmt.setString(1, "123"); // 传入字符串 // 正确:使用 Long stmt.setLong(1, 123L); // 传入 Long
在 Spring Boot 中,@Param 的类型也要匹配:
@Query("SELECT o FROM Order o WHERE o.userId = :userId")
List findByUserId(@Param("userId") Long userId); // 不要用 String
诊断技巧:查看执行计划中是否有 Cast 或 Function Scan 节点,这些通常就是隐式转换的信号。
复合索引是多条件查询的好帮手,但必须遵循最左前缀原则。
简单解释一下:对于索引 (col1, col2, col3),以下查询可以使用索引:
WHERE col1 = WHERE col1 = AND col2 = WHERE col1 = AND col2 = AND col3 = 但以下查询无法使用该索引:
WHERE col2 = WHERE col3 = WHERE col2 = AND col3 = 来看个实际例子:
-- 创建复合索引 CREATE INDEX idx_orders_status_date ON orders(status, created_at); -- 可用索引 SELECT * FROM orders WHERE status = 'shipped'; -- 可用索引 SELECT * FROM orders WHERE status = 'shipped' AND created_at > '2023-01-01'; -- 无法使用索引 SELECT * FROM orders WHERE created_at > '2023-01-01';
在 Spring Data JPA 中动态查询时,可以用 Specification:
// 使用 Spring Data JPA 的 Specification
public class OrderSpecs {
public static Specification byStatusAndDate(String status, LocalDate date) {
return (root, query, cb) -> {
List predicates = new ArrayList<>();
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
if (date != null) {
predicates.add(cb.greaterThan(root.get("createdAt"), date.atStartOfDay()));
}
// 注意:只有 status 有值时,索引才可能被使用
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}

建议:将选择性高(区分度大)的列放在复合索引左侧。
这些操作符通常导致索引失效,因为它们要排除大量行,优化器可能认为全表扫描更高效。
举个例子:
-- 有索引
CREATE INDEX idx_users_status ON users(status);
-- 可能不走索引
SELECT * FROM users WHERE status != 'inactive';
-- 改写为 IN 或具体值
SELECT * FROM users WHERE status IN ('active', 'pending');
当然,也有例外情况。比如如果 != 的值占比极小(如 99% 的用户都是 ‘active’,只查 != 'active'),优化器可能会使用索引,但这并不可靠。
Ja va 代码里,建议这样优化:
// 不推荐 Listusers = userRepository.findByStatusNot("inactive"); // 推荐:明确列出有效状态 List activeStatuses = Arrays.asList("active", "pending", "verified"); List users = userRepository.findByStatusIn(activeStatuses);
经验法则:如果 != 条件返回超过 10% 的行,优化器几乎总是选择 Seq Scan。
OR 条件在多个索引列上使用时,可能导致索引合并失败,退化为全表扫描。
问题示例:
CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_phone ON users(phone); -- 可能不走索引 SELECT * FROM users WHERE email = 'a@example.com' OR phone = '1234567890';
解决方案是使用 UNION:
-- 每个子查询都能走索引 SELECT * FROM users WHERE email = 'a@example.com' UNION SELECT * FROM users WHERE phone = '1234567890';
注意:
UNION会去重,若不需要去重,使用UNION ALL更高效。
MyBatis 动态 SQL 实现:

PostgreSQL 的查询优化器依赖表的统计信息(通过 ANALYZE 收集)来估算行数和选择执行计划。如果统计信息过期,即使有索引,优化器也可能错误地选择 Seq Scan。
触发场景:
autovacuum 未及时运行手动更新统计:
-- 更新单表统计 ANALYZE users; -- 更新整个数据库 ANALYZE;
检查统计信息是否准确:
-- 查看表的行数估计是否准确 SELECT relname, reltuples FROM pg_class WHERE relname = 'users'; -- 查看列的最常见值(MCV) SELECT attname, most_common_vals FROM pg_stats WHERE tablename = 'users' AND attname = 'status';
在 Ja va 应用里,可以在数据批量导入后触发 ANALYZE:
@Transactional public void bulkImportUsers(Listusers) { userRepository.sa veAll(users); // 手动触发 ANALYZE(谨慎使用,生产环境建议由 DBA 控制) jdbcTemplate.execute("ANALYZE users"); }
注意:频繁手动
ANALYZE可能影响性能,建议依赖autovacuum,并合理配置其参数。
与函数类似,在索引列上进行加减乘除等运算也会导致索引失效。
举个例子:
-- 有索引 CREATE INDEX idx_orders_amount ON orders(amount); -- 无法使用索引 SELECT * FROM orders WHERE amount * 1.1 > 100; -- 改写为 SELECT * FROM orders WHERE amount > 100 / 1.1;
Ja va 代码里,在 Controller 层预计算:
// Controller 层预计算 double threshold = 100.0 / 1.1; Listorders = orderRepository.findByAmountGreaterThan(threshold);
原则:将计算移到应用层,让 WHERE 条件保持为 column OP constant 形式。
PostgreSQL 的 B-tree 索引默认包含 NULL 值,但某些查询(如 IS NULL)可能无法高效使用索引,除非创建部分索引(Partial Index)。
场景:经常查询非空 email
-- 普通索引包含 NULL,体积大 CREATE INDEX idx_users_email ON users(email); -- 更优:只索引非空 email CREATE INDEX idx_users_email_not_null ON users(email) WHERE email IS NOT NULL; -- 查询非空 email 时效率更高 SELECT * FROM users WHERE email = 'user@example.com';
场景:查询特定状态的订单
-- 只索引未完成的订单
CREATE INDEX idx_orders_pending ON orders(order_id) WHERE status IN ('pending', 'processing');
-- 查询时自动使用
SELECT * FROM orders WHERE status = 'pending';
在 Spring Data JPA 中,Repository 定义后会自动匹配:
public interface OrderRepository extends JpaRepository{ // Spring Data JPA 会自动使用部分索引(如果存在) List findByStatus(String status); }
优势:部分索引更小、更快,且减少写入开销。
最后,也是最核心的:不要猜测,要验证。用 EXPLAIN 看执行计划,这是诊断索引是否生效的黄金标准。
基本用法:
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com'; -- 更详细 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM users WHERE email = 'user@example.com';
关键指标:
Index Scan 或 Index Only Scan在 Ja va 测试中,可以打印执行计划:
@Test
void testIndexUsage() {
String explainSql = "EXPLAIN (ANALYZE, BUFFERS) " +
"SELECT * FROM users WHERE email = 'test@example.com'";
List plan = jdbcTemplate.queryForList(explainSql, String.class);
plan.forEach(System.out::println);
}
还可以定期检查从未被使用的索引:
SELECT
schemaname,
tablename,
indexname,
idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY tablename, indexname;
建议:删除长期未使用的索引,减少写入开销。
避免索引失效不是一次性任务,而是贯穿应用开发、测试、上线和运维的持续过程。掌握以上十大技巧,结合 EXPLAIN 工具和良好的编码习惯,可以显著提升 PostgreSQL 查询性能,降低系统延迟。
记住:索引是工具,不是魔法。只有理解其工作原理,才能真正发挥其威力。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述