ITPub博客

首页 > 大数据 > 数据挖掘 > StarRocks 查询探秘(六):从 EXPLAIN SQL 看执行计划的构建与优化

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

原创 数据挖掘 作者:乐逍遥happy 时间:2025-09-17 18:33:34 0 删除 编辑

题外话

       在写《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 为例:

    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  等功能。

      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 关联查询,摘取其中的一个子句如下:

         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 如下:


          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 的计算逻辑,敬请期待!


          来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70046653/viewspace-3095982/ ,如需转载,请注明出处,否则将追究法律责任。

          请登录后发表评论 登录
          全部评论

          注册时间:2025-06-05

          • 博文量
            18
          • 访问量
            17427