PostgreSQL 索引与查询计划
辨认访问路径、索引形态、实际计划和读写代价。
本分册采用 PostgreSQL 18 文档作为语法和行为基线。归档中 PostgreSQL 16 或其他环境的 SQL、计划与耗时保留为历史记录,2026-10-05 整理未重跑这些数据库实验。示例需在独立可丢弃数据库内使用,各章同名表可能有不同结构;先核对 DDL、角色和数据再执行。
我们的在线书店运营了几个月:books 表攒下十万种图书,orders 表每天新增几千条订单。某天客服反馈:"按图书编号查详情,要转两三秒才出来。"数据库并没有坏,它只是还在用最原始的方式找数据——把整张表从头到尾读一遍。
本章回答四个问题:为什么没有索引(Index)会慢;索引是什么、PostgreSQL 提供哪几种;如何用 EXPLAIN 验证索引是否真的生效;以及索引的代价与慢查询的标准排查思路。除特别标注外,内容在 PostgreSQL 14~18 通用;示例均在 PostgreSQL 16 上原调研记录通过,可直接在 psql 中运行。为聚焦索引行为本身,本章实验重建了只含必要列的简化版 books、orders、customers 等表,与第 2、3 章的业务表结构无关——建议开一个独立的示例库跟做,避免与书店库里的同名表互相干扰。
4.1 没有索引的世界:顺序扫描
为什么需要:表在磁盘上按"到达顺序"一页页堆放,天生没有目录。想象一个闭架书库,书不分类、按入库顺序堆叠,你要找《数据库系统概念》,管理员只能从第一本开始逐本核对。PostgreSQL 把这种执行方式叫顺序扫描(Sequential Scan,计划中显示为 Seq Scan)。官方文档的描述很直白:没有索引时,系统只能把整张表逐行扫一遍才能找出所有匹配的行。一千行的表毫无感觉,一千万行的日志表每次查询都全量读一遍,磁盘和 CPU 都会叫苦。
眼见为实。先造一张十万行的图书表:
-- 建一张 10 万行的图书表
CREATE TABLE books (
id integer, -- 图书编号
title text -- 书名
);
INSERT INTO books (id, title)
SELECT g,
CASE WHEN g % 100 = 0 THEN 'PostgreSQL 从入门到精通'
ELSE '图书 #' || g
END
FROM generate_series(1, 100000) AS g
ORDER BY random(); -- 打乱物理顺序,更接近真实业务表
ANALYZE books; -- 更新统计信息,让规划器"心里有数"(见 4.8 节)
-- 无索引时查 5000 号图书
EXPLAIN SELECT * FROM books WHERE id = 5000; QUERY PLAN
-----------------------------------------------------
Seq Scan on books (cost=0.00..1890.00 rows=1 width=18)
Filter: (id = 5000)Filter 是"逐行判断":十万行全部读出来,一行行比对条件。建一个索引再看:
CREATE INDEX books_id_idx ON books (id);
EXPLAIN SELECT * FROM books WHERE id = 5000; QUERY PLAN
----------------------------------------------------------------------
Index Scan using books_id_idx on books (cost=0.29..8.31 rows=1 width=18)
Index Cond: (id = 5000)两处变化:扫描方式变成 Index Scan(索引扫描);条件从 Filter 变成 Index Cond——在索引里直接定位,不再逐行过滤,总成本从 1890 掉到 8.31。官方文档用书末的字母序术语索引作类比:读者不必通读全书,按字母翻到词条拿到页码即可;有了索引,系统"只需在搜索树中向下走几层"就能命中目标行。
4.2 B-tree:像书末术语索引一样找数据
B-tree(平衡树)是 CREATE INDEX 不指定类型时的默认索引,理解了它,一半的性能问题就有了答案。
它为什么快:术语索引按字母排序,你翻到 P 打头的区域时,其余 25 个字母的条目瞬间与你无关。B-tree 做的是同一件事,只是组织成树形:键值排序后分层存放,每深入一层就排除一大段键区间,因此定位一个键只需访问极少数页面;"平衡"则保证任何键的深度都差不多,最坏情况也不退化。有序性还附赠两个红利:其一,天然支持范围查询,<、<=、=、>=、>、BETWEEN、IN、IS NULL 都能用上;其二,可按索引顺序返回行,ORDER BY 恰好匹配时能省掉显式排序。模式匹配方面,锚定开头的 LIKE 'Post%'、col ~ '^Post' 可以走 B-tree(前提见 4.7 节),前导通配符的 LIKE '%SQL%' 永远不行——有序结构帮不了"任何位置都可能是开头"的查找。
两个自动行为,一个例外:声明主键(Primary Key)或唯一约束(Unique Constraint)时,PostgreSQL 会自动创建唯一 B-tree 索引,所以按主键查找天然快——若 books.id 是主键,甚至不必手工建索引。但外键(Foreign Key)列不会自动建索引,大表上按外键反查或 JOIN 常因此变慢,需要手工补上。
4.3 六种索引类型:先选对工具
B-tree 之外还有五种内置索引类型,它们覆盖不同操作符、值结构和数据分布。选错类型,索引就形同虚设——这是本章最重要的观念之一。
| 索引类型 | 擅长的查询 | 书店里的例子 |
|---|---|---|
| B-tree(默认) | 等值、范围、排序 | WHERE id = 5000、按价格区间筛选 |
| Hash | 只支持等值 = | 按会话令牌精确查一行 |
| GIN | "包含"类查询:数组、JSONB、全文检索 | payload @> '{...}'、tags && ARRAY['vip'] |
| GiST | 几何、范围类型、近邻搜索,可扩展的"索引基础设施" | 按坐标找最近的自提点:ORDER BY loc <-> point(x, y)(loc 是 point 坐标类型的列,<-> 是"距离"运算符) |
| SP-GiST | 把值逐层划分进子区间的"分区树"(四叉树、k-d 树、radix tree 都属此类结构) | 按前缀逐层划分的数据(如 IP 地址段、电话区号),B-tree 难以处理 |
| BRIN | 超大表中与物理行序强相关的列 | 千万行日志按时间戳查时间窗 |
Hash 索引只支持等值比较,内部存 32 位哈希值,能力窄,多数场景仍应首选 B-tree。GIN(Generalized Inverted Index,通用倒排索引)思路完全不同:为复合值中的元素建立条目,并记录包含这些元素的行——就像书末索引按"关键词 → 页码列表"组织。事件埋点表是它的主场:
CREATE TABLE events (
id bigint,
payload jsonb, -- 事件内容,如 {"type": "view", "book_id": 42}
tags text[] -- 事件标签
);
-- 插入约 20 万行事件数据后建两个 GIN 索引
CREATE INDEX events_payload_gin ON events USING GIN (payload);
CREATE INDEX events_tags_gin ON events USING GIN (tags);
-- JSONB "包含"查询:@> 交给 GIN
SELECT * FROM events WHERE payload @> '{"type": "view"}'::jsonb;
-- 数组"重叠"查询:&& 同样交给 GIN
SELECT * FROM events WHERE tags && ARRAY['vip'];把 GIN 与表达式索引结合(对 to_tsvector('english', 列) 建 GIN 索引——索引中的文本搜索函数必须显式指定配置名,单参数的 to_tsvector(列) 不能用于表达式索引)还能加速全文检索。
BRIN(Block Range Index,块范围索引)走另一个极端:不为每行建条目,只按物理块区间记录列的最小/最大值,像给每"摞"数据贴一张摘要标签。事件日志几乎总按时间追加,时间戳与物理顺序强相关,正是 BRIN 的主场:
CREATE TABLE logs (
id bigint,
created_at timestamptz
);
-- 写入千万行按时间递增的日志后……
CREATE INDEX logs_created_brin ON logs USING BRIN (created_at);
-- 时间窗查询可走 BRIN;\di+ logs_created_brin 可查看它的体积本章实验中,422 MB 的千万行日志表上,created_at 的 BRIN 索引只占约 56 kB——而十万行图书表的一个 B-tree 就有约 2.2 MB。代价是:若列值与物理顺序无关(数据乱序存放),摘要标签几乎筛不掉任何块,BRIN 就派不上用场。
4.4 索引的四种常见形态
普通索引就是 4.1 节的 CREATE INDEX ... ON books (id):单列、原值、全表。它的检索分两步:先在索引里定位匹配项,再按索引记录的位置回到表中读出整行——后一步俗称回表,每命中一行就多一次随机访问。真实业务的查询条件往往更复杂,于是有了下面四种变体。
复合索引与最左前缀
书店后台最常见的问题是"某顾客最近的订单",查询同时过滤顾客和时间:
CREATE TABLE orders (
id bigint,
customer_id integer, -- 顾客编号
created_at timestamptz, -- 下单时间
billed boolean, -- 是否已开票
amount numeric -- 金额
);
INSERT INTO orders (id, customer_id, created_at, billed, amount)
SELECT g, (g % 1000) + 1, now() - (g * interval '1 minute'),
(g % 3 <> 0), (g % 990) + 0.99
FROM generate_series(1, 1000000) AS g;
ANALYZE orders;
-- 等值列在前、范围列在后
CREATE INDEX orders_customer_created_idx ON orders (customer_id, created_at);多列索引(Multicolumn Index,复合索引)最多 32 列(含后文的 INCLUDE 列),但官方建议谨慎使用,超过三列通常不会再有收益。它遵循最左前缀规则:真正用索引收窄扫描范围的,是最左列上的等值条件加第一个范围条件;更靠右的列只做索引内过滤,能省回表、省不了扫描量。验证:
-- ① 两列都用上:最优,Index Cond 同时含两列
EXPLAIN SELECT * FROM orders
WHERE customer_id = 7 AND created_at > '2026-01-01';
-- ② 只用最左列:索引依然可用
EXPLAIN SELECT * FROM orders WHERE customer_id = 7;
-- ③ 只用第二列:PostgreSQL 17 及以前基本不走这个索引
EXPLAIN SELECT * FROM orders WHERE created_at > '2026-01-01';因此列顺序是设计决策:等值过滤列放最左、范围列随后。PostgreSQL 18 新增了 B-tree skip scan(跳跃扫描):当前导列取值很少时,即使查询没提供前导列条件,规划器也能按前导列的每个不同取值逐段定位,让 ③ 这类查询在部分场景可用——这是 18 之前没有的缓解(截至 2026 年 10 月,当前稳定大版本为 PostgreSQL 18,最新小版本 18.6)。
部分索引
订单表里有个反差:未开票订单只占少数,却被财务天天查。给全表建索引浪费,不如只给少数派建:
-- 只为"未开票"的行建索引:更小、写入更便宜
CREATE INDEX orders_unbilled_idx ON orders (id)
WHERE billed IS NOT TRUE;
-- 查询条件与谓词吻合时走索引
EXPLAIN SELECT * FROM orders
WHERE billed IS NOT TRUE AND id < 10000;这就是部分索引(Partial Index):WHERE 子句限定哪些行进入索引。前提是查询条件必须能"数学上推出"蕴含索引谓词(例如查询同样写 billed IS NOT TRUE),系统才会用它。部分索引还能实现"部分唯一约束"——书店的比价任务表里,同一本书在同一渠道只需一条成功记录,失败可以重试任意多次:
CREATE TABLE price_checks (
subject text, -- 书名
target text, -- 比价渠道
success boolean -- 是否抓到价格
);
CREATE UNIQUE INDEX price_checks_success_idx
ON price_checks (subject, target)
WHERE success; -- 唯一性只约束 success 为真的行表达式索引
用户输入的邮箱大小写五花八门,登录时得按 lower(email) 匹配。但普通索引存的是原始值,WHERE lower(email) = ... 用不上它——那就给"表达式本身"建索引:
CREATE TABLE customers (
id integer,
email text
);
INSERT INTO customers
SELECT g, 'User' || g || '@Example.COM'
FROM generate_series(1, 100000) AS g;
ANALYZE customers;
CREATE INDEX customers_email_lower_idx ON customers (lower(email));
-- 能走索引:查询表达式与索引定义一模一样
EXPLAIN SELECT * FROM customers WHERE lower(email) = 'user42@example.com';
-- 走不上:条件作用在原始列上
EXPLAIN SELECT * FROM customers WHERE email = 'User42@Example.COM';表达式索引(Index on Expressions)的关键限制是逐字匹配:查询里的表达式必须与索引定义完全一致,换一种等价写法都可能失配。它的维护代价也更高——每次插入和非 HOT 更新都要重新计算表达式,适合读多写少的列。
覆盖索引与仅索引扫描
商品页按 SKU 查价格,只碰 sku 和 price 两列。若两列都在索引里,理论上可以完全不回表——这就是仅索引扫描(Index-Only Scan)。INCLUDE 子句可往索引里追加"只搭车、不参与排序键"的列:
CREATE TABLE inventory (
sku text, -- 商品编号(如 ISBN)
price numeric, -- 售价
weight integer -- 重量(克)
);
INSERT INTO inventory
SELECT 'ISBN-' || lpad(g::text, 10, '0'), (g % 500) + 0.50, (g % 2000) + 100
FROM generate_series(1, 100000) AS g;
ANALYZE inventory;
-- INCLUDE 追加载荷列,保持按 sku 排序的结构
CREATE INDEX inventory_sku_cover_idx ON inventory (sku) INCLUDE (price);
-- price 在索引里:Index Only Scan,不回表
EXPLAIN (ANALYZE) SELECT price FROM inventory WHERE sku = 'ISBN-0000000042';
-- weight 不在索引里:退回普通 Index Scan,仍要回表
EXPLAIN (ANALYZE) SELECT weight FROM inventory WHERE sku = 'ISBN-0000000042';一个前提:仅索引扫描依赖可见性映射(visibility map)——PostgreSQL 内部把表的数据文件称为"堆"(heap),堆页即表的数据页;可见性映射是一张记录"哪些堆页上已没有待清理的旧版本行"的位图,由 VACUUM 维护。索引想跳过回表直接出结果,就得靠它确认索引项与表数据一致,也就是表足够静态、VACUUM 跟得上。本章实验里,刚导入数据的表首次执行显示 Heap Fetches: 1(回堆检查了一次可见性);VACUUM inventory; 之后再跑,Heap Fetches 变为 0、成本随之下降。频繁更新的表上,这个优化的红利会被吃掉。
4.5 用 EXPLAIN 读懂查询计划
索引建好了不等于被用了——决定"怎么执行查询"的是规划器(Planner),看它的决策靠 EXPLAIN。
EXPLAIN:只出计划,不动数据。它展示规划器打算怎么执行:输出是一棵节点树,扫描节点在底部,连接、排序、聚合在上层,上层节点的成本包含其全部子节点。每个节点带三个数字:cost=启动成本..总成本、rows=估计行数、width=估计行宽。cost 是规划器的相对估算单位,不能作为实测毫秒。普通 EXPLAIN 展示目标语句的计划;EXPLAIN ANALYZE 会实际执行语句,数据修改语句也会产生修改。生产环境需考虑规划开销、锁、函数与实际执行副作用,按所用选项安排验证。
EXPLAIN ANALYZE:真跑一遍。它把语句真实执行并追加实际数据:actual time=启动..结束、rows=实际行数、loops=循环次数;时间与行数是每次循环的平均值,总量要乘以 loops。读法的核心只有一句(官方文档也这样强调):比较估计行数与实际行数——两者接近,统计信息可信、计划大概率合理;差距悬殊,往往就是错误计划的根源。
EXPLAIN ANALYZE SELECT * FROM books WHERE id = 5000; Index Scan using books_id_idx on books
(cost=0.29..8.31 rows=1 width=18)
(actual time=0.006..0.006 rows=1 loops=1)
Index Cond: (id = 5000)
Planning Time: 0.005 ms
Execution Time: 0.008 ms加上 BUFFERS(写成 EXPLAIN (ANALYZE, BUFFERS))还能看到 shared hit/read,区分缓存命中与磁盘读取,帮你判断瓶颈在 CPU 还是 I/O。
同一列上,计划随选择性(Selectivity,即命中行占总行数的比例)自动切换:
EXPLAIN SELECT * FROM books WHERE id = 42; -- 命中 1 行
EXPLAIN SELECT * FROM books WHERE id BETWEEN 1 AND 1000; -- 命中约 1%
EXPLAIN SELECT * FROM books WHERE id < 70000; -- 命中约 70%实跑结果依次是:Index Scan(索引直接定位);Bitmap Heap Scan + Bitmap Index Scan(先在索引里把命中行的位置画成位图,再批量回表);Seq Scan(放弃索引、直接全表扫)。第三种最反直觉,却往往是正确的:用索引取回大比例行时,"查索引 + 逐行随机回表"的两步开销,反而比一口气顺序读全表更贵。所以"没走索引"不等于"有 bug",初学者要先接受这一点再谈优化。
两个安全警告。第一,EXPLAIN ANALYZE 会真实执行语句,对 UPDATE/INSERT/DELETE 来说副作用照常发生,测试修改语句务必包在事务里:
BEGIN;
EXPLAIN ANALYZE UPDATE books SET title = '新书名' WHERE id = 5000;
ROLLBACK; -- 撤销刚才的修改,表保持原样第二,它自带额外计时开销,很快的语句可能被显著拖慢,必要时用 EXPLAIN (ANALYZE, TIMING FALSE) 关掉逐节点计时再看耗时。
4.6 索引的写入、空间与维护成本
索引是"花钱买查询速度"的交易,账单有四张:一是写入变慢,每次 INSERT/UPDATE/DELETE 都要同步维护相关索引,让索引与表保持一致;二是阻碍 HOT(Heap-Only Tuple,仅堆内元组)优化——HOT 是一种内部机制:更新未被索引的列时,新行版本尽量写在原数据页内而不去动索引,省下大量索引维护;一旦被更新的列本身出现在某个索引里,这条捷径就被挡掉,一次逻辑修改在物理上被放大成多处写入(即"写放大");三是占用磁盘;四是建索引本身耗时,且默认阻塞该表上的写入,线上大表要改用 CREATE INDEX CONCURRENTLY。
由此得出一条朴素的结论:很少或从不被查询使用的索引,只有维护开销、没有收益,观察期足够长(覆盖完整的业务周期)之后应当删除。靠统计视图 pg_stat_user_indexes 的 idx_scan 列(该索引被发起的扫描次数)就能找出它们:
-- 候选删除名单:从未被扫描过的索引(观察期要足够长)
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0;规划器面对一堆功能重叠的索引也要花更多时间选路,"以防万一"式地堆索引是负优化。
4.7 常见索引失效场景
以下五种场景是"建了索引却没被用上"的高发区,逐条对照比盲目重建索引有效得多。
- 前导通配符:
LIKE '%SQL%'永远用不上 B-tree,有序结构帮不了"任意位置开头"的匹配。 - 对索引列套函数:
WHERE lower(email) = ...不会命中email上的普通索引,需要建表达式索引(见 4.4 节)。 - 非 C locale 下的 LIKE。先补两个背景:locale(排序规则,collation)决定数据库按什么规则比较和排序文本,它随操作系统环境在建库时确定——简体中文环境通常是 zh_CN.UTF-8 这类;C locale 是只按字符编码逐字节比较的"最简规则",中文环境几乎都不会是它。B-tree 索引按库的排序规则组织键的顺序,而 LIKE 前缀匹配需要的是"按字符逐个比较"式的定位,两者在非 C 排序规则下并不通用——所以即使写
LIKE 'Post%',默认 B-tree 也匹配不上。解决办法是另建一个指定text_pattern_ops操作符类(operator class,可理解为索引的"比较方式选项",这里让索引改按字符模式比较来组织;varchar 列对应varchar_pattern_ops)的索引:
CREATE INDEX books_title_idx ON books (title); -- 普通 B-tree
CREATE INDEX books_title_pat_idx ON books (title text_pattern_ops); -- 模式匹配专用
EXPLAIN SELECT * FROM books WHERE title LIKE 'Post%'; -- 实测走 pat 索引
EXPLAIN SELECT * FROM books WHERE title < 'P'; -- 实测只走普通索引实测中 LIKE 'Post%' 走 books_title_pat_idx,Index Cond 里出现 ~>=~、~<~ 这类模式比较操作符——这正是 text_pattern_ops 在工作;而 title < 'P' 用的是普通索引。注意:*_pattern_ops 索引不能服务普通的 <、> 比较,两种查询形态需要两个索引。
- 复合索引不满足最左前缀(见 4.4 节;PostgreSQL 18 起 skip scan 可部分缓解)。
- 查询条件与索引的操作符类不匹配——第 3 条的推广:索引按特定操作符组织,条件所用的操作符不在其中,就用不上。
4.8 慢查询排查思路
把前面的工具串成一条可复用的动线。假设书店后台又出现慢 SQL:
第 1 步:找到真凶。用 pg_stat_statements 扩展按总耗时排序找 Top SQL。它需要在 postgresql.conf 中把 pg_stat_statements 加入 shared_preload_libraries 并重启,然后启用:
CREATE EXTENSION pg_stat_statements;
-- 官方文档给出的 Top 5 模板:最耗时的语句 + 缓存命中率
SELECT query, calls, total_exec_time, rows,
100.0 * shared_blks_hit /
nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;第 2 步:看计划、比对行数。对目标 SQL 跑 EXPLAIN (ANALYZE, BUFFERS),重点比对估计行数与实际行数。本章实验里的一次真实案例——ANALYZE 之后又批量写入了 20 万行同一新顾客的订单:
Bitmap Heap Scan on orders (cost=25.66..3313.89 rows=1191 width=27)
(actual time=2.038..9.761 rows=200000 loops=1)
Recheck Cond: (customer_id = 1001)估 1191 行、实际 200000 行,差了 167 倍——统计信息过期,规划器在瞎猜。
第 3 步:先 ANALYZE 再判断。执行 ANALYZE orders; 后复测,同一查询的估计行数变为 201080,与实际几乎一致。官方措辞很重:不先运行 ANALYZE 就检查索引使用是"徒劳的"(a lost cause);大批量导入后手动 ANALYZE 是基本卫生习惯。也别在玩具规模的数据上调优——"用测试数据设索引,只会告诉你测试数据需要什么索引"。
第 4 步:检查过滤与排序列。确认 WHERE、JOIN、ORDER BY 涉及的列上是否有类型正确的索引:对照 4.3 节选类型、4.4 节选形态。
第 5 步:长期观察。用 pg_stat_user_indexes.idx_scan 找出从未被使用的索引,清理负资产(见 4.6 节)。
第 6 步:诊断"为什么不用索引"。怀疑规划器"偷懒"时,可在当前会话里抬高顺扫成本做对比:
SET enable_seqscan = off; -- 仅当前会话有效
EXPLAIN SELECT * FROM books WHERE title LIKE '%SQL%';
RESET enable_seqscan;官方说法是:若这样还不走索引,"很可能存在更根本的原因,例如查询条件与索引不匹配"。上面这条 '%SQL%' 正是如此——顺扫成本被人为抬到天文数字,计划依旧 Seq Scan,说明问题在条件本身,而非规划器。
常见误区
- 以为建了索引就一定会被用。选择性差、表太小、统计过期都会让规划器理直气壮选顺扫,而且往往是对的;先看 EXPLAIN 的估 versus 实,别一见 Seq Scan 就慌。
- 以为
LIKE 'xx%'没走索引就是坏了。非 C locale 下默认 B-tree 本就不服务它,需要text_pattern_ops/varchar_pattern_ops;而LIKE '%xx'任何 B-tree 都救不了。 - 奇怪
WHERE lower(email) = ...为何不命中索引。对列套函数即失效,需要表达式索引,且查询表达式要逐字一致。 - 以为外键会自动建索引。只有主键和唯一约束自动建;外键列要手工补。
- 以为索引越多越好。每个索引都拖慢写入、可能阻断 HOT、占磁盘;复合索引超过三列通常无益,没用的索引应当删。
- 复合索引列顺序随手写。范围列放最左会砍掉索引价值,应等值列在前、范围列在后。
- 对 UPDATE/DELETE 直接跑 EXPLAIN ANALYZE。副作用真实发生,必须用
BEGIN ... ROLLBACK包裹。 - 在百行级的表上调优。小表整页就能装下,索引毫无意义,结论不能外推到生产规模。
- 大量导入后不 ANALYZE 就开始排查。没有统计信息时,行数估算就是瞎猜。
- 把 Hash/BRIN/GIN 当万能 B-tree。Hash 只支持等值;BRIN 只对与物理顺序强相关的列高效;GIN 面向数组、JSONB、全文这类复合值。
参考来源
- PostgreSQL 官方文档 §11.1 索引·引言(Indexes - Introduction):https://www.postgresql.org/docs/current/indexes-intro.html
- PostgreSQL 官方文档 §11.2 索引类型(Index Types):https://www.postgresql.org/docs/current/indexes-types.html
- PostgreSQL 官方文档 §11.3 多列索引(Multicolumn Indexes):https://www.postgresql.org/docs/current/indexes-multicolumn.html
- PostgreSQL 官方文档 §11.7 表达式索引(Indexes on Expressions):https://www.postgresql.org/docs/current/indexes-expressional.html
- PostgreSQL 官方文档 §11.8 部分索引(Partial Indexes):https://www.postgresql.org/docs/current/indexes-partial.html
- PostgreSQL 官方文档 §11.9 覆盖索引与仅索引扫描(Index-Only Scans and Covering Indexes):https://www.postgresql.org/docs/current/indexes-index-only-scans.html
- PostgreSQL 官方文档 §11.10 操作符类与操作符族(Operator Classes):https://www.postgresql.org/docs/current/indexes-opclass.html
- PostgreSQL 官方文档 §11.12 检查索引使用(Examining Index Usage):https://www.postgresql.org/docs/current/indexes-examine.html
- PostgreSQL 官方文档 §14.1 使用 EXPLAIN(Using EXPLAIN):https://www.postgresql.org/docs/current/using-explain.html
- PostgreSQL 官方文档 SQL 参考·EXPLAIN:https://www.postgresql.org/docs/current/sql-explain.html
- PostgreSQL 官方文档 §5.5 约束(Constraints):https://www.postgresql.org/docs/current/ddl-constraints.html
- PostgreSQL 官方文档 F.32 pg_stat_statements:https://www.postgresql.org/docs/current/pgstatstatements.html
- PostgreSQL 官方文档 §27.2 累计统计系统(The Cumulative Statistics System):https://www.postgresql.org/docs/current/monitoring-stats.html
- PostgreSQL 18 发行说明(Release 18):https://www.postgresql.org/docs/18/release-18.html
- PostgreSQL 官方版本支持政策(Versioning Policy):https://www.postgresql.org/support/versioning/
- thoughtbot 工程博客《Why Postgres Won't Always Use an Index》:https://thoughtbot.com/blog/why-postgres-wont-always-use-an-index
最后更新于