惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

I
InfoQ
S
SegmentFault 最新的问题
N
Netflix TechBlog - Medium
B
Blog
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 聂微东
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
大猫的无限游戏
大猫的无限游戏
U
Unit 42
J
Java Code Geeks
IT之家
IT之家
aimingoo的专栏
aimingoo的专栏
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
WordPress大学
WordPress大学
腾讯CDC

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日
PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日
2026-04-19 · via 暗无天日

来源:Things you didn't know about indexes

索引是什么

索引的核心思路就是*排序*。把数据按某个列排好序,查找时就能用二分法快速定位,不用逐条遍历。

假设有一张宝可梦表:

id   | name       | type_1   | type_2   | generation | is_legendary | base_attack
1    | Bulbasaur  | Grass    | Poison   | 1          | false        | 49
4    | Charmander | Fire     | NULL     | 1          | false        | 52
25   | Pikachu    | Electric | NULL     | 1          | false        | 55
150  | Mewtwo     | Psychic  | NULL     | 1          | true         | 110

没有索引时,查 Pikachu 意味着逐行读取 name 列做比较,这就是 全表扫描 (full table scan)。四行无所谓,一千万行就是问题。全表扫描本身不慢——现代数据库每秒能扫几百万行——但它是 线性的 :数据翻倍,时间翻倍。索引查找则几乎不受数据量影响。

name 加索引后,数据库得到一个按名字排序的结构,可以用二分查找定位:

name          row
Bulbasaur   → 1
Charmander  → 4
Mewtwo      → 150
Pikachu     → 25

Postgres 底层用 B-tree 实现,跟新华字典按拼音查字一个道理——排好序的东西,查找才快。

索引的代价

索引不是免费的。一句话概括:

读变快,写变慢。

每次 INSERTUPDATEDELETE 都要同步更新索引——把新值插入排序结构的正确位置。多个索引就乘以多倍。

其次,索引是实打实的数据结构,占磁盘空间,也要占缓存。一张表有八个索引,需要常驻缓存的数据就从一份变成九份。

最后还有查询规划器。索引越多,规划器要权衡的方案越多,规划时间可能超过执行时间。

为什么你的索引不生效

加了索引却不走索引,通常掉进了以下陷阱。

复合索引在乎顺序

在宝可梦表上建 type_1type_2 的复合索引:

CREATE INDEX ON pokemon (type_1, type_2);

这个索引对以下查询有效:

SELECT * FROM pokemon WHERE type_1 = 'Water';
SELECT * FROM pokemon WHERE type_1 = 'Water' AND type_2 = 'Flying';

但对这个查询无效:

SELECT * FROM pokemon WHERE type_2 = 'Flying';

原因在索引的排序结构。复合索引 (type_1, type_2) 先按 type_1 排,再在每个 type_1 组内按 type_2 排:

Bug      → Flying   → [Butterfree, Beedrill, ...]
         → Poison   → [Venonat, Spinarak, ...]
Electric → NULL     → [Pikachu, Raichu, ...]
         → Flying   → [Zapdos, ...]
Fire     → NULL     → [Charmander, Vulpix, ...]
         → Flying   → [Charizard, Moltres, ...]
Grass    → Poison   → [Bulbasaur, Oddish, ...]
Water    → NULL     → [Squirtle, Psyduck, ...]
         → Flying   → [Wingull, Pelipper, ...]
         → Ground   → [Wooper, ...]

"Flying"散落在 Bug、Electric、Fire、Water 下面,没有统一入口。数据库无法跳到某个位置一次取完所有 Flying 记录。

如果经常单独查 type_2 ,就需要再加一个 type_2 的独立索引。

函数会破坏索引

不区分大小写的搜索很常见:

SELECT * FROM pokemon WHERE lower(name) = 'pikachu';

lower(name) 看起来跟查 name 差不多,但索引是按 name 原始值排序的(Bulbasaur、Charmander...),不是按 lower(name) 排序的。数据库找不到现成的排序结构,只好回退全表扫描。

这适用于任何包裹列的函数。只要比较的左边不是原始的索引列,索引就不参与。隐式类型转换也算—— textinteger 比较时会触发静默转换,效果等同于包了一层函数。

解决办法是建函数索引(见下文)。

用 EXPLAIN 诊断

Postgres 提供了 EXPLAIN ,在任何 SELECT 前加上就能看到执行计划:

EXPLAIN SELECT * FROM pokemon WHERE name = 'Pikachu';
Index Scan using pokemon_name_idx on pokemon
  Index Cond: (name = 'Pikachu'::text)

Index Scan 说明走了索引。再看问题查询:

EXPLAIN SELECT * FROM pokemon WHERE lower(name) = 'pikachu';
Seq Scan on pokemon
  Filter: (lower(name) = 'pikachu'::text)

Seq Scan 就是全表扫描。加上 ANALYZE 可以拿到实际执行时间:

EXPLAIN ANALYZE SELECT * FROM pokemon WHERE name = 'Pikachu';

三种你可能不知道的索引

单列索引和复合索引覆盖了大部分场景,但 Postgres 还有几种特殊索引。

函数索引(Functional Index)

前面的 lower(name) 问题,解法是直接对表达式建索引:

CREATE INDEX ON pokemon (lower(name));

任何确定性、不可变表达式都可以索引:

CREATE INDEX ON users ((created_at::date));
CREATE INDEX ON pokemon ((base_attack * 2));

但要注意:如果频繁查 lower(name) ,也许该考虑直接把数据存成小写,而不是靠函数索引绕路。函数索引是手段,不是目的。

部分索引(Partial Index)

普通索引覆盖全表所有行。但如果只查其中一小部分,索引其余行就是浪费。

宝可梦大约 1000 种,传说宝可梦只有约 80 种(不到 10%)。如果应用有个"显示所有传说宝可梦"的功能,建 is_legendaryname 的复合索引虽然能用,但索引里 920 条非传说记录纯粹是摆设。

部分索引只包含满足条件的行:

CREATE INDEX ON pokemon (name) WHERE is_legendary = true;

索引从 1000 条缩减到 80 条。查 WHERE is_legendary = true 走索引,查 WHERE is_legendary = false 走全表扫描——正好,因为 false 匹配几乎所有行,索引帮不上忙。

另一个典型场景是软删除:

CREATE INDEX ON users (email) WHERE deleted_at IS NULL;

软删除的行几乎不会被查询,不把它们排除出索引只是白占空间。

覆盖索引(Covering Index)

数据库用索引找到行后,还要去表中取查询需要的其他列——两次查找。但如果索引里已经包含了查询需要的所有列,就不需要访问表了。这就是覆盖索引,在 EXPLAIN 中显示为 Index Only Scan

前面的部分索引恰好就是一个覆盖索引:

SELECT name FROM pokemon WHERE is_legendary = true;

索引在 name 上,查询也只要 name ,全部命中。

也可以用 INCLUDE 显式构建覆盖索引:

CREATE INDEX ON pokemon (name) INCLUDE (base_attack);

这样以下查询可以纯索引完成:

SELECT name, base_attack FROM pokemon WHERE name = 'Pikachu';

为什么不直接把 base_attack 放进索引列?因为索引列是用来排序和搜索的。把 base_attack 加进索引列意味着每次写入都要按 (name, base_attack) 排序,但你从来不按 base_attack 搜索,这排序就是白做的。 INCLUDE 的意思是"带上这列,但别排序"。

INCLUDE 更实际的用途有两个:

  1. 数据类型不支持 B-tree 操作符的列(如 box 类型),不能作为索引键列,但可以 INCLUDE
  2. 唯一索引中附加列而不改变唯一性语义: CREATE UNIQUE INDEX ON users (email) INCLUDE (user_id) 只在 email 上强制唯一,同时覆盖需要 user_id 的查询

小结

  • 索引让读变快,但写变慢、占空间、增加规划开销
  • 复合索引按左前缀匹配,跳过首列的查询不生效
  • 函数(包括隐式转换)包裹索引列会导致索引失效
  • EXPLAIN 诊断, Index Scan 走索引, Seq Scan 全表扫描
  • 函数索引、部分索引、覆盖索引是应对特殊场景的利器

原文推荐了 Use The Index, Luke 作为深入学习资料。