PostgreSQL中btree_gist索引查询计划后续步骤为何必要?
问题背景
我使用PostgreSQL v15.2创建了如下表结构:
CREATE TABLE IF NOT EXISTS postgres_air_bitemp.frequent_flyer_transaction( frequent_flyer_transaction_key integer NOT NULL DEFAULT nextval('postgres_air_bitemp.frequent_flyer_transaction_frequent_flyer_transaction_key_seq' ::regclass), frequent_flyer_id integer NOT NULL, level text , booking_leg_id integer, award_points integer, status_points integer, effective temporal_relationships.timeperiod NOT NULL, asserted temporal_relationships.timeperiod NOT NULL, row_created_at timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT frequent_flyer_transaction_pk PRIMARY KEY (frequent_flyer_transaction_key), CONSTRAINT frequent_flyer_transaction_frequent_flyer_id_assert_eff_excl EXCLUDE USING gist ( effective WITH &&, asserted WITH &&, frequent_flyer_id WITH =) )
执行以下查询后得到对应的查询计划:
airlines=# explain analyze select * from postgres_air_bitemp.frequent_flyer_transaction t where frequent_flyer_id=39189 and now()<@asserted and now()<@effective;
查询计划输出:
QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on frequent_flyer_transaction t (cost=4.69..87.91 rows=21 width=74) (actual time=0.097..0.099 rows=1 loops=1) Recheck Cond: (frequent_flyer_id = 39189) Filter: ((now() <@ (asserted)::tstzrange) AND (now() <@ (effective)::tstzrange)) Heap Blocks: exact=1 -> Bitmap Index Scan on frequent_flyer_transaction_frequent_flyer_id_assert_eff_excl (cost=0.00..4.68 rows=21 width=0) (actual time=0.077..0.077 rows=1 loops=1) Index Cond: ((frequent_flyer_id = 39189) AND ((asserted)::tstzrange @> now()) AND ((effective)::tstzrange @> now())) Planning Time: 0.333 ms Execution Time: 0.172 ms (8 rows)
疑问
查询计划已正确使用btree_gist索引,但后续执行了基于frequent_flyer_id过滤的Bitmap索引扫描,最后还对frequent_flyer_id条件进行重检查。为何初始btree_gist索引扫描后需要这些后续步骤?我原本预期查询计划仅包含btree_gist索引扫描,这些额外步骤是否因gist索引是lossy(有损)的,需检查假阳性?
解答
首先要明确:PostgreSQL中没有单独的"GIST索引扫描"直接返回完整行数据的计划节点——所有索引扫描最终都需要访问表堆来获取完整的行记录(除非是仅索引扫描,但这里查询的是*,包含不在索引里的字段,比如level、booking_leg_id等,所以必须访问堆)。
关于Bitmap索引扫描+Bitmap堆扫描的流程
- Bitmap Index Scan:通过GIST索引快速定位满足
frequent_flyer_id=39189且now()在asserted和effective时间范围内的行对应的堆块位置,把这些位置标记在一个位图里。 - Bitmap Heap Scan:根据位图去读取对应的表堆块,然后从堆块中提取行数据。
为什么需要Recheck Cond
你猜对了,这确实和GIST索引的有损(lossy)特性有关:
- GIST索引为了优化空间和查询效率,对于某些索引键(包括这里结合了等值条件的时间范围索引),可能会返回一些"候选"堆块——这些堆块里可能包含符合条件的行,但不是所有行都符合。
- 所以在读取堆块后,PostgreSQL需要重新检查
frequent_flyer_id=39189这个条件,确保最终返回的行确实满足过滤规则,排除掉索引扫描带来的假阳性结果。
为什么不能只靠索引扫描返回结果
因为你的查询是select *,需要返回表中所有字段,而GIST索引只包含frequent_flyer_id、effective、asserted这三个字段,其他字段(比如level、award_points)不在索引里,必须去表堆中读取完整行。如果你的查询只选择索引包含的字段,可能会触发仅索引扫描(Index Only Scan),此时就不需要访问堆,也不会有后续的重检查步骤。
内容的提问来源于stack exchange,提问作者Curt Kolovson
相关产品推荐
相关产品推荐

