注册
一条SQL的执行过程
专栏/技术分享/ 文章详情 /

一条SQL的执行过程

诗想与论语 2026/10/10 205 3 0
摘要

平时连达梦,无非是 DM 管理工具、JDBC、Python 这几种姿势。SQL 敲完,回车,结果集出来,这事就算结束了。至于 SQL 提交给 dmserver 之后,服务器内部到底干了哪些活,很少有人细看。
最近读了两篇梳理达梦服务端执行链路的文章,我把上下两篇消化之后,整理成这篇简化版的笔记。示例用一个 student 测试表(7 个列、2 个索引、1 个主键约束,建表语句见文末附录),执行最朴素的一条查询:
select * from student limit 10;
image.png
这条语句从按下回车到拿回结果,在 dmserver 里要经过的完整链路如下:
连接登录 → 接收请求 → 计划复用查找 → 词法/语法分析 → 语义分析 → 逻辑计划与查询改写 → 物理计划 → 创建执行VM → 算子取数 → B+树扫描 → Buffer取页 → 返回结果
下面按这个顺序逐节展开。

1建立会话:

SQL 执行之前,得先有一个登录好的会话。用 disql 或 DM 管理工具登录时,服务端的监听线程收下连接,把会话申请任务派给会话线程,然后校验客户端版本、协商加密方式、校验账户口令,全部通过之后才返回登录成功。之后所有的 SQL 都在这一条连接上执行。

2接受请求:SQL文本如何进入dmserver

登录成功之后,会话线程还在原来的连接上等着。在 disql 里敲下 SQL 回车,客户端发来的是一条请求消息——SQL 文本只是这条消息携带的一部分,不是全部。
服务端把消息收全之后先做完整性校验,然后根据消息里的命令类型分流:本次这条是“准备并执行”请求,取出 SQL 文本,建立语句上下文。
注意,这个阶段还不会碰 SQL 的内容,解析在后面。

3计划复用:

拿到 SQL 文本后,达梦的第一反应不是去解析,而是先问一句:这条 SQL 我之前见过吗?
SQL 缓冲区里存着 SQL 文本、物理计划和结果集,其中的计划池用哈希表组织。查找逻辑不复杂:先算 SQL 文本的哈希值,快速定位候选计划;哈希值相同不代表是同一条 SQL,所以还要核对 SQL 文本、当前模式、对象版本和会话环境,全部一致才算真正命中。命中了就直接复用现成的物理计划,省掉后面一大串分析工作;没命中才走解析的老路。

4解析:

没命中计划池,SQL 文本就进入解析阶段,分三步。

4.1词法

词法分析负责拆词。拿我们的例子来说,select * from student limit 10 会被拆成一个个记号:SELECT、FROM、LIMIT 是关键字,STUDENT 是标识符,星号是一个记号,10 是整数常量。这一步只做分类,不判断 STUDENT 这张表存不存在。

4.2语法

语法分析按 SQL 的语法规则组装记号。FROM 缺失、LIMIT 后面没有行数,都会在这一步报语法错误。检查通过后生成一棵查询语法树。

4.3语义

语义分析负责绑定,把 SQL 里的表名、列名对应到数据库里的真实对象。STUDENT 没写模式名,就先确定当前会话的模式,再到这个模式下面找 STUDENT。查找依靠字典缓冲区,命中直接用,没命中就从系统字典装载。

5逻辑计划与查询改写:

走到这里,SQL 要查询什么已经明确。接下来要解决的是:怎样把这些查询要求组织成计划。
先说*号展开。select * 里的星号会展开成当前查询可见的用户列——我们的例子就是 student 的 7 个列。
逻辑计划是把查询要求组织成一棵关系运算树:基表关系说明数据从哪里来,过滤关系说明保留哪些行,投影关系说明输出哪些列。这棵树只确定要完成哪些运算以及先后关系——还不决定用哪个索引,也不读任何数据。
回到本例,这条 SQL 没有 WHERE、JOIN、GROUP BY,逻辑计划很简单:
投影关系 → Top-N关系(limit 10)→ STUDENT基表关系
生成过程中,优化器还会做查询改写,比如谓词下推、IN 列表化简、子查询改写为连接,让执行更划算。

6物理计划:

逻辑计划描述包含哪些关系运算,代价计算从候选方案里选出具体的访问方法。物理计划把两者合在一起,转换成执行器能直接调用的算子树。
回到本例,最终得到四层计划:
NSET2 收集结果集
PRJT2 计算并排列输出项
TOPN2 保留前10行
CSCN2 扫描STUDENT的聚集索引B+树
image.png
用 explain 执行这条 SQL,就能在管理工具里看到这四层计划,这是理解执行过程最直观的方式。

7执行:

物理计划确定用哪些算子、怎么连接,但保存不了一次运行中的进度。执行器为每个算子建立运行状态,用统一的方式驱动父子算子,这就是火山模型:父算子需要数据时,向子算子发出取数请求——请求自上而下传递,数据自下而上返回,也叫拉取式执行。
回到本例,取数请求沿着 NSET2 → PRJT2 → TOPN2 → CSCN2 一路向下;数据反过来:CSCN2 从 B+ 树取出行,TOPN2 只留前 10 行,PRJT2 整理输出项,最后 NSET2 收口。

8CSCN2扫描:

数据真正落地的地方,在 CSCN2 这个叶子算子上。扫描分三个层次:B+ 树负责定位记录,一致性读负责选择可以返回的版本,Buffer 负责提供页面。
普通表的聚集索引 B+ 树由根页、中间页和叶子页组成,真正的表记录保存在叶子页里。游标从树的上层定位叶子页,按槽位读取页内记录;当前页处理完,继续访问右侧叶子页。
一致性读里的“可见性”,是判断某个记录版本是否应当出现在当前事务的查询结果中。达梦的 MVCC 机制下,扫描时发现记录的最新版本对当前事务不可见,就沿回滚地址找旧版本。
每次取得页面地址,扫描算子都交给 Buffer Manager 取页。页面在数据缓冲区里就直接用,不在就从数据文件调入。一次逻辑取页不等于一次磁盘 IO,只有缓冲区里没有才发生物理读取。

9返回结果与收尾:

数据从扫描算子向上返回后,TOPN2 保留需要的记录,PRJT2 整理输出列,NSET2 生成结果描述、编码行数据,把结果交给会话通信层——在 disql 或管理工具里看到的结果网格,就是这么来的。
结果消息发出以后,这次执行进入收尾:关闭算子和游标、清理执行状态。可以复用的东西不会被删:有效的物理计划仍留在 SQL 缓冲区,字典对象继续留在字典缓冲区,数据页面继续留在数据缓冲区。
SELECT 结束后连接并没有关闭,随后观察到的 COMMIT 是同一会话上的下一条独立请求——释放事务锁和视图等资源,然后会话线程再次等待下一条请求。

10总结:

把整条链路串起来说一遍:disql 回车之后,会话线程收下请求消息,先查计划池有没有现成的物理计划;没有就词法语法分析出查询语法树,语义分析把名字绑定到真实对象;然后展开星号、构造逻辑计划,改写、算代价、生成物理计划;执行时四个算子按火山模型从上往下要数、从下往上送数,CSCN2 沿聚集索引 B+ 树定位记录,过一致性读这道筛子,从 Buffer 里取页;最后 NSET2 打包结果发回客户端,清理现场,回到 socket 等下一次。
跟完整条链路,有两个感受比较深。
一是达梦把“能复用的绝不重算”贯彻得很彻底:SQL 文本有计划池,字典对象有字典缓冲区,数据页有数据缓冲区,三层缓存思路如出一辙。
二是“准备”和“执行”的边界划得很清楚:物理计划生成之前的全部工作,都只是在确定执行方法,一行数据都没读。
如果想观察这些过程,explain 和 V$ 动态视图是最方便的两大工具,可自行使用了解。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服