为何PostgreSQL在JOIN查询中未使用指定匹配索引?
问题背景
现有两张表:Tasks(30万行)和TaskAssignedRoles(50万行),表结构与索引定义如下:
CREATE TABLE public."Tasks" ( "ID" uuid NOT NULL, "RowID" uuid NOT NULL, "Planned" timestamptz NOT NULL, CONSTRAINT "pk_Tasks" PRIMARY KEY ("RowID") ); CREATE INDEX "idx_Tasks_ID" ON public."Tasks" USING btree ("ID"); CREATE TABLE public."TaskAssignedRoles" ( "ID" uuid NOT NULL, "RowID" uuid NOT NULL, "TaskRoleID" uuid NOT NULL, "RoleID" uuid NOT NULL, "RoleName" text NOT NULL, "RoleTypeID" uuid NOT NULL, "ParentRowID" uuid NULL, CONSTRAINT "pk_TaskAssignedRoles" PRIMARY KEY ("RowID") ); CREATE INDEX "idx_TaskAssignedRoles_ID" ON public."TaskAssignedRoles" USING btree ("ID"); CREATE INDEX "ndx_TaskAssignedRoles_TaskRoleID" ON public."TaskAssignedRoles" USING btree ("TaskRoleID") INCLUDE ("ID", "RoleID", "RoleName", "RoleTypeID") WHERE (("ParentRowID" IS NULL) AND ("TaskRoleID" = 'f726ab6c-a279-4d79-863a-47253e55ccc1'::uuid));
执行以下查询语句:
explain (analyze) SELECT t.*, tarp."RoleID", tarp."RoleName" FROM "Tasks" AS "t" INNER JOIN "TaskAssignedRoles" AS tarp ON "tarp"."ID" = "t"."RowID" AND "tarp"."TaskRoleID" = 'f726ab6c-a279-4d79-863a-47253e55ccc1' AND "tarp"."ParentRowID" IS NULL
得到的执行计划:
Merge Join (cost=73.06..41145.44 rows=229928 width=434) (actual time=0.024..945.202 rows=234668 loops=1) Merge Cond: (t."RowID" = tarp."ID") -> Index Scan using "pk_Tasks" on "Tasks" t (cost=0.42..13861.52 rows=234668 width=388) (actual time=0.007..227.215 rows=234668 loops=1) -> Index Scan using "idx_TaskAssignedRoles_ID" on "TaskAssignedRoles" tarp (cost=0.42..23823.15 rows=229928 width=62) (actual time=0.013..541.680 rows=234668 loops=1) Filter: (("ParentRowID" IS NULL) AND ("TaskRoleID" = 'f726ab6c-a279-4d79-863a-47253e55ccc1'::uuid)) Rows Removed by Filter: 280198 Planning Time: 0.353 ms Execution Time: 953.534 ms
提问:为何PostgreSQL未使用ndx_TaskAssignedRoles_TaskRoleID索引,尽管该索引的条件完全匹配查询中的筛选条件?
原因分析
连接方式的排序需求限制了索引选择
当前执行计划采用的是Merge Join,这种连接方式要求参与连接的两个数据集必须按连接键(t.RowID = tarp.ID)排序。idx_TaskAssignedRoles_ID是按ID排序的,刚好满足Merge Join对TaskAssignedRoles侧的排序要求;而ndx_TaskAssignedRoles_TaskRoleID是按TaskRoleID排序的,结果集无法直接用于Merge Join,若使用该索引,要么需要额外排序操作(增加成本),要么切换为Nested Loop或Hash Join,优化器评估后认为当前Merge Join方案的总成本更低。目标索引的排序键设计冗余
ndx_TaskAssignedRoles_TaskRoleID的WHERE子句已经固定了TaskRoleID的具体值,因此索引中以TaskRoleID作为排序键没有实际意义——所有符合条件的行的TaskRoleID完全相同,排序无法起到过滤或高效组织数据的作用。同时该索引未按连接键ID排序,无法直接支持Merge Join的匹配逻辑。优化器的成本估算结果导向
从执行计划可见,使用idx_TaskAssignedRoles_ID扫描后仅过滤掉28万行,最终返回23万行。优化器认为,直接扫描按ID排序的索引再做过滤,结合Merge Join的高效匹配,比使用目标索引后再处理排序或切换连接方式的成本更低。若要验证此结论,可通过SET enable_mergejoin = off;禁用Merge Join,或使用索引提示强制使用目标索引,对比两种方案的执行时间。覆盖索引的优先级低于连接逻辑需求
虽然ndx_TaskAssignedRoles_TaskRoleID是覆盖索引(包含了查询所需的所有字段),但连接操作的排序需求是优化器优先考虑的因素。Merge Join在处理大结果集时,能避免Hash Join的内存开销或Nested Loop的多次索引扫描,这种性能优势让优化器选择了更适配连接逻辑的索引。
内容的提问来源于stack exchange,提问作者b0bi

