卷 V · 计划CH 19深度 19/24

声明式的真相:planner 登场

这是「你的查询也被当真」的兑现。你写下 SELECT ... WHERE ...,只说了要什么,从没说怎么拿——用不用索引、先连哪张表、用哪种连接算法,你一个字都没提。替你决定这一切的,是 PostgreSQL 那个会思考的大脑:查询规划器。这一章,我们把它拆开看。

声明式代价模型Seq vs Index选择性

SQL 是声明式的:说「what」,不说「how」

回到第 3 章那个洞见:一句 SQL 是一个关系代数表达式,它描述「我要满足这些条件的这些列」,是一个结果的规格说明,不是一段执行步骤。这就叫声明式

对比一下命令式:如果用普通编程语言查「customer_id = 42 的订单」,你得亲手写循环、决定要不要先建个索引、怎么遍历。而 SQL 里你只写:

SELECT * FROM orders WHERE customer_id = 42;

「怎么拿」的自由,全交给了数据库。这份自由,正是规划器施展的空间——同一句 SQL,它可以用天差地别的物理方式执行,得到的结果一样,快慢却能差上千倍。

从「要什么」到「怎么拿」:两步走

规划器把你的查询变成可执行计划,大致两步:

  1. 逻辑计划:把 SQL 解析成关系代数的树(选择、投影、连接……),并做等价改写(把过滤条件下推、展开子查询、重排连接顺序等)——这些改写不改变结果,只为后面有更多省钱的可能。
  2. 物理计划:为逻辑计划里的每一步,挑一个具体的执行算法。「读一张表」可以是 Seq Scan(顺扫全表)或 Index Scan(走索引);「连两张表」可以是 Nested Loop / Hash Join / Merge Join(第 7 章见过)。每种算法都有成本,规划器要挑最省的组合。

关键问题就变成:怎么比较不同物理计划的成本?

代价模型:给每条路标个价

PostgreSQL 用一套代价模型给每个计划估一个抽象的「成本」数字(单位是「读一个顺序页」的开销)。几个核心参数:

参数默认值含义
seq_page_cost1.0顺序读一个数据页的成本(基准)
random_page_cost4.0随机读一个页的成本(比顺序贵,因为磁盘寻道)
cpu_tuple_cost0.01处理一行的 CPU 成本
cpu_index_tuple_cost0.005处理一个索引项的成本

注意 random_page_cost(4.0)比 seq_page_cost(1.0)贵——这反映机械磁盘上随机寻道比顺序读慢。(用 SSD 时,随机读没那么贵,很多人把它调到 1.1,这是最常见的一处调优,第 22 章会说。)

Seq Scan vs Index Scan:成本说了算

最基本的抉择:查一张表满足某条件的行,是顺扫全表(Seq Scan)还是走索引(Index Scan)?直觉说「当然走索引」,但真相微妙得多,取决于选择性(selectivity)——你的条件挑出了全表的百分之几。

▶ 动手 · 查询计划器成本对决

下面这台真的计划器引擎,对着一张 10 万行、938 页、customer_id 上有索引的 orders 表工作。换不同的 WHERE(挑出的行占比不同)、开关索引,看它怎么估算两条路的成本、挑出赢家、打印 EXPLAIN 计划树。

玩几下你会发现规律。Seq Scan 的成本是恒定的 ≈ 2188(938 页 × 1.0 + 10 万行 × 0.0125),因为不管条件多挑,它总得把整张表扫一遍。而 Index Scan 的成本,全看命中多少行

  • customer_id = 42 只挑出约 100 行时,索引成本 ≈ 413——远小于 2188,Index Scan 完胜
  • 但换成 customer_id > 42,命中约半张表(5 万行)时,索引要为每一个命中行去堆里做一次随机 I/O,成本飙到几十万,反而比顺扫贵得多——这时规划器故意不用你的索引,直接顺扫。
◆ 「它不用我的索引」——多半是它对

这是最反直觉、也最重要的一课:当一个查询要命中大量行时,走索引反而更慢。因为索引的路子是「先在索引里找到行的位置,再回堆里一个个把行捞出来」——命中行少时,这几次精准跳转很划算;命中行多时,这就变成了成千上万次随机跳转,比老老实实顺序扫一遍全表还慢。规划器算得出这笔账,所以它选顺扫。

所以下次你看到「我明明建了索引,它怎么不用?」,先别急着怪它——用下一章的 EXPLAIN 看看它估算命中多少行。十有八九,是你的条件选择性太低,顺扫确实更快。它把你的查询当真了。

规划器会考虑一大堆计划

Seq/Index 只是「读一张表」的选择。一个多表 JOIN 查询,规划器要在连接顺序 × 每对表的连接算法 × 每张表的访问方式的组合里搜索。三张表的连接顺序就有若干种,每种再配不同算法,候选计划可能有几十上百个。规划器给每个估成本,挑最便宜的。(表太多时全搜会爆炸,PostgreSQL 会在超过 geqo_threshold(默认 12 张表)时改用遗传算法近似搜索——第 22 章提。)

这就是「你的查询被当真」的全部含义:你只声明了目标,它认认真真地为你搜索、估价、择优。你写 SQL 越声明式(少用花招、别硬塞执行细节),就越是给了这个大脑发挥的空间。

⇄ 对照 MySQL
  • MySQL 也有基于成本的优化器,也做 Seq/Index 抉择、也会「命中太多就不用索引」。这个核心思想两家一致——「加了索引不一定走」在 MySQL 里同样成立。
  • 成熟度和计划多样性:PostgreSQL 的规划器历史更久、支持的计划类型更全(三种连接算法、更丰富的扫描方式如 Bitmap Scan),对复杂查询的优化通常更稳。MySQL 近年进步很快,但一些复杂子查询 / 派生表的优化仍偶有短板。
  • hint 哲学:MySQL 提供不少优化器 hint(USE INDEX / FORCE INDEX / STRAIGHT_JOIN)让你干预。PostgreSQL 官方刻意不提供这类 hint——理念是「信任并改进规划器,而不是让用户手动指路」,要强行控制得靠 pg_hint_plan 扩展。这个理念差异,本身就很「PostgreSQL」。

这一章的一句话

SQL 是声明式的:你说要什么,规划器用代价模型估算每条物理路的成本、挑最省的。Seq Scan 成本恒定、Index Scan 成本随命中行数暴涨——所以命中太多时它故意不用索引,而它是对的。

规划器怎么想的,不用猜——EXPLAIN 会把它的计划和估算摊给你看。下一章,我们学着读懂这份「思路说明书」,并看同一句 JOIN 怎么随数据量在两种算法间切换。