前言
在客户现场,我们经常遇到一种看似简单、实际很容易误判的问题 —— 同一条 SQL,到底跑了多久?
有人习惯看 EXPLAIN ANALYZE 最顶层算子的 actual time,有人喜欢看最后一行 Execution Time,也有人直接在客户端打开 \timing 看 SQL 返回耗时。但麻烦的是,这三个数字经常不一样:
有时最顶层算子只显示几十毫秒,但 Execution Time 却有几秒;
有时直接执行 SQL 很快,但 EXPLAIN ANALYZE 明显慢很多;
甚至还有时 \timing 比数据库内部统计更大。
那么问题来了:到底哪个数字才是真的?答案可能有点反直觉:它们都是真的,但其实它们测量的不是同一件事。
在 YMatrix 这样的 MPP 分布式数据库里,一条 SQL 的执行并不是一个简单的“开始-结束”,它包含计划初始化、QD 调度、QE 执行、数据交换、结果返回、触发器收尾、统计信息回收、资源释放等多个阶段。
不同的计时方式,就像把一块“秒表”安在了不同的位置。
前置知识
在正式介绍原理之前,我们需要对相关的术语和知识有一个基本的了解:
QD:Query Dispatcher,即主节点 Master,负责建立与管理会话连接,解析 SQL 语句以及分发查询计划和汇总最终结果
QE:Query Executor,即计算节点 Segment,负责执行查询以及数据存储
Motion:数据移动,数据按照一定规则分散存储在各个计算节点,因此计算过程中会涉及到数据的移动
Slice:为了提高查询执行并行度和效率,YMatrix 把一个完整的分布式执行计划分割成多个 Slice,每个 Slice 负责查询计划的一部分
Gang:在不同 Segment 上执行同一个 Slice 的所有 QE 进程称为 Gang,Gang 用来表示一组进程
01
源码中隐藏的三块秒表
我们先来看看三个不同时间是如何进行统计的。
第一块秒表,是 EXPLAIN ANALYZE 中最顶层算子的 actual time。例如 Gather Motion 3:1 (actual time=12.315..18.742 rows=100 loops=1),这个时间来自计划节点自身的 instrumentation。简单来说,它统计的是这个节点在执行器中被调用、产生元组的耗时。
在源码层面,节点执行会被类似这样的逻辑包裹:
InstrStartNode(node->instrument);result = node->ExecProcNodeReal(node);InstrStopNode(node->instrument, TupIsNull(result) ? 0.0 : 1.0);
也就是说,顶层算子的 actual time 关注的是“这个节点自己被拉取数据期间花了多久”。
第二块秒表,是 EXPLAIN ANALYZE 最后的 Execution Time。例如 Execution Time: 1280.531 ms,这个时间不是某一个算子的时间,而是整条语句在 EXPLAIN ANALYZE 框架下的总耗时。它包含更外层的执行阶段,例如:
ExecutorStart
ExecutorRun
ExecutorFinish
ExecutorEnd
QD 等待 QE 返回统计
dispatcher / interconnect 收尾
snapshot、QueryDesc 等资源清理
所以它可能比最顶层算子的 actual time 更大。
第三块秒表,是客户端的 \timing。比如在 psql 客户端中
\timing onselect ...
它测量的是客户端视角下“一条 SQL 从发出到返回完成”的耗时。它不仅包含数据库服务端执行时间,还可能包含网络传输、结果集返回、客户端处理和展示等时间。
如果 SQL 返回几百万行,\timing 看到的时间可能明显大于数据库内部执行时间。
如果 SQL 因为 LIMIT 很快返回了少量数据,\timing 又可能小于 EXPLAIN ANALYZE 的执行时间。
所以,本质上三块秒表不是互相打架,而是在回答三个不同的问题:
顶层算子 actual time 表示计划树最顶层节点执行了多久
Execution Time 表示 EXPLAIN ANALYZE 这次完整诊断执行花了多久
\timing 表示客户端感知 SQL 返回用了多久
02
为什么顶层算子和 Execution Time 会差很多?
这是很多数据库工程师最容易混淆的地方。按直觉理解,最顶层算子应该代表整棵计划树的耗时。既然它已经在计划最上方,为什么还会和 Execution Time 差很多?根本原因在于计划节点的执行时间,不等于 SQL 语句的完整生命周期。
在 YMatrix 中,EXPLAIN ANALYZE 的执行流程大致是:创建 QueryDesc -> 初始化统计上下文 -> ExecutorStart -> ExecutorRun -> 等待 QE 完成并收集结果 -> ExecutorFinish -> 打印执行计划 -> ExecutorEnd -> 释放资源 -> 输出 Execution Time。
顶层算子的 actual time 主要发生在 ExecutorRun 期间。而 Execution Time 覆盖的范围明显更大。尤其在 YMatrix 这样的 MPP 架构中,下面这些时间都可能被计入 Execution Time,但不一定体现在最顶层算子里,比如:
分发计划到 QE 的时间;
创建 gang、连接 segment 的时间;
初始化 interconnect 的时间;
等待所有 QE 返回执行统计的时间;
触发器、约束、DML 收尾的时间;
ExecutorEnd 中 dispatcher、interconnect、snapshot 等资源释放时间;
某些 initplan 预执行或后处理时间。
因此如果当我们看到顶层算子和 Execution time 相差很多的时候,这不一定说明执行计划打印错了。它更可能说明:真正耗时的部分不在顶层算子的 tuple 拉取阶段,而在执行前后的分布式调度、等待、收尾或统计回收阶段。
在大多数时候,Gang 的创建效率是重要因素之一,我们可以调整 gp_vmem_idle_resource_timeout、gp_cached_segworkers_threshold 等 GUC 以节省数据库创建 Gang 的开销,循环利用,但是代价是会导致更多的空闲进程无法被及时回收。
这也是为什么分析 YMatrix / GPDB / PostgreSQL 类数据库问题时,不能只盯着计划树第一行。第一行很重要,但它不是全部。
03
为什么 EXPLAIN ANALYZE 可能比直接执行 SQL 慢?
这是另一个容易和前一个问题混在一起的现象。在某客户环境中,笔者曾遇到过另一个奇怪的现象:直接执行 SQL,\timing 显示 200 ms;但当执行 explain analyze 的时候却足足用了 5 秒。
这是不是说明数据库执行计划有问题?其实并不是,根本在于,EXPLAIN ANALYZE 和直接执行 SQL 的目的不同。
直接执行 SQL 的目标是尽快把用户需要的结果返回。
EXPLAIN ANALYZE 的目标是执行 SQL,并尽可能完整地收集计划节点、QE、slice、Motion、buffer、memory 等诊断信息。
在 YMatrix 此类 MPP 场景下,这个差异非常关键。举个例子:
对于一条带 LIMIT 的 SQL,普通执行时,QD 只要拿够结果,就可以进入收尾流程,并通过 squelch 机制通知下游节点或 QE:不用继续执行了。
但 EXPLAIN ANALYZE 为了拿到完整统计,需要等待 QE 完成并回收执行信息。这样它看到的是“完整诊断执行”的成本,而不是“业务取到足够结果”的成本。
从数据库内核原理出发的话:
普通执行在 ExecutorEnd 会触发 dispatch DISPATCH_WAIT_FINISH,接收到足够的数据之后主动发送 query finish 消息,让各个 QEs 尽快停止工作;
explain analyze 在 ExecutroRun 之后,会 dispatch DISPATCH_WAIT_NONE,等待所有的 QEs 完成自己的工作,所以会造成时间上的差异。
于是,这就会出现一个很经典的现象:
select ... limit 10; -- 很快explain analyze select ... limit 10; -- 可能慢很多
这其实并不是 bug,而是观测方式改变了系统行为。就好像给赛车装上传感器、摄像头、遥测设备之后,车还是那辆车,但它已经不再是纯粹的比赛状态。
04
三种时间分别应该怎么用?
在真实场景中,我们建议这样理解:
如果客户说“某个页面慢”或者“某个接口慢”,在这种情况下,\timing 或应用侧耗时更接近用户真实感受。它回答的是:用户等待结果花费了多久?
而 Execution Time,用于判断一次 EXPLAIN ANALYZE 的完整诊断成本。它适合回答:这次带统计的执行,从数据库内部看完整花了多久?但值得注意的是,它不是普通 SQL 的纯业务耗时。尤其在 LIMIT、部分取数、游标、数据倾斜、结果集很小但下游很重的场景下,Execution Time 和直接执行 SQL 可能差异明显。
看顶层算子 actual time,判断计划树执行主路径。它适合回答:数据从计划树最顶层节点产出,用了多久?
但如果我们发现和 Execution Time 差异很大,就应该继续排查:
是否有 gang 创建或 segment 连接慢?
是否有 dispatch 等待?
是否有某些 QE 拖尾?
是否发生了 squelch?
是否有 DML trigger 或后处理?
是否有 initplan?
是否有 Motion 等待或 interconnect 收尾?
是否有自定义算子统计没有完全覆盖?
换句话说,顶层算子时间适合看计划内,Execution Time 适合看语句内,\timing 适合看端到端。
05
结论:没有一个时间能代表全部真相
在对数据库进行性能诊断时,我们需要全面辩证地看待这三块“秒表”:
EXPLAIN ANALYZE 顶层算子不是全部。
Execution Time 也不是普通 SQL 的用户体感。
\timing 更不是数据库内核纯执行时间。
它们本质上是分别站在不同位置观察同一条 SQL。
真正的诊断能力,不是记住哪个数字最权威,而是知道每个数字背后的计时边界。在 YMatrix 中,这一点尤其重要,因为 YMatrix 面向的是大规模分析、时序写入、实时数仓、HTAP 和 AI 数据底座等复杂场景。SQL 的执行不只是单机上的算子调用,而是一个跨 QD、QE、slice、Motion、interconnect、存储引擎和执行器的协同过程。
当你看到三个时间不一致时,不要把它当成异常。它往往是数据库在提醒你:这条 SQL 的时间,不能只用一块秒表来解释。
感谢你的阅读,YMatrix 期待与志同道合的你一起同行。

添加 YMatrix 小助手
加入官方技术交流群
电话 | 400-800-0824
官网 |www.ymatrix.cn
推荐阅读




夜雨聆风