OctoBlog 以 GitHub 为蓝本的个人博客系统
全部文章

PostgreSQL 索引入门:从一条慢查询说起

现场还原

一个后台列表页突然变慢。表里 800 万行,查询很普通:

SQL
SELECT id, title, created_at
FROM articles
WHERE tenant_id = 42
  AND status = 'published'
ORDER BY created_at DESC
LIMIT 20;

EXPLAIN ANALYZE 之后看到的是 Seq Scan on articles (cost=... rows=8000000)——全表扫描,3.2 秒。

第一个索引:顺序很重要

复合索引的顺序应该遵循:等值条件在前,范围/排序在后。

SQL
CREATE INDEX CONCURRENTLY idx_articles_tenant_status_created
  ON articles (tenant_id, status, created_at DESC);
  • tenant_id、status 是等值条件,放前面;
  • created_at 参与排序,放最后,并且声明 DESC 可以让 ORDER BY ... DESC LIMIT 20 直接走索引顺序,不需要额外排序。

耗时从 3200ms 降到 9ms。

什么时候索引用不上

写法 是否走索引 原因
WHERE tenant_id = 42 ✅ 最左前缀匹配
WHERE status = 'published' ❌ 跳过了最左列
WHERE tenant_id = 42 AND lower(title) = 'x' ❌ 列上套了函数
WHERE tenant_id = 42 AND created_at::date = '2026-06-01' ❌ 隐式类型转换

第 3、4 条的正解是表达式索引:

SQL
CREATE INDEX idx_articles_title_lower ON articles (lower(title));
CREATE INDEX idx_articles_created_date ON articles ((created_at::date));

EXPLAIN 里该看什么

SQL
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

我的阅读顺序是:

  1. actual time:真实耗时,倒数第二段是这一层的耗时;
  2. rows=估计 vs actual:两个数字差一个数量级,说明统计信息不准,先 ANALYZE;
  3. Buffers: shared read:读了多少个 8KB 页,能反推数据是否命中缓存;
  4. 出现 Sort 或 Hash Join 且行数很大,通常还有优化空间。

索引不是越多越好

每多一个索引,写入就要多维护一棵 B-tree。我的经验值:

  • 单表索引控制在 5 个以内;
  • 上线前用 pg_stat_user_indexes 检查 idx_scan = 0 的索引,直接删;
  • 建索引一定要 CONCURRENTLY,否则会锁表。
SQL
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

结论

这次排查真正花时间的不是写索引,而是确认查询到底在做什么。先看执行计划,再动手——顺序反过来,就只是在赌运气。