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

推荐订阅源

S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园_首页
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
N
News and Events Feed by Topic
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
IT之家
IT之家
W
WeLiveSecurity
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
月光博客
月光博客
Schneier on Security
Schneier on Security
博客园 - 三生石上(FineUI控件)
Application and Cybersecurity Blog
Application and Cybersecurity Blog
腾讯CDC
H
Heimdal Security Blog
Y
Y Combinator Blog
Engineering at Meta
Engineering at Meta
量子位
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
V
Visual Studio Blog
L
LangChain Blog
Last Week in AI
Last Week in AI
The Cloudflare Blog
Hacker News: Ask HN
Hacker News: Ask HN
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
A
Arctic Wolf
T
Tenable Blog
I
Intezer
P
Privacy International News Feed
Attack and Defense Labs
Attack and Defense Labs
N
News | PayPal Newsroom
Martin Fowler
Martin Fowler

暗无天日

读: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 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
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 作为深入学习资料。