Presto EXPLAIN ANALYZE 使用指南:真实执行统计与分布式执行计划深度解析
2026/9/23 17:51:20 网站建设 项目流程
  • 大数据
  • 数据库
  • 后端

【免费下载链接】presto

The official home of the Presto distributed SQL query engine for big data

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载

EXPLAIN ANALYZE 是 Presto 中唯一能够真正执行语句并返回"分布式执行计划 + 每个操作真实开销"的诊断命令。本指南以 Presto 官方文档为基础,结合仓库源码(SQL 文法定义、语句分析器、执行计划打印器、ExplainAnalyzeOperator 与测试用例)深入讲解其语法、输出解读、VERBOSE 模式与 JSON 格式,帮助你用它定位数据倾斜、哈希碰撞异常与热点算子。

语法(Synopsis)

EXPLAIN ANALYZE [VERBOSE] [(format <TEXT|JSON>)] statement

各组成部分说明如下:

语法元素含义
EXPLAIN ANALYZE执行语句并展示分布式执行计划及各操作的开销
VERBOSE可选。输出更详细的信息与底层统计量,理解这些信息通常需要了解 Presto 内部实现细节
(format <TEXT\|JSON>)可选。指定输出格式,默认是TEXT,可选JSON
statement要执行并分析的目标语句(如 SELECT)

从语法层面看,EXPLAIN ANALYZE? VERBOSE? ('(' explainOption ... ')')? statement这条产生式定义在 SqlBase.g4 中。注意VERBOSE既可以跟在ANALYZE之后,也可以独立出现;而format选项与TYPE(如DISTRIBUTED)等通用 explain 选项一样被解析进括号列表。

与普通 EXPLAIN 的区别

  • EXPLAIN:只展示查询的分布式执行计划(包含估算成本),不执行语句。
  • EXPLAIN ANALYZE先真正执行语句,再基于真实运行统计信息输出分布式执行计划与每个计划节点的实际开销(CPU、Scheduled、Input、Output 等)。
  • EXPLAIN ANALYZE VERBOSE:在分析模式基础上额外输出底层统计量(如窗口算子的驱动数、索引大小、分区大小等)。

语义校验的源码约束

在语义分析阶段,StatementAnalyzer.java 的visitExplain对 EXPLAIN ANALYZE 做了以下硬性校验:

  • 只支持单个format选项(only a single format option is supported in EXPLAIN ANALYZE);
  • format 只允许TEXTJSONonly TEXT and JSON formats are supported in EXPLAIN ANALYZE);
  • TYPE 只允许DISTRIBUTEDonly DISTRIBUTED type is supported in EXPLAIN ANALYZE)。

也就是说,EXPLAIN ANALYZE (TYPE LOGICAL)这类写法是不被接受的。

描述(Description)

EXPLAIN ANALYZE执行该语句,并展示语句的分布式执行计划以及每个操作的开销(cost)。

  • VERBOSE选项会给出更详细的信息和低层统计量;理解这些内容可能需要了解 Presto 内部机制与实现细节。
  • 输出格式可通过format选项设置,默认输出格式为TEXT
  • 注意:统计信息可能并非完全精确,尤其是执行很快的查询(例如涉及小表、内存表或刚执行完的缓存查询),其数值波动和采样误差会更明显。

底层执行原理

从实现上看,EXPLAIN ANALYZE并不是在协调器上"事后画图",而是把计划本身当作一个真实的查询来调度执行:

  1. 分析器(analyzer)把ExplainAnalyze语句改写为带 ExplainAnalyzeNode 的查询计划;
  2. 查询执行期间,ExplainAnalyzeOperator 负责"吸收"上游所有真实运行的数据页(addInput中直接忽略输入内容),并持续等待最终 Stage 统计信息就绪;
  3. 当所有子 Stage 都进入 final 状态后(见hasFinalStageInfo/isFinalStageInfo,其中对尚未完成的 stage 会以 100ms 间隔轮询等待),该算子通过QueryPerformanceFetcher拉取 QueryInfo 中的完整StageInfo
  4. 根据 format 分派到 PlanPrinter:
    • TEXTtextDistributedPlan(...),并把verbose标志透传给TextRenderer
    • JSONjsonDistributedPlan(...)
  5. 最终把渲染出的计划文本作为单行 VARCHAR 输出返回给客户端。

测试方面,AbstractTestQueries.java 的testExplainOfExplainAnalyze验证了EXPLAIN (EXPLAIN ANALYZE SELECT * FROM orders)这类"对 EXPLAIN ANALYZE 再做 EXPLAIN"的嵌套场景能够正常产出逻辑计划,说明 EXPLAIN ANALYZE 可以像普通查询一样被继续包装分析。

示例:TEXT 格式解读(TPC-H 查询)

以下示例来自官方文档(在presto:tiny>提示符下执行),它对 TPC-H tiny 规模的 5 表连接查询做真实执行与统计输出:

presto:tiny> EXPLAIN ANALYZE SELECT -> s.acctbal, -> s.name, -> n.name, -> p.partkey, -> p.mfgr, -> s.address, -> s.phone, -> s.comment -> FROM -> part p, -> supplier s, -> partsupp ps, -> nation n, -> region r -> WHERE -> p.partkey = ps.partkey -> AND s.suppkey = ps.suppkey -> AND p.size = 15 -> AND p.type like '%BRASS' -> AND s.nationkey = n.nationkey -> AND n.regionkey = r.regionkey -> AND r.name = 'EUROPE' -> AND ps.supplycost = ( -> SELECT -> min(ps.supplycost) -> FROM -> partsupp ps, -> supplier s, -> nation n, -> region r -> WHERE -> p.partkey = ps.partkey -> AND s.suppkey = ps.suppkey -> AND s.nationkey = n.nationkey -> AND n.regionkey = r.regionkey -> AND r.name = 'EUROPE' -> ) -> ORDER BY -> s.acctbal desc, -> n.name, -> s.name, -> p.partkey -> LIMIT 100;

对应的分布式执行计划输出(节选):

Query Plan ----------------------------------------------------------------------------------------------- ... Fragment 4 [SOURCE] CPU: 31.55ms, Scheduled: 38.34ms, Input: 8,020 rows (260B); per task: avg.: 8,020.00 std.dev.: 0.00, Output: 1,196 rows (21.02kB), 1 tasks Output layout: [partkey_15, min_73] Output partitioning: HASH [partkey_15] Output encoding: COLUMNAR Stage Execution Strategy: UNGROUPED_EXECUTION - Aggregate(PARTIAL)[partkey_15][PlanNodeId 3023] => [partkey_15:bigint, min_73:double] CPU: 3.00ms (1.74%), Scheduled: 4.00ms (0.54%), Output: 1,196 rows (21.02kB) Input total: 1,600 rows (40.63kB), avg.: 400.00 rows, std.dev.: 0.00% Collisions avg.: 4.50 (160.41% est.), Collisions std.dev.: 86.78% min_73 := "presto.default.min"((supplycost_18)) (1:365) - InnerJoin[PlanNodeId 2455][("suppkey_16" = "suppkey_21")] => [partkey_15:bigint, supplycost_18:double] Estimates: {source: CostBasedSourceInfo, rows: 1,600 (28.13kB), cpu: 684,460.00, memory: 225.00, network: 234.00} CPU: 11.00ms (6.40%), Scheduled: 13.00ms (1.77%), Output: 1,600 rows (40.63kB) Left (probe) Input total: 8,000 rows (210.94kB), avg.: 2,000.00 rows, std.dev.: 0.00% Right (build) Input total: 20 rows (260B), avg.: 1.25 rows, std.dev.: 60.00% Collisions avg.: 0.40 (30.84% est.), Collisions std.dev.: 183.71% Distribution: REPLICATED - ScanFilter[PlanNodeId 9,2699][table = TableHandle {connectorId='tpch', connectorHandle='partsupp:sf0.01', layout='Optional[partsupp:sf0.01]'}, grouped = false, filterPredicate = (not(IS_NULL(partkey_15))) AND (not(IS_NULL(suppkey_16)))] => [partkey_15:bigint, suppkey_16:bigint, supplycost_18:double] Estimates: {source: CostBasedSourceInfo, rows: 8,000 (210.94kB), cpu: 216,000.00, memory: 0.00, network: 0.00}/{source: CostBasedSourceInfo, rows: 8,000 (210.94kB), cpu: 432,000.00, memory: 0.00, network: 0.00} CPU: 14.00ms (8.14%), Scheduled: 16.00ms (2.17%), Output: 8,000 rows (210.94kB) Input total: 8,000 rows (0B), avg.: 2,000.00 rows, std.dev.: 0.00% partkey_15 := tpch:partkey (1:389) supplycost_18 := tpch:supplycost (1:389) suppkey_16 := tpch:suppkey (1:389) Input: 8,000 rows (0B), Filtered: 0.00% - LocalExchange[PlanNodeId 2949][HASH] (suppkey_21) => [suppkey_21:bigint] Estimates: {source: CostBasedSourceInfo, rows: 20 (180B), cpu: 7,480.00, memory: 54.00, network: 234.00} CPU: 0.00ns (0.00%), Scheduled: 0.00ns (0.00%), Output: 20 rows (260B) Input total: 20 rows (260B), avg.: 1.25 rows, std.dev.: 225.39% - RemoteSource[5] => [suppkey_21:bigint] CPU: 0.00ns (0.00%), Scheduled: 0.00ns (0.00%), Output: 20 rows (260B) Input total: 20 rows (260B), avg.: 1.25 rows, std.dev.: 225.39% ...

输出结构阅读方法

  • Fragment(执行片段):每个 Fragment 是一个可在节点上并行执行的计划子树。Fragment 4 [SOURCE]表示该片段直接读取数据源(source)。片段级统计行展示了整个片段的 CPU 总耗时(31.55ms)、调度耗时(38.34ms)、输入行数/字节、每任务平均值与标准差、输出行数与任务数。
  • Stage Execution Strategy:如UNGROUPED_EXECUTION,表示该阶段未按 group 分组执行(与GROUPED_EXECUTION相对,后者常见于聚合类查询)。
  • 计划节点(Plan Node):如Aggregate(PARTIAL)InnerJoinScanFilterLocalExchangeRemoteSource。每个节点带PlanNodeId,其统计信息包括:
    • CPU/Scheduled:节点消耗的 CPU 时间与调度(墙钟)时间,括号内为该节点占整片段的百分比;
    • Output:该节点实际输出的行数与字节数;
    • Input total/avg./std.dev.:输入总行数、每个 task 实例的平均输入行数与标准差(标准差百分比越大,说明各节点实例间输入越不均衡,即潜在的数据倾斜);
    • Collisions avg./Collisions std.dev.:哈希表碰撞的平均次数与标准差,括号内是相对估算值的百分比——这是判断哈希碰撞异常的关键指标;
    • Estimates:基于成本的估算行数、CPU、内存与网络开销(来自CostBasedSourceInfo)。
  • 输出表达式与赋值来源:如min_73 := "presto.default.min"((supplycost_18)) (1:365),其中(1:365)表示该表达式在 SQL 文本中的位置(行:列);partkey_15 := tpch:partkey表示输出列由 tpch 连接器的partkey列赋值而来。
  • Join 细节InnerJoin节点会列出Left (probe)Right (build)两路输入各自的统计,以及Distribution: REPLICATED(右表被复制广播到所有 worker)。

官方文档特别提醒:各计划节点的相对成本基于墙钟时间(wall time)计算,它不一定与 CPU 时间相关。例如一个节点 CPU 占比 1.74% 但 Scheduled 占比可能不同,这是因为调度时间包含等待、网络传输与线程调度开销。因此应结合 CPU 与 Scheduled 两列综合判断热点。

对于每个计划节点,还可以看到附加统计(例如每个节点实例的平均输入量、相关计划节点的平均哈希碰撞次数)。这些统计在检测查询的数据异常(倾斜 skewness、异常哈希碰撞)时非常有用。

VERBOSE 模式:窗口算子等底层统计

当使用VERBOSE选项时,部分算子会报告额外信息。例如窗口函数算子会输出以下内容:

EXPLAIN ANALYZE VERBOSE SELECT count(clerk) OVER() FROM orders WHERE orderdate > date '1995-01-01'; Query Plan ----------------------------------------------------------------------------------------------- ... - Window[] => [clerk:varchar(15), count:bigint] Cost: {rows: ?, bytes: ?} CPU fraction: 75.93%, Output: 8130 rows (230.24kB) Input avg.: 8130.00 lines, Input std.dev.: 0.00% Active Drivers: [ 1 / 1 ] Index size: std.dev.: 0.00 bytes , 0.00 rows Index count per driver: std.dev.: 0.00 Rows per driver: std.dev.: 0.00 Size of partition: std.dev.: 0.00 count := count("clerk") ...

VERBOSE 模式下窗口算子新增的统计量含义:

统计项含义
CPU fraction窗口计算消耗的 CPU 时间占节点总时间的比例(此处 75.93%,说明窗口排序/计算是该节点主要开销)
Active Drivers实际激活的驱动(driver)数/总驱动数,[ 1 / 1 ]表示单并发执行
Index size窗口内部索引(用于定位分区内行)的大小,以字节与行数衡量
Index count per driver每个驱动维护的索引数量
Rows per driver每个驱动处理的行数
Size of partition每个窗口分区的行数大小

注意Cost: {rows: ?, bytes: ?}显示为?,说明该窗口节点的成本估算缺失——这正体现了 EXPLAIN ANALYZE 中"估算成本"与"真实统计"两套信息的差异:真实统计来自执行过程,估算成本来自优化器模型。

VERBOSE的底层透传逻辑可参见 ExplainAnalyzeOperator.java 中textDistributedPlan(..., verbose)的调用,以及 PlanPrinter.java 将verbose标志传入TextRenderer的实现路径。

JSON 格式输出

通过(format JSON)可以要求输出结构化 JSON,便于程序化解析与后续可视化:

EXPLAIN ANALYZE (format JSON) SELECT count(*) FROM orders;

JSON 输出由 PlanPrinter.jsonDistributedPlan 生成,与 TEXT 格式共享同一份 StageInfo 运行时统计来源,但以树形 JSON 组织,包含fragmentsplan节点、estimatesstats等字段。注意 JSON 模式目前不接收 VERBOSE 的额外低层统计(参考 ExplainAnalyzeOperator 的分支实现)。

统计准确性说明

官方文档明确提示:统计信息可能不完全准确,尤其是快速完成的查询。原因可以从执行机制推断:

  1. 快速查询的总执行时间极短(毫秒级),各算子的计时粒度与采样窗口有限,误差占比被放大;
  2. 部分统计(如哈希碰撞估算、CPU 占比)基于采样或估算模型,与真实值存在偏差;
  3. 输出中Estimates一行来自 CBO 的成本估算,与真实CPU/Input统计属于不同来源,二者不应直接对比。

因此,在对查询做性能诊断时,建议对同一查询多次执行EXPLAIN ANALYZE观察数值稳定性,并结合std.dev.(标准差)判断是否存在节点间不均衡。

实战排查指引

结合文档示例与源码结构,EXPLAIN ANALYZE可用于以下典型诊断场景:

  • 数据倾斜检测:观察各算子Input total ... avg.: x rows, std.dev.: y%中的标准差。标准差显著偏大(例如超过 100%)说明不同 task 实例处理行数差异悬殊,可考虑在 JOIN 或 GROUP BY 键上做加盐(salting)或调整分区策略。
  • 哈希碰撞异常Collisions avg.Collisions std.dev.反映哈希表的碰撞情况,括号内为相对估算值的百分比。异常高的碰撞(如示例中 Aggregate 的160.41% est.)可能源于 hash 分布不佳,可检查是否有低基数字段被用作分布键。
  • 定位耗时热点算子:比较各节点的CPU百分比与Scheduled百分比。CPU 占比高说明计算密集;Scheduled 占比高而 CPU 低说明存在等待(网络、I/O 或调度瓶颈)。
  • 验证连接器行为ScanFilter节点中的TableHandle {connectorId='tpch', ...}Input: 8,000 rows (0B), Filtered: 0.00%展示了连接器下推与过滤效果,可用于确认谓词下推是否生效。

参见(See Also)

  • EXPLAIN 语法:仅展示分布式执行计划(含估算成本),不执行语句;
  • SQL 文法定义:SqlBase.g4;
  • 语义分析:StatementAnalyzer.java;
  • 运行时统计输出实现:ExplainAnalyzeOperator.java、PlanPrinter.java;
  • 相关测试:AbstractTestQueries.java(testExplainOfExplainAnalyze)。
  • 大数据
  • 数据库
  • 后端

【免费下载链接】presto

The official home of the Presto distributed SQL query engine for big data

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载

相关推荐

上一篇:Rerun ROS2桥接工具使用指南:机器人传感器数据实时可视化
下一篇:GitHub_Trending/rea/reader最新更新:2024功能盘点

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询