上一篇我们剖析了 limit 1 误走全表扫描的根因,并给出了两个治理手段:多列扩展统计信息、条件索引。留了个无奖竞猜——如果两种手段同时用上,优化器最终会走哪个计划?我把计划和源码喂给AI,结果AI胡说八道。这一篇就用 GDB 把选路过程完整地走一遍,给出答案。
上一篇:明明有索引,limit 1 反而慢了两万倍
一、先看前置:只建条件索引,确实走了它
先只做第一步——建条件索引 idx_demo_partial,此时还没建多列统计。跑一下 limit 1,优化器如愿走了这条条件索引,Limit 成本低到 0.12..0.14:
postgres=# CREATE INDEX idx_demo_partial ON t_demo (c1) WHERE c2=0 AND c3=false AND c4=0; postgres=# analyze t_demo; postgres=# EXPLAIN (ANALYZE, BUFFERS, VERBOSE) postgres-# SELECT c1 FROM t_demo WHERE c2 = 0 AND c3 = false AND c4 = 0 limit 1; ---------------------------------------------------------------------------------- Limit (cost=0.12..0.14 rows=1 width=21) (actual time=0.004..0.005 rows=0.00 loops=1) Buffers: shared hit=1 -> Index Only Scan using idx_demo_partial on public.t_demo (cost=0.12..10867.80 rows=1086367 width=21) (actual time=0.003..0.004 rows=0.00 loops=1) Heap Fetches: 0 Buffers: shared hit=1 Execution Time: 0.020 ms (12 rows)
注意这里有个细节:条件索引子节点的 total 其实是 10867.80(rows 估成了 108 万),但套上 Limit 后成本被"打骨折"降到了 0.14。因为这时候 adjust_limit_rows_costs 用子路径的 rows(108 万)当分母,limit 1 当分子,打折比例 ≈ 1/108 万 ≈ 0,Limit 成本被压到极低。但这个 0.14 是一个虚假的低成本——它建立在行数严重高估的基础上。(顺带记住 10867.80 这个子路径 total——下一步加了多列统计后,它会被微调成 10813.69,这个细微变化正是"条件索引确实被重新评估过"的凭证。)
记住这个 0.14。它是"条件索引单飞"时的成本。一旦下一步引入了多列统计、让 idx_demo 也变成一条强力候选,这个 0.14 就再也不会出现了。
二、谜底:加了统计,反而"抛弃"了 0.14
接着做第二步——在条件索引已存在的基础上,再建多列统计 stat_demo_c234。此时对这条查询而言,一共有三条可选扫描路径:复合索引 idx_demo、条件索引 idx_demo_partial、以及 SeqScan。结果 limit 1 不再走 0.14 的条件索引,改走了 cost=0.56..4.58 的 idx_demo:
先回放这一步用到的全部命令——注意多列统计是在条件索引已经建好之后才追加的,两次 ANALYZE 分别刷新了各自的统计:
-- 第一步:先建条件索引,此时还没有多列统计 postgres=# CREATE INDEX idx_demo_partial ON t_demo (c1) WHERE c2=0 AND c3=false AND c4=0; CREATE INDEX postgres=# analyze t_demo; ANALYZE -- 第二步:在条件索引已存在的基础上,再追加多列统计 postgres=# CREATE STATISTICS stat_demo_c234 (mcv, ndistinct) ON c2, c3, c4 FROM t_demo; CREATE STATISTICS postgres=# analyze t_demo; ANALYZE
postgres=# EXPLAIN (ANALYZE, BUFFERS, VERBOSE) postgres-# SELECT c1 FROM t_demo WHERE c2 = 0 AND c3 = false AND c4 = 0 limit 1; QUERY PLAN ---------------------------------------------------------------------------------- Limit (cost=0.56..4.58 rows=1 width=21) (actual time=0.017..0.017 rows=0.00 loops=1) Output: c1 Buffers: shared hit=4 -> Index Only Scan using idx_demo on public.t_demo (cost=0.56..4.58 rows=1 width=21) (actual time=0.015..0.016 rows=0.00 loops=1) Output: c1 Index Cond: ((t_demo.c2 = 0) AND (t_demo.c3 = false) AND (t_demo.c4 = 0)) Heap Fetches: 0 Index Searches: 1 Buffers: shared hit=4 Planning Time: 0.297 ms Execution Time: 0.037 ms (13 rows)
上一篇里,条件索引单独存在时走的是 cost=0.12..0.14,startup 低至 0.12。既然它这么便宜,为什么这次两个索引同台竞技,优化器没选它、改选了 startup 高得多的 idx_demo?
三、三条路径的底层成本
要理解选路结果,先看清三条基表扫描路径(还没套 Limit 之前)各自的成本。我在 compare_path_costs_fuzzily 打断点,把 GDB 里抓到的 Path 结构体逐一列出:
idx_demo(复合索引):startup=0.56,total=4.5825,rows=1 idx_demo_partial(条件索引):startup=0.125,total=10813.69,rows=1 SeqScan:startup=0,total=290590.56,rows=1
这里最关键的对比,是两个索引的 total_cost 差了三个数量级:idx_demo 只有 4.58,而条件索引 idx_demo_partial 却高达 10813.69。条件索引不是「只存匹配行、更精准」吗,怎么反而贵这么多?
为什么条件索引的 total 这么高
根子还是行数估算——但要分清两个不同的数字。多列统计 stat_demo_c234 是建在 c2、c3、c4 三列上的,它通过 clauselist_selectivity 修正了 baserel->rows。这个值会共享给所有扫描路径的 Path.rows——所以你看 GDB 里三条路径的 rows 全是 1,无论是 idx_demo 还是条件索引还是 SeqScan。这一层,多列统计确实把行估算修准了。
但条件索引 idx_demo_partial 的代价计算走的不是 Path.rows 这条路。它的谓词已经固化进索引定义,cost_index 在估算扫描代价时用的是 pg_class.reltuples 对该 partial index 的估算——也就是这个索引里到底存了多少条目——这里被估成了 1086367(约百万,仍是老问题的高估残留)。扫百万条目,total 自然被抬到 10813。注意:这个 reltuples 是索引条目数,和 Path.rows 是两个不同的数字。
多列统计把所有路径的
Path.rows都修正成了 1(三条路径的 rows 全是 1,GDB 可证)。但条件索引的代价计算不看Path.rows,它看的是pg_class.reltuples(索引条目数 = 百万级)。这个非对称,决定了后面的选路结局。
四、GDB 实证:fuzzily 没淘汰它,set_cheapest 定了生死
第一步:add_path 阶段,fuzzily 让两条索引「共存」
路径生成阶段,add_path 会调用 compare_path_costs_fuzzily 对两两路径做「模糊比较」,fuzz_factor = 1.01(1% 容差)。GDB 抓到 idx_demo 与 idx_demo_partial 正面交锋的那一刻:
-- 在 compare_path_costs_fuzzily 处断下,path1/path2 是两个待比较的 IndexPath Breakpoint 1, compare_path_costs_fuzzily (path1=0x198e258, path2=0x198e048, fuzz_factor=1.01) at pathnode.c:187 (gdb) p *path1 $5 = {type = T_IndexPath, pathtype = T_IndexOnlyScan, ... rows = 1, disabled_nodes = 0, startup_cost = 0.56000000000000005, total_cost = 4.5824999999999996, pathkeys = 0x0} (gdb) p *path2 $6 = {type = T_IndexPath, pathtype = T_IndexOnlyScan, ... rows = 1, disabled_nodes = 0, startup_cost = 0.125, total_cost = 10813.689999999999, pathkeys = 0x0} (gdb) n -- 关键:用 get_rel_name 把两个 path 的索引名回查出来 (gdb) p get_rel_name(((IndexPath *) path1)->indexinfo->indexoid) $7 = 0x199b020 "idx_demo" (gdb) p get_rel_name(((IndexPath *) path2)->indexinfo->indexoid) $8 = 0x199baa8 "idx_demo_partial"
注意
$8用get_rel_name把 path2 的名字回查出来,明确是 idx_demo_partial,而它的$6total 是 10813.69。可第一部分条件索引单飞时,EXPLAIN 里同一条子路径的 total 却是 10867.80。同一条索引、两个不同的数值,这说明什么?
① 它确实被走到了:如果条件索引这次"没被评估",它的 total 根本不会出现在 $6 里,更不会被 $8 回查出名字。它带着数值和名字双双出现在 fuzzily 比较现场,就是"被完整生成、进了 pathlist、参与了对决"的实锤。
② 数值变了 = 被重估了:10867.80 → 10813.69 这个变化,唯一的解释是加多列统计后的 ANALYZE 刷新了基础统计,条件索引子路径被重新估算了一遍。total 仍是万级(条件索引的代价由 pg_class.reltuples 决定,多列统计修不了它),但小数发生漂移——这个"变而不大变"的特征,精准印证了"统计微调"而非"结构性改变"。
③ 微调从哪来:条件索引谓词已固化、没有 Index Cond,多列统计管不到它的选择性;但 ANALYZE 刷新的 reltuples、页数、相关性等通用输入项进了 cost_index,于是扫描代价里那部分非谓词分量被重算,total 从 10867.80 轻微挪到 10813.69。
path1 是 idx_demo(total=4.58),path2 是 idx_demo_partial(total=10813)。接着看 compare_path_costs_fuzzily 的判定逻辑(先比 total,因为多数路径 startup 为零):
/* Check total cost first ... */ if (path1->total_cost > path2->total_cost * fuzz_factor) { ... } // 4.58 > 10813×1.01? 否 if (path2->total_cost > path1->total_cost * fuzz_factor) // 10813 > 4.58×1.01? 是! { /* path2 fuzzily worse on total cost */ if (CONSIDER_PATH_STARTUP_COST(path2) && path1->startup_cost > path2->startup_cost * fuzz_factor) // 0.56 > 0.125×1.01? 是! { return COSTS_DIFFERENT; // ← 各有所长,两条都保留! } return COSTS_BETTER1; }
代入数据:
① total 比较:path2 的 total(10813.69)远大于 path1 的 total × 1.01(≈4.63),「path2 在 total 上模糊更差」成立,进入这个分支。
② startup 反查(关键):内层条件 path1.startup > path2.startup × 1.01,即 0.56 > 0.125×1.01(≈0.126)——成立!意味着 path2(条件索引)虽然 total 差,但它 startup 明显更低,「各有所长」。
③ 返回 COSTS_DIFFERENT:两条路径谁也不能支配谁,都被保留进 pathlist——条件索引并没有在这一步被淘汰。
所以 total 碾压并不足以让 fuzzily 淘汰对手。只要对手在 startup 上有优势,fuzzily 就判「DIFFERENT」把它留下。add_path 之后,三条路径全在 pathlist 里。
GDB 里的 pathlist 长度变化和逐条 dump 坐实了这一点——add_path 后 pathlist 从 2 条变 3 条,三条路径俱在:
-- 进入 add_path 后,pathlist 长度从 2 增到 3 add_path (parent_rel=0x19899e0, new_path=0x198e258) at pathnode.c:505 (gdb) p *parent_rel->pathlist $12 = {type = T_List, length = 2, max_length = 5, ...} // 插入前 (gdb) n (gdb) p *parent_rel->pathlist $13 = {type = T_List, length = 3, max_length = 5, ...} // 插入后:三条都在 -- 逐条 dump 三条路径,坐实条件索引没被淘汰 (gdb) p *(Path *) list_nth(parent_rel->pathlist, 0) $14 = {type = T_IndexPath, pathtype = T_IndexOnlyScan, ... rows = 1, startup_cost = 0.56000000000000005, total_cost = 4.5824999999999996} // idx_demo (gdb) p *(Path *) list_nth(parent_rel->pathlist, 1) $15 = {type = T_IndexPath, pathtype = T_IndexOnlyScan, ... rows = 1, startup_cost = 0.125, total_cost = 10813.689999999999} // idx_demo_partial —— 没被淘汰! (gdb) p *(Path *) list_nth(parent_rel->pathlist, 2) $16 = {type = T_Path, pathtype = T_SeqScan, ... rows = 1, startup_cost = 0, total_cost = 290590.56} // SeqScan
第二步:set_cheapest 阶段,按 total_cost 定生死
三条路径都进了 pathlist 后,set_cheapest 才是真正的决胜场。它用 compare_path_costs 分两个维度各挑一条:STARTUP_COST 维度挑 cheapest_startup_path,TOTAL_COST 维度挑 cheapest_total_path。GDB 抓到它正是按这两个 criterion 轮流比较:
set_cheapest (parent_rel=0x19899e0) at pathnode.c:361 -- 先按 STARTUP_COST 挑 cheapest_startup_path Breakpoint 2, compare_path_costs (path1=0x198e048, path2=0x198d278, criterion=STARTUP_COST) (gdb) p get_rel_name(((IndexPath *) path1)->indexinfo->indexoid) $31 = 0x199c390 "idx_demo_partial" -- 再按 TOTAL_COST 挑 cheapest_total_path(决定 fractional 选路的锚点) Breakpoint 2, compare_path_costs (path1=0x198e258, path2=0x198d278, criterion=TOTAL_COST) (gdb) p *path1 $34 = {type = T_IndexPath, ... startup_cost = 0.56, total_cost = 4.5824999999999996} (gdb) p get_rel_name(((IndexPath *) path1)->indexinfo->indexoid) $36 = 0x199c3b8 "idx_demo" (gdb) n -- 比较完毕,cheapest_total_path 最终锁定 idx_demo(total=4.58) (gdb) p *cheapest_total_path $37 = {type = T_IndexPath, pathtype = T_IndexOnlyScan, ... rows = 1, startup_cost = 0.56000000000000005, total_cost = 4.5824999999999996}
而 get_cheapest_fractional_path 最终要的锚点正是 cheapest_total_path。三条路径按 total 排序一目了然:
idx_demo:total = 4.58 ← 最小,当选 cheapest_total_path ✓ idx_demo_partial:total = 10813.69 SeqScan:total = 290590.56
条件索引不是被 fuzzily「淘汰」的,而是它虽然凭低 startup 活着进了 pathlist,却在 set_cheapest 的 total_cost 维度上败给了 idx_demo(4.58 vs 10813)。而
get_cheapest_fractional_path最终要的锚点正是 cheapest_total_path,于是 idx_demo 胜出。
第三步:LimitPath 层再对决 —— 0.14 到底去哪了
看到这你可能仍有疑问:第一部分里条件索引明明能算出 0.14 的 Limit,这次它怎么变成 10813 了?GDB 数据能直接回答这个问题。选出 cheapest_total_path 后,grouping_planner 会给候选路径分别套上 Limit 节点,再来一轮 fuzzily 比较。抓到这两条 LimitPath 正面 PK:
grouping_planner (root=0x1988e58, tuple_fraction=1, setops=0x0) at planner.c:2072 // limit 1 归一化 Breakpoint 1, compare_path_costs_fuzzily (path1=0x199c2d8, path2=0x199c250, fuzz_factor=1.01) at pathnode.c:187 (gdb) p *path1 $62 = {type = T_LimitPath, pathtype = T_Limit, parent = 0x199bfc0, ... rows = 1, disabled_nodes = 0, startup_cost = 0.125, total_cost = 10813.689999999999} // 条件索引的 Limit (gdb) p *path2 $63 = {type = T_LimitPath, pathtype = T_Limit, parent = 0x199bfc0, ... rows = 1, disabled_nodes = 0, startup_cost = 0.56000000000000005, total_cost = 4.5824999999999996} // idx_demo 的 Limit
先厘清一个容易误读的点:
$62里条件索引的 LimitPath total 是 10813.69,而第一部分单飞时 EXPLAIN 显示的子路径 total 是 10867.80——这两个数略有差异,恰恰说明条件索引这条路径这次也被完整生成、评估了,只是加了多列统计后它的子路径估算被微调了(10867.80 → 10813.69)。它不是没走到,而是走到了、参与了比较,只是最终没被选中。问题在于:为什么这次它的 Limit 没能像单飞时那样打折到 0.14?
为什么同样套 Limit,idx_demo 能压到 4.58、条件索引却压不下去?关键在 adjust_limit_rows_costs 的打折比例 count_rows / input_rows:
idx_demo:子路径 Path.rows 被多列统计修正为 1,打折比例 = count/input = 1/1 = 1,Limit.total = 0.56 + (4.58−0.56)×1 = 4.58。
idx_demo_partial:子路径 Path.rows 同样被多列统计修正为 1(GDB $6 可证),打折比例 = 1/1 = 1,Limit.total = 0.125 + (10813.69−0.125)×1 = 10813.69。
这次两条路径都没有被打折。因为加多列统计后,两条路径的
Path.rows都被修正成了 1,adjust_limit_rows_costs的打折比例 = 1/1 = 1——等于没打折。$62显示条件索引 Limit total 是 10813.69(子路径原样透传),$63显示 idx_demo Limit total 是 4.58(同样是子路径原样透传)。0.14 不是被谁「抢走」了,而是它根本就不该存在——它是行数高估制造的假象。
这一步同样有 GDB 实锤。在 get_cheapest_fractional_path 里打断点,直接抓 best_path:
(gdb) b get_cheapest_fractional_path Breakpoint 4 at 0x83ec6c: file planner.c, line 6867. Breakpoint 4, get_cheapest_fractional_path (rel=0x199bfc0, tuple_fraction=0) at planner.c:6867 // 源码第一行:Path *best_path = rel->cheapest_total_path; —— 起手就锚定 total 最小者 (gdb) n (gdb) p *best_path $87 = {type = T_LimitPath, pathtype = T_Limit, parent = 0x199bfc0, ... rows = 1, disabled_nodes = 0, startup_cost = 0.56000000000000005, total_cost = 4.5824999999999996} // 套了 Limit 的 idx_demo (gdb) p tuple_fraction $88 = 0 // ← 命中 if (tuple_fraction <= 0.0) return best_path; 直接短路返回
这段把最后一环钉死了:
best_path起手就等于rel->cheapest_total_path,$87显示它是 total=4.58 的那条 LimitPath(即套了 Limit 的 idx_demo);而$88抓到tuple_fraction=0,命中源码if (tuple_fraction <= 0.0) return best_path;直接短路返回——后面的 fraction 归一化、逐条 fractional 比较都没走,就把 cheapest_total_path 定成了最终计划。决定终局的就是谁的 cheapest_total_path 最小,谁就赢。
换句话说,那个 0.14 从一开始就是行数高估制造的虚假低成本。第一部分条件索引单飞时,Path.rows 还没被修正(仍是百万),打折比例 = 1/百万 ≈ 0,Limit 被压到 0.14——但这只是因为优化器误以为要从百万行里捞 1 行,才给了这么夸张的折扣。加了多列统计后,Path.rows 被修正成 1,折扣比例 = 1/1 = 1(等于没打折),两条路径的真实 Limit 代价浮出水面:idx_demo 是 4.58、条件索引是 10813。4.58 完胜 10813,最终计划锁定 idx_demo。
对照:4.58 为何没被"打骨折"
最后回收一个对照。上一篇裸 limit 1(无任何治理)时,Seq Scan 的 rows 高估到百万,fraction 极小,Limit 成本被打骨折到 0.26——同样是行数高估制造的假象。这次胜出的 idx_demo 因为 Path.rows 被多列统计修正成 1,count/input = 1/1 = 1,打折比例是 1,Limit.total 等于子路径成本原样透传(4.58)。条件索引同理,它的 Path.rows 也被修正成了 1,打折比例也是 1,Limit.total = 10813.69。两条路径都没有被打折,它们各自暴露了真实的代价——4.58 和 10813,优化器选前者。
本篇小结
「只建条件索引 → 走 0.14」「再加多列统计 → 改走 4.58」这个反直觉的切换,本质是:那个 0.14 从一开始就是行数高估制造的虚假低成本。① 只建条件索引时,Path.rows 还没被修正(仍是百万),打折比例 ≈ 1/百万 ≈ 0,Limit 被压到 0.14——但这只是因为优化器误以为要从百万行里捞 1 行;② 再加多列统计后,多列统计修正了 baserel->rows,三条路径的 Path.rows 全部变成 1,打折比例 = 1/1 = 1(等于没打折),0.14 的假象消失;③ 此时两条路径的真实 Limit 代价浮出水面:$62 条件索引 = 10813、$63 idx_demo = 4.58,4.58 完胜 10813;④ 最终 get_cheapest_fractional_path 因 tuple_fraction=0 短路返回 cheapest_total_path($87=4.58)。根因:条件索引的代价由 pg_class.reltuples(索引条目数,百万级)决定,多列统计修正了 Path.rows 却无法修正 reltuples,所以它的真实代价仍是万级。
五、总结:路径在哪些环节被淘汰?三个 cost 比较函数一览
这篇 limit 1 的案例里,条件索引是「活到 set_cheapest 才输在 total」;而我之前一篇讲 nestloop 跑偏的案例里,HashPath 却是「在 add_path 阶段就被 fuzzily 直接淘汰」。同一个 compare_path_costs_fuzzily,两种截然不同的结局。趁这个机会,把优化器选路链条上几个关键的 cost 比较函数拉通梳理一遍——搞清楚「一条路径到底会在哪一环、被谁、按什么标准淘汰」。
比较函数 | 触发环节 | 比较维度 | 淘汰行为 |
|---|---|---|---|
compare_path_costs_fuzzily | add_path 加入新路径时两两模糊比较 | disabled_nodes → total → startup,均带 1% 模糊系数 | 会当场淘汰:返 BETTER1/BETTER2 则劣者被回收;返 DIFFERENT/EQUAL 则保留 |
compare_path_costs | set_cheapest 选 cheapest_total / cheapest_startup | 单一维度精确比较(TOTAL_COST 或 STARTUP_COST),无模糊系数 | 不删路径:只从存活路径里挑出各维度最优者做锚点 |
compare_fractional_path_costs | get_cheapest_fractional_path 按 fraction 选路 | 按 fraction 在 startup~total 间插值出一个折中成本再比 | 不删路径:挑 fractional 成本最低者;fraction≤0 时短路直接返 cheapest_total |
它们的分工:只有 compare_path_costs_fuzzily 会真正「删」路径(在 add_path 里当场回收劣者),后两个都不删——compare_path_costs 和 compare_fractional_path_costs 都是从「活下来的路径」里挑锚点,并不淘汰谁。至于 adjust_limit_rows_costs,它压根不是比较函数、不在上表之列——它只在 create_limit_path 里给 LimitPath 按 count_rows/input_rows 打折定价,但正是这个打折比例一旦失真,才制造出 0.26 / 0.14 那种「看着极小、实则虚高」的假象,是前面各案例的隐藏推手。
案例 A(之前一篇·nestloop 跑偏)——被 fuzzily 当场淘汰:HashPath 与 NestPath 的 total 差异不足 1%(模糊近似),转比 startup 时 216.13 > 200.01×1.01 成立,返 COSTS_BETTER2,HashPath 被 accept_new=false 直接回收。它压根没能进 pathlist——尽管它的 total 其实更小,只因 startup 高出 1% 就在生成阶段出局。
案例 B(本篇·limit 1)——活到 set_cheapest 才输在 total:条件索引与 idx_demo 的 total 差异极大(10813 vs 4.58),但条件索引 startup 更低(0.125 vs 0.56),fuzzily 返 COSTS_DIFFERENT——两条都留进 pathlist。真正的生死在 set_cheapest:按 TOTAL_COST 维度 idx_demo(4.58) 胜出成为 cheapest_total_path。
两个案例的分水岭全在 fuzzily 的返回值:total 差异 <1% 时,startup 成了决定 BETTER 还是 DIFFERENT 的开关。差异小 → 一方 startup 更差就判 BETTER 当场淘汰(案例 A);差异大 → 两条各有所长判 DIFFERENT 双双保留、留待 set_cheapest 定夺(案例 B)。「最终 cost 最小 ≠ 一定被选中」——因为它可能在 add_path 阶段就被那个 1% 模糊系数提前淘汰了。
六、优化器比的是 cost,不是「索引」
读到这里,很容易冒出一个念头:「优化器在挑用哪个索引」。但翻遍上面所有比较函数,没有一处在比较「索引」——compare_path_costs_fuzzily、compare_path_costs、set_cheapest 眼里只有一堆 Path 结构体,每条 Path 带着自己的 startup_cost 和 total_cost,比的永远是这两个数值,谁小谁胜。
所谓「走了 idx_demo」,不过是「idx_demo 对应那条 Path 的 cost 赢了」这一结果的人话翻译。因果方向是 cost → 索引名:先各条 Path 独立算出 cost,再比 cost 决出 cheapest_total_path,最后才由这条获胜 Path 反查出它用的是哪个索引。索引名是比较之后才浮现的标签,绝不是比较时的输入。
本篇的现象也能印证这一点:加多列统计后,条件索引那条 Path 的 total_cost=10813.69,idx_demo 那条是 4.58。比 cost,4.58 < 10813.69,idx_demo 的 Path 胜出——因为 cost 小所以走它。
那我们建索引、建统计到底在「引导」什么?——本质是在改写各条 Path 的 cost 输入值,而不是直接命令优化器「用这个索引」(那是 hint 干的事,我们从头到尾没下过这种命令):
· 建条件索引 → 凭空多出一条 idx_demo_partial 的 Path,此时还没建多列统计,Path.rows 仍是百万,Limit 打折比例 ≈ 0,Limit 成本被压到虚假的 0.14 → 于是「走」了它;
· 再建多列统计 → 修正了 baserel->rows,三条路径的 Path.rows 全变成 1,Limit 打折比例 = 1/1 = 1(不再打折),0.14 的假象消失,两条路径的真实代价浮出:idx_demo = 4.58、条件索引 = 10813 → idx_demo 的 cost 更小 → 于是改「走」idx_demo。
底层永远是「cost 比大小」,索引名只是胜出后的标签我们建索引、建统计的每一个 DDL,都是在操纵那个被比较的 cost 输入,让我们想要的那条 Path 的 cost 变成最小——所谓「引导走哪个索引」,准确说是「引导哪条 Path 的 cost 最小」。
— END —
本文分享自 PostgreSQL运维之道 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!