首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >StarRocks 查询探秘(六):从 EXPLAIN SQL 看执行计划的构建与优化

StarRocks 查询探秘(六):从 EXPLAIN SQL 看执行计划的构建与优化

原创
作者头像
用户11806606
发布2025-09-17 18:32:06
发布2025-09-17 18:32:06
2880
举报

题外话

在写《StarRocks 查询探秘》系列文章,有同学疑惑是否有必要研究底层代码?在我看来是有必要的,第一,深入理解一门技术,理解其原理和底层代码,对各种特性深入理解,知道针对业务场景如何使用,才能发挥出该技术/框架的最大能力;第二,很多时候线上问题往往是使用姿势不对造成,某种意义上就是对其理解不深刻,即使是线上bug,也要求我们拥有强悍的 troubleshooting 能力,而这些就是通过阅读其源码锻炼出来的。

言归正传,如果单纯讲源码,确实有些无聊,甚至收获有限。本文将从 EXPLAIN 命令入手,深入剖析 StarRocks 如何生成和优化执行计划,并结合实际案例,通过执行计划优化业务 SQL,把一个线上十几分钟耗时的 SQL 优化到3分钟内。


EXPLAIN SQL:窥探执行计划的窗口

EXPLAIN 显示 SQL 的物理执行计划,支持的选项:EXPLAIN、EXPLAIN LOGICAL、EXPLAIN VERBOSE、EXPLAIN COSTS、EXPLAIN ANALYZE

EXPLAIN 的不同级别:

LOGICAL

显示简要的执行计划。

VERBOSE

显示详细的执行计划,包括数据类型,nullable 信息,优化策略等。

COSTS

显示详细的执行计划,包括统计信息。

ANALYZE

显示实际运行后的执行计划信息。

以下面这条 SQL 为例:

代码语言:javascript
复制
EXPLAIN SELECT a.id, uidFROM         (SELECT id         FROM hive_hms.db_xx.hive_table_xx    ) aJOIN         (SELECT regexp_extract(extend2, 'id:([0-9]+)', 1) AS id,                 click_uid AS uid        FROM paimon_hms.db_xx.paimon_table_xx        WHERE dt='20250807'                AND hour='01'                AND min='1'    ) b   ON a.id=b.idLIMIT 3;

输出的执行计划以及计划分段如下:

图片
图片
  • Fragment:StarRocks 将查询拆分为多个片段,每个片段可在不同节点并行执行。
  • Operator:如 HdfsScanNode、PaimonScanNode、HASH JOIN等,描述具体的操作。
  • Partition 和 Exchange:展示数据分区和节点间数据交换的方式。
  • 等等等

通过这张图能更直观看出整个执行计划和计划分段,每个计划分段包含的 Operator

图片
图片
ExecPlan - 执行计划

ExecPlan 是 SQL 的执行计划,包含多个 PlanFragment(计划片段),以及与查询执行相关的元数据和上下文信息。它是查询优化器和执行引擎之间的桥梁,负责组织查询计划的结构、跟踪节点和片段的分配、生成执行计划的解释信息等。

PlanFragment - 执行计划分段

PlanFragment 是数据库查询执行引擎中用于表示查询计划的一个片段(Fragment)。查询计划通常被分解为多个片段,这些片段通过 ExchangeNode(交换节点)形成树状结构,共同完成查询的执行。每个片段负责执行一部分查询逻辑,并将结果输出到下一个片段或最终的查询结果。

那么 PlanFragment 的划分依据是啥呢?从源码看是以物理执行计划中的 PhysicalDistributionOperator 作为切分点,直观的理解就是需要数据进行 re-shuffle,需要节点跟节点之间交换数据,这时候可以认为可以切分成不同的 PlanFragment,单个 PlanFragmen 可以在一个节点内执行。


源码解析

在 StatementPlanner.java 中,经过 CBO 优化器后,逻辑计划树从 LogicalOperator 转换成 PhysicalOperator 以及相关重写后, 进入执行计划的构建。

图片
图片

PlanFragment

  • 封装执行计划的一个子树,包含执行节点 PlanNode 和输出表达式 outputExprs。
  • 定义数据的分区方式 dataPartition 和 outputPartition,决定数据如何在节点间分发。
  • 支持并行执行parallelExecNum 和 pipelineDop和运行时过滤器RuntimeFilter 等功能。
代码语言:javascript
复制
public class PlanFragment extends TreeNode<PlanFragment> {        ....        protected PlanNode planRoot;        // exchange node to which this fragment sends its output        private ExchangeNode destNode;        // if null, outputs the entire row produced by planRoot        protected ArrayList<Expr> outputExprs;        // created in finalize() or set in setSink()        protected DataSink sink;        protected final DataPartition dataPartition;        protected DataPartition outputPartition;        ....    protected int parallelExecNum = 1;        protected int pipelineDop = 1;        protected boolean dopEstimated = false;        protected Map<Integer, RuntimeFilterDescription> buildRuntimeFilters = Maps.newHashMap();        protected Map<Integer, RuntimeFilterDescription> probeRuntimeFilters = Maps.newHashMap();        ....}

通过 Visitor 模式递归遍历构建出 PlanFragment

图片
图片

下图中的红框圈中的 PhysicalDistributionOperator 就是作为 PlanFragment 的切分点

图片
图片

根据分布类型设置不同的 DataPartition,比如,ROUND_ROBIN:随机分发数据,dataPartition 设置为 RANDOM。将计算出的 dataPartition 设置到 exchangeNode 上,形成一个新的 PlanFragment。

图片
图片

线上 SQL 优化案例分析

线上一个复杂的 SQL,里面嵌套着大量的两两表 Join 关联查询,摘取其中的一个子句如下:

代码语言:javascript
复制
 SELECT pid,        uid  FROM               (SELECT pid,             word      FROM table_A      WHERE dt='${date}'      AND hour='${hour}'      AND pid like '5%'     ) a          JOIN               (SELECT uid,             query      FROM table_B WHERE dt='$date'      AND hour='${hour}'     ) b          WHERE instr(b.query, a.word) > 0;

一开始怀疑是不是两表进行 shuffle join 导致的性能差,通过 Explain Verbose 查看,发现是 instr(b.query, a.word) 这个非等值条件导致使用 NESTLOOP JOIN。NESTLOOP JOIN 会对于两个数据集(表 A 和表 B),对表 A 的每一行,遍历表 B 的所有行,检查 Join 条件是否满足。如果条件满足,则将匹配的行组合成结果。

图片
图片

进一步分析确认 CPU 开销所在

图片
图片

优先尝试看看能不能转换成等值Join,但是从业务角度是没有办法的;通过分析维度表,word 和 pid 是一对多的关系,于是先对维度表按 word 进行聚合操作:SELECT word, GROUP_CONCAT(pid) AS pid_list;

聚合后的数据量少了8倍左右,也就是说在 NESTLOOP JOIN 避免不了的情况下,匹配的数据量减少了8倍;然后再进行 JOIN 后再打平,改进后的 SQL 如下:

代码语言:javascript
复制
SELECT t.pid,        b.uidFROM table_xx aJOIN search_expo bCROSS JOIN UNNEST(SPLIT(a.pid_list, ',')) AS t(pid)WHERE INSTR(b.query, a.word) > 0;

优化后的结果,从原来的十几分钟耗时减少到 3 分钟内;

图片
图片

通过执行计划,我们可以分析:

  • Join 方式:使用的是 Broadcast、Shuffle、NESTLOOP JOIN 等 哪种Join 方式
  • 检查数据倾斜:观察 Fragment 的行数(cardinality)
  • 谓词下推:是否将过滤条件尽可能推到 SCAN 扫描阶段
  • 分区裁剪:验证是否有效裁剪分区,避免全表扫描
  • 物化视图选择:是否命中合适的物化视图以加速查询
  • 等等等

在下一篇文章《StarRocks 查询探秘(七):执行计划分段 PlanFragment 的调度》中,深入分析 StarRocks 调度器 Coordinator 是如何进行 PlanFragment 的调度分发,并以线上SQL Scan 数据不均衡导致的耗时长,剖析 Selector 的计算逻辑,敬请期待!

更多大数据干货,欢迎关注我的微信公众号—BigData共享

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ExecPlan - 执行计划
  • PlanFragment - 执行计划分段
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档