postgesql索引计划
PostgreSQL 官方支持以下几种核心索引类型:
| 索引类型 | 核心特点与适用场景 |
|---|---|
| B-tree | 默认索引,适合等值查询 (=)、范围查询 (>, <, BETWEEN)、排序 (ORDER BY) 以及前缀匹配 (LIKE 'abc%')。 |
| Hash | 仅支持等值查询 (=),在等值查询场景下比 B-tree 更快。 |
| GiST | 一种通用索引框架,适用于几何/地理数据类型(如坐标、多边形)的邻近搜索,也支持全文搜索。 |
| SP-GiST | 类似 GiST,但适合非平衡数据结构,如平面上的点(四叉树)、网络地址等。 |
| GIN | 倒排索引,专为包含多个键值的数据类型设计。非常适合全文搜索和 JSONB/数组 的包含查询。 |
| BRIN | 块范围索引,适合超大型表,且索引列的值与物理存储顺序相关(如自增ID、时间戳),索引极小。 |
| Bloom | 通过布隆过滤器提供快速等值查询,适合有大量列且需要组合等值查询的场景。 |
此外,还有一些特殊类型的索引:
-
唯一索引 (Unique Index):确保索引列值的唯一性。目前只有 B-tree 索引可以声明为唯一。
-
部分索引 (Partial Index):只针对表中满足特定条件的行子集建立索引。
-
表达式索引 (Expression Index):建立在函数或表达式的结果上,而不是直接建立在列上。
-
多列索引 (Multicolumn Index):建立在多个列上,但只有 B-tree、GiST、GIN 和 BRIN 这四种类型支持。
-
常规场景首选 B-tree:对于
id、ban_id、gradeid这类字段上的等值、范围或排序查询,默认的 B-tree 索引是最佳选择。 -
唯一性约束用唯一索引:如果需要保证
student.number(学号)唯一,应创建一个唯一索引。PostgreSQL 在执行PRIMARY KEY或UNIQUE约束时,会自动创建同名的唯一 B-tree 索引。 -
特殊数据类型选专用索引:如果未来有地理空间数据(如坐标),应使用 GiST 索引;如果需要进行全文检索,则应使用 GIN 索引。
-
超大型表考虑 BRIN:如果表数据量极大(如千万级以上),且查询字段与物理存储顺序相关(如自增的
id),BRIN 索引能以极小的空间开销提供不错的性能。
简单来说,对于大部分常规业务,默认的 B-tree 索引已经能满足 90% 以上的需求

explain select count(*) from student where ban_id=9;
这个查询的执行分为 两步,最终统计出 ban_id = 9 的学生总数:
-
索引扫描 → 2. 堆表扫描 → 3. 聚合计数
🔍 逐行解读
第一行(最顶层):
Aggregate (cost=101.25..101.26 rows=1 width=8)
-
Aggregate:表示最终执行
count(*)聚合操作。 -
cost=101.25..101.26:预估的启动成本 101.25,总成本 101.26(成本单位是“顺序页读取次数”,数值越小越快)。
-
rows=1:最终输出只有 1 行(即计数值)。
-
width=8:结果行的字节宽度(count 结果是 8 字节的 bigint)。
第二行(Bitmap Heap Scan):
-> Bitmap Heap Scan on student (cost=4.67..101.12 rows=50 width=0)
Recheck Cond: (ban_id = 9)
-
Bitmap Heap Scan:从表中读取符合条件的行。这里的“Bitmap”意味着它通过一个位图快速定位到数据页。
-
cost=4.67..101.12:这一步的启动成本 4.67,总成本 101.12。
-
rows=50:预估会找到 50 行符合
ban_id = 9的记录(这是优化器的估算值,实际可能不同)。 -
Recheck Cond:由于位图索引可能把一些不符合条件的页也标记了,所以在读取堆表时需要二次检查条件
ban_id = 9确保精确。
第三行(Bitmap Index Scan):
-> Bitmap Index Scan on student_ban_id_idx (cost=0.00..4.66 rows=50 width=0)
Index Cond: (ban_id = 9)
-
Bitmap Index Scan:在索引
student_ban_id_idx上查找ban_id = 9的条目。它并不是直接取出行的物理地址,而是先在内存中建立一个位图,标记哪些数据页(或行)可能包含符合条件的记录。 -
cost=0.00..4.66:扫描索引的成本很低。
-
rows=50:索引扫描预估找到 50 行。
-
Index Cond:索引条件就是
ban_id = 9。
🤔 为什么是 Bitmap Scan 而不是普通的 Index Scan?
PostgreSQL 的优化器判断:如果符合条件的数据可能分布在多个数据页上,使用 位图扫描 会更高效。它这样做:
-
先扫描索引,构建一个位图,标记出所有包含目标行的数据页。
-
然后按照位图顺序读取这些数据页,每个页上的行再过滤一遍。

第一行(顶层聚合):
Aggregate (cost=101.25..101.26 rows=1 width=8)
(actual time=0.071..0.072 rows=1 loops=1)
-
actual time=0.071..0.072:这一步实际用了 0.071 毫秒启动,0.072 毫秒完成。非常快,说明内存处理。
-
rows=1:最终输出 1 行(就是 count 总数)。
-
loops=1:该步骤只执行了 1 次。
第二行(Bitmap Heap Scan - 从表中读取数据):
-> Bitmap Heap Scan on student (cost=4.67..101.12 rows=50 width=0)
(actual time=0.050..0.062 rows=100 loops=1)
Recheck Cond: (ban_id = 9)
Heap Blocks: exact=2
这是最关键的信息差异:
-
预估 rows=50,但实际 rows=100 —— 说明优化器的统计信息不够新,实际匹配的行数是 100 行,而之前
ANALYZE未执行时,优化器预估只有 50 行。 -
actual time=0.050..0.062:这步实际用了 0.05 毫秒启动,0.062 毫秒完成,效率极高。
-
Heap Blocks: exact=2:需要从磁盘/缓存中读取 2 个数据页(每个数据页通常 8KB),而且全部命中(exact 表示没有误判需要重新检查的页)。这 2 个页里包含了所有 100 条符合条件的记录。
第三行(Bitmap Index Scan - 扫描索引):
-> Bitmap Index Scan on student_ban_id_idx (cost=0.00..4.66 rows=50 width=0)
(actual time=0.036..0.036 rows=100 loops=1)
Index Cond: (ban_id = 9)
-
实际 rows=100,与上面的堆扫描一致,说明索引中确实有 100 条
ban_id=9的记录。 -
actual time=0.036..0.036:仅用 0.036 毫秒就完成了索引扫描。
-
Bitmap Index Scan不会直接访问数据行,而是先在内存中构建一个位图(标记哪些数据页包含目标行),然后交给Bitmap Heap Scan去读取。
额外信息(计划与执行总耗时):
Planning Time: 0.144 ms -- 生成执行计划花了 0.144 毫秒 Execution Time: 0.120 ms -- 实际执行查询花了 0.120 毫秒
总响应时间约 0.26 毫秒,非常快。
-
索引生效且高效:查询走了索引
student_ban_id_idx,没有全表扫描,总执行时间仅 0.12 毫秒。 -
实际行数与预估不符:优化器预估值是 50,实际是 100。这说明表的统计信息可能过期了(比如插入大量数据后没执行
ANALYZE)。虽然不影响结果,但精确的统计能帮助优化器在复杂查询中做出更好的决策。 -
I/O 开销极小:只读了 2 个数据页,说明
ban_id=9的数据物理存储很紧凑。
再看下多字段检索 索引计划
gradeid 与ban_id 都是单独加的索引
explain select s.id, s."name", s.age, s.gradeid, s.ban_id from public.student s inner join public.banji b on s.ban_id = b.id left join public.geade g on s.gradeid = g.id where s.ban_id = 9 and s.gradeid = 1;

一、整体结构(由下往上读)
Nested Loop (cost=9.57..15.85 rows=1 width=35) -> Bitmap Heap Scan on student s (内层) -> Seq Scan on banji b (外层)
这是一个 嵌套循环连接(Nested Loop):
-
外层:顺序扫描
banji表(Seq Scan on banji),找到满足id = 9的班级。 -
内层:对
banji的每一行(这里只有 1 行),在student表上执行一次 位图堆扫描,查找同时满足ban_id = 9和gradeid = 1的学生。
二、内层:student 表的检索方式
Bitmap Heap Scan on student s
Recheck Cond: ((ban_id = 9) AND (gradeid = 1))
-> BitmapAnd (cost=9.57..9.57 rows=1 width=0)
-> Bitmap Index Scan on student_ban_id_idx
Index Cond: (ban_id = 9)
-> Bitmap Index Scan on student_gradeid_idx
Index Cond: (gradeid = 1)
1. 两个独立的索引扫描
-
先分别扫描
student_ban_id_idx(查找ban_id=9)和student_gradeid_idx(查找gradeid=1)。 -
每个索引扫描都会生成一个 位图(bitmap),标记哪些数据页包含符合条件的行。
2. BitmapAnd 操作
-
将两个位图进行 与(AND)运算,得到同时满足两个条件的数据页集合。
-
这个操作的成本为
cost=9.57,预估返回rows=1行(实际可能是 0 或少量)。
3. Bitmap Heap Scan
-
根据
BitmapAnd产生的位图,去读取对应的数据页。 -
由于位图是“页级别”的,每个页上可能有一些不符合条件的行,所以需要
Recheck Cond进行二次过滤,确保最终结果完全符合(ban_id=9 AND gradeid=1)。
三、外层:banji 表
Seq Scan on banji b Filter: (id = 9)
-
因为
banji表很小(只有 100 条),优化器选择 全表扫描,然后逐行过滤id = 9。 -
成本很低(
cost=0.00..2.25),这是合理的。
四、连接过程
Nested Loop (cost=9.57..15.85 rows=1 width=35)
-
外层扫描
banji,得到id=9的那一行(只有 1 行)。 -
然后用这一行去驱动内层的
Bitmap Heap Scan,即对于这个banji行,在student表中查找所有ban_id=9 AND gradeid=1的记录。 -
最后输出两表连接的结果(
width=35表示结果行的宽度)。
使用了索引,没有全表扫描 student。 |
BitmapAnd 需要读取两个索引,并做位图运算,成本略高于单个组合索引。 |
banji 是全表扫描,但表很小,影响可忽略。 |
预估 rows=1,但若实际符合条件的 student 行数很多(比如上百行),嵌套循环的效率会下降。 |
| 整体成本不到 16,响应应该很快。 | 如果经常出现 ban_id=9 AND gradeid=1 这种查询,组合索引会更优。 |
给ban_id, gradeid加上组合索引
CREATE INDEX student_ban_grade_idx ON student (ban_id, gradeid);

新旧计划对比
| 对比项 | 旧计划(两个单列索引) | 新计划(组合索引) |
|---|---|---|
| student 表访问方式 | Bitmap Heap Scan + BitmapAnd |
直接 Index Scan |
| 索引使用 | 先后扫描两个单列索引,做位图与运算 | 直接使用一个组合索引,一步到位 |
| 成本估算 | cost=9.57..13.59(内层) |
cost=0.29..8.30(内层),明显更低 |
| 额外操作 | 有 Recheck Cond 二次检查 |
无需二次检查,索引直接包含条件 |
新计划逐层解析
Nested Loop (cost=0.29..10.56 rows=1 width=35)
-> Index Scan using student_ban_grade_idx on student s (cost=0.29..8.30 rows=1 width=35)
Index Cond: ((ban_id = 9) AND (gradeid = 1))
-> Seq Scan on banji b (cost=0.00..2.25 rows=1 width=8)
Filter: (id = 9)
内层:student 表
-
Index Scan using student_ban_grade_idx:直接通过组合索引(ban_id, gradeid)定位到满足两个条件的具体行。 -
索引条件(
Index Cond):ban_id = 9 AND gradeid = 1,完全匹配索引的前两列,因此索引可以高效地直接返回结果行的 TID(物理地址),然后回表读取完整行。 -
无 Bitmap 中间步骤:避免了构建位图和位图与运算的开销,也无需二次过滤(
Recheck消失)。
外层:banji 表
-
Seq Scan on banji:依然全表扫描,但banji仅有 100 行,全表扫描成本仅0.00..2.25,是合理的选择。 -
连接顺序:优化器将
banji作为外表,student作为内表。因为banji结果只有一行(id=9),所以Nested Loop只需执行一次内层索引扫描,性能极佳。
-
单一索引访问路径:一个 B-tree 索引可以直接处理多列等值条件,无需多次读取索引页。
-
减少 I/O:旧计划需要读取两个索引页并写入临时位图,新计划只需一次索引查找,减少了内存和磁盘操作。
-
统计信息更准确:组合索引的统计信息(如
n_distinct和most_common_vals)更精确地反映多列联合分布,帮助优化器更准估算行数。
本文来自博客园,作者:余生请多指教ANT,转载请注明原文链接:https://www.cnblogs.com/wangbiaohistory/p/21346945

浙公网安备 33010602011771号