查过几十次慢SQL之后,我最深的感受是:很多人并不是不会写SQL,而是不了解PostgreSQL在执行一条SQL时究竟走了哪些路。同样的SELECT,在A库上跑3毫秒,换到B库变成3秒,中间差出来的往往不只是数据量,而是执行计划、缓存命中率、锁等待甚至统计信息新鲜度这些看不见的因素。
这篇是“PostgreSQL SQL语句的执行过程”系列的第二篇。上一篇把SQL从客户端到服务端的过程带到了解析和计划生成的门口,这篇我们继续往里走,重点聊清楚三件事:
- 一条SQL从文本变成执行计划,中间经历了解析、分析、重写、规划、执行这几个阶段,每个阶段各自在做什么;
- 执行引擎拿到计划之后,如何在表、索引、缓存、内存之间来回倒腾,最终把结果一行行返回给你;
- 以及当SQL变慢、会话卡住、锁文件报错时,应该从执行链路的哪个环节下手排查。
同样一篇文章,看懂“执行过程”只能算入门,能在生产环境里顺着链路定位问题才算真正吃透。这篇文章尽量把复杂的机制讲得落地一点,配合实际案例和排查经验,适合正在学习PostgreSQL的开发者,也适合被慢SQL折磨过的DBA和后端工程师。
1. 一条SQL进门之后,服务端到底做了什么
1.1 从“纯文本”到“可执行程序”的三站路
PostgreSQL的架构是经典的“服务端进程+共享内存”模型。你在客户端敲下一条SQL,按回车,服务端会有一个后台进程(backend process)接收这段文本。接下来的处理流程可以概括成一句话:文本先进解析器,再进分析器,然后过重写器,最后交给计划器和执行器。
很多人容易把“解析”和“分析”混在一起。用生活里点外卖类比:
- 解析器相当于前台接单员,只看你的订单格式对不对。你写“要一份宫保鸡丁盖饭”,它负责把这句话拆成“动词+名词+数量”这样的结构,但完全不知道宫保鸡丁是什么。
- 分析器相当于后厨备菜师傅,它知道鸡丁要切多大、花生米要炸到什么程度。对应到数据库里,分析器会检查表是否存在、列是否存在、数据类型是否匹配。
在PostgreSQL源码中,解析之后会产生一棵解析树(parse tree),这个阶段的产物是RawStmt和SelectStmt这类结构。等分析阶段结束,解析树会被转换成查询树(Query tree),查询树是后续所有处理的基础数据结构,里面已经有语义信息了。
如果你写过简单的语法解析器就会知道,解析阶段的报错通常很直白:语法错误、关键词拼错、括号不匹配。分析阶段的报错则更偏语义化:表不存在、字段不存在、函数不存在。实际使用中,这里有一个容易踩的坑:PostgreSQL对大小写敏感的处理。未加引号的标识符会被自动折叠成小写,如果你建表时用了大写表名,查询时没加双引号,就会报“relation does not exist”。这个错误经常让新手误以为表丢了,其实只是在解析阶段,表名被统一成了小写而已。
1.2 分析器:做语义检查,也做表达式展开
分析阶段结束后,PostgreSQL会得到一个已经“消毒”的查询树。这时候还没完,还有一个容易被忽略的动作:把视图展开,以及把函数调用、表达式递归处理成可计算的形式。
举个例子,你查询时写了:
sql复制SELECT id, name FROM user_view WHERE create_time > now() - interval '1 day';
其中user_view是一个视图,内部可能关联了三张表。分析器会先解析视图定义,然后把这个视图引用改写成底层的子查询或者连接树。同理,now() - interval '1 day'这样的表达式会被组织成表达式树,在执行阶段逐行求值。
这里插一个实战经验:如果视图层级嵌套过深,分析阶段的耗时也会明显上升。我曾经优化过一个接口,SQL本身不复杂,但视图套了四层,每次执行光分析就花了几十毫秒。解决方法并不复杂,把多层视图拆成中间表,或者用LATERAL子查询替代部分嵌套,响应时间立刻降下来了。
分析阶段的性能问题平时不明显,但对高频短查询影响极大。一条查询每秒执行上千次,分析多花5毫秒,整体压力就会显著增加。这就是为什么很多团队要求把常用查询写成预处理语句(PREPARE)的原因之一,因为预处理语句可以跳过重复的分析和计划生成。
1.3 重写规则:不只是视图的内衣
接下来进入重写阶段(Rewrite)。PostgreSQL的规则系统非常强大,视图本质上就是一种规则。你可以用CREATE RULE自定义规则,也可以利用物化视图的自动刷新机制。但对99%的开发场景来说,重写阶段最重要的任务是处理视图和安全性。
规则系统会让查询树发生变换。还是用刚才的user_view举例,重写阶段会把视图引用替换成对应的SELECT语句。如果视图定义中有聚合、连接、排序,重写后的查询树会变得比原始语句复杂很多。这也是为什么一条看起来很简单的“查询视图”的SQL,执行计划却可能包含多个表扫描和聚合操作。
值得一提的是,PostgreSQL的规则系统和中台的“拦截器”并不一样,它是在查询树上做替换,而不是像触发器那样针对每一行触发。如果你需要审计某张表的读写,别指望用规则系统替代触发器,规则系统本身不面向逐行处理,逐行逻辑应该用触发器函数去实现。
重写完之后,查询树才真正到达优化器面前。优化器要做的事情,本质上就是回答一个问题:在满足SQL语义的条件下,用哪种方式拿数据最划算。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 计划器和执行器都做了哪些看不见的取舍
2.1 代价模型:为什么全表扫描不一定比索引扫描差
PostgreSQL的优化器是典型的基于代价的优化器(CBO)。所谓代价,不是指CPU耗时,而是一个无量纲的估算值。它综合了顺序扫描的块数、索引扫描的选择率、连接操作的中间结果大小等多个因素。
默认情况下,执行计划的代价公
