跳到主要内容

查询分析

查询分析

Doris 提供了一个图形化的命令以帮助用户更方便的分析一个具体的查询或导入。本文介绍如何使用该功能。

查询计划树

SQL 是一个描述性语言,用户通过一个 SQL 来描述想获取的数据。而一个 SQL 的具体执行方式依赖于数据库的实现。而查询规划器就是用来决定数据库如何具体执行一个 SQL 的。

比如用户指定了一个 Join 算子,则查询规划器需要决定具体的 Join 算法,比如是 Hash Join,还是 Merge Sort Join;是使用 Shuffle 还是 Broadcast;Join 顺序是否需要调整以避免笛卡尔积;以及确定最终的在哪些节点执行等等。

Doris 的查询规划过程是先将一个 SQL 语句转换成一个单机执行计划树。

     ┌────┐
│Sort│
└────┘

┌───────────┐
│Aggregation│
└───────────┘

┌────┐
│Join│
└────┘
┌───┴────┐
┌──────┐ ┌──────┐
│Scan-1│ │Scan-2│
└──────┘ └──────┘

之后,查询规划器会根据具体的算子执行方式、数据的具体分布,将单机查询计划转换为分布式查询计划。分布式查询计划是由多个 Fragment 组成的,每个 Fragment 负责查询计划的一部分,各个 Fragment 之间会通过 ExchangeNode 算子进行数据的传输。

        ┌────┐
│Sort│
│F1 │
└────┘

┌───────────┐
│Aggregation│
│F1 │
└───────────┘

┌────┐
│Join│
│F1 │
└────┘
┌──────┴────┐
┌──────┐ ┌────────────┐
│Scan-1│ │ExchangeNode│
│F1 │ │F1 │
└──────┘ └────────────┘

┌──────────────┐
│DataStreamDink│
│F2 │
└──────────────┘

┌──────┐
│Scan-2│
│F2 │
└──────┘

如上图,我们将单机计划分成了两个 Fragment:F1 和 F2。两个 Fragment 之间通过一个 ExchangeNode 节点传输数据。

而一个 Fragment 会进一步的划分为多个 Instance。Instance 是最终具体的执行实例。划分成多个 Instance 有助于充分利用机器资源,提升一个 Fragment 的执行并发度。

查看查询计划

可以通过以下两种命令查看一个 SQL 的执行计划。

  • EXPLAIN GRAPH select ...;
  • EXPLAIN select ...;

其中第一个命令以图形化的方式展示一个查询计划,这个命令可以比较直观的展示查询计划的树形结构,以及 Fragment 的划分情况:

mysql> desc graph select tbl1.k1, sum(tbl1.k2) from tbl1 join tbl2 on tbl1.k1 = tbl2.k1 group by tbl1.k1 order by tbl1.k1;
+---------------------------------------------------------------------------------------------------------------------------------+
| Explain String |
+---------------------------------------------------------------------------------------------------------------------------------+
| |
| ┌───────────────┐ |
|[9: ResultSink]|
|[Fragment: 4]|
| │RESULT SINK │ |
| └───────────────┘ |
||
| ┌─────────────────────┐ |
|[9: MERGING-EXCHANGE]|
|[Fragment: 4]|
| └─────────────────────┘ |
||
| ┌───────────────────┐ |
|[9: DataStreamSink]|
|[Fragment: 3]|
| │STREAM DATA SINK │ |
| │ EXCHANGE ID: 09|
| │ UNPARTITIONED │ |
| └───────────────────┘ |
||
| ┌─────────────┐ |
|[4: TOP-N]|
|[Fragment: 3]|
| └─────────────┘ |
||
| ┌───────────────────────────────┐ |
|[8: AGGREGATE (merge finalize)]|
|[Fragment: 3]|
| └───────────────────────────────┘ |
||
| ┌─────────────┐ |
|[7: EXCHANGE]|
|[Fragment: 3]|
| └─────────────┘ |
||
| ┌───────────────────┐ |
|[7: DataStreamSink]|
|[Fragment: 2]|
| │STREAM DATA SINK │ |
| │ EXCHANGE ID: 07|
| │ HASH_PARTITIONED │ |
| └───────────────────┘ |
||
| ┌─────────────────────────────────┐ |
|[3: AGGREGATE (update serialize)]|
|[Fragment: 2]|
| │STREAMING │ |
| └─────────────────────────────────┘ |
||
| ┌─────────────────────────────────┐ |
|[2: HASH JOIN]|
|[Fragment: 2]|
|join op: INNER JOIN (PARTITIONED)|
| └─────────────────────────────────┘ |
| ┌──────────┴──────────┐ |
| ┌─────────────┐ ┌─────────────┐ |
|[5: EXCHANGE]│ │[6: EXCHANGE]|
|[Fragment: 2]│ │[Fragment: 2]|
| └─────────────┘ └─────────────┘ |
| │ │ |
| ┌───────────────────┐ ┌───────────────────┐ |
|[5: DataStreamSink]│ │[6: DataStreamSink]|
|[Fragment: 0] │ │[Fragment: 1]|
| │STREAM DATA SINK │ │STREAM DATA SINK │ |
| │ EXCHANGE ID: 05 │ │ EXCHANGE ID: 06|
| │ HASH_PARTITIONED │ │ HASH_PARTITIONED │ |
| └───────────────────┘ └───────────────────┘ |
| │ │ |
| ┌─────────────────┐ ┌─────────────────┐ |
|[0: OlapScanNode]│ │[1: OlapScanNode]|
|[Fragment: 0] │ │[Fragment: 1]|
|TABLE: tbl1 │ │TABLE: tbl2 │ |
| └─────────────────┘ └─────────────────┘ |
+---------------------------------------------------------------------------------------------------------------------------------+

从图中可以看出,查询计划树被分为了5个 Fragment:0、1、2、3、4。如 OlapScanNode 节点上的 [Fragment: 0] 表示这个节点属于 Fragment 0。每个Fragment之间都通过 DataStreamSink 和 ExchangeNode 进行数据传输。

图形命令仅展示简化后的节点信息,如果需要查看更具体的节点信息,如下推到节点上的过滤条件等,则需要通过第二个命令查看更详细的文字版信息:

mysql> explain select tbl1.k1, sum(tbl1.k2) from tbl1 join tbl2 on tbl1.k1 = tbl2.k1 group by tbl1.k1 order by tbl1.k1;
+----------------------------------------------------------------------------------+
| Explain String |
+----------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:<slot 5> <slot 3> `tbl1`.`k1` | <slot 6> <slot 4> sum(`tbl1`.`k2`) |
| PARTITION: UNPARTITIONED |
| |
| RESULT SINK |
| |
| 9:MERGING-EXCHANGE |
| limit: 65535 |
| |
| PLAN FRAGMENT 1 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: <slot 3> `tbl1`.`k1` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 09 |
| UNPARTITIONED |
| |
| 4:TOP-N |
| | order by: <slot 5> <slot 3> `tbl1`.`k1` ASC |
| | offset: 0 |
| | limit: 65535 |
| | |
| 8:AGGREGATE (merge finalize) |
| | output: sum(<slot 4> sum(`tbl1`.`k2`)) |
| | group by: <slot 3> `tbl1`.`k1` |
| | cardinality=-1 |
| | |
| 7:EXCHANGE |
| |
| PLAN FRAGMENT 2 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: `tbl1`.`k1` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 07 |
| HASH_PARTITIONED: <slot 3> `tbl1`.`k1` |
| |
| 3:AGGREGATE (update serialize) |
| | STREAMING |
| | output: sum(`tbl1`.`k2`) |
| | group by: `tbl1`.`k1` |
| | cardinality=-1 |
| | |
| 2:HASH JOIN |
| | join op: INNER JOIN (PARTITIONED) |
| | runtime filter: false |
| | hash predicates: |
| | colocate: false, reason: table not in the same group |
| | equal join conjunct: `tbl1`.`k1` = `tbl2`.`k1` |
| | cardinality=2 |
| | |
| |----6:EXCHANGE |
| | |
| 5:EXCHANGE |
| |
| PLAN FRAGMENT 3 |
| OUTPUT EXPRS: |
| PARTITION: RANDOM |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 06 |
| HASH_PARTITIONED: `tbl2`.`k1` |
| |
| 1:OlapScanNode |
| TABLE: tbl2 |
| PREAGGREGATION: ON |
| partitions=1/1 |
| rollup: tbl2 |
| tabletRatio=3/3 |
| tabletList=105104776,105104780,105104784 |
| cardinality=1 |
| avgRowSize=4.0 |
| numNodes=6 |
| |
| PLAN FRAGMENT 4 |
| OUTPUT EXPRS: |
| PARTITION: RANDOM |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 05 |
| HASH_PARTITIONED: `tbl1`.`k1` |
| |
| 0:OlapScanNode |
| TABLE: tbl1 |
| PREAGGREGATION: ON |
| partitions=1/1 |
| rollup: tbl1 |
| tabletRatio=3/3 |
| tabletList=105104752,105104763,105104767 |
| cardinality=2 |
| avgRowSize=8.0 |
| numNodes=6 |
+----------------------------------------------------------------------------------+

查询计划中显示的信息还在不断规范和完善中,我们将在后续的文章中详细介绍。

查看查询 Profile

用户可以通过以下命令打开会话变量 is_report_success

SET is_report_success=true;

然后执行查询,则 Doris 会产生该查询的一个 Profile。Profile 包含了一个查询各个节点的具体执行情况,有助于我们分析查询瓶颈。

执行完查询后,我们可以通过如下命令先获取 Profile 列表:

mysql> show query profile "/"\G
*************************** 1. row ***************************
QueryId: c257c52f93e149ee-ace8ac14e8c9fef9
User: root
DefaultDb: default_cluster:db1
SQL: select tbl1.k1, sum(tbl1.k2) from tbl1 join tbl2 on tbl1.k1 = tbl2.k1 group by tbl1.k1 order by tbl1.k1
QueryType: Query
StartTime: 2021-04-08 11:30:50
EndTime: 2021-04-08 11:30:50
TotalTime: 9ms
QueryState: EOF

这个命令会列出当前保存的所有 Profile。每行对应一个查询。我们可以选择我们想看的 Profile 对应的 QueryId,查看具体情况。

查看一个Profile分为3个步骤:

  1. 查看整体执行计划树

    这一步主要用于从整体分析执行计划,并查看每个Fragment的执行耗时。

    mysql> show query profile "/c257c52f93e149ee-ace8ac14e8c9fef9"\G
    *************************** 1. row ***************************
    Fragments:
    ┌──────────────────────┐
    [-1: DataBufferSender]
    │Fragment: 0
    │MaxActiveTime: 6.626ms│
    └──────────────────────┘

    ┌──────────────────┐
    [9: EXCHANGE_NODE]
    │Fragment: 0
    └──────────────────┘

    ┌──────────────────────┐
    [9: DataStreamSender]
    │Fragment: 1
    │MaxActiveTime: 5.449ms│
    └──────────────────────┘

    ┌──────────────┐
    [4: SORT_NODE]
    │Fragment: 1
    └──────────────┘
    ┌┘
    ┌─────────────────────┐
    [8: AGGREGATION_NODE]
    │Fragment: 1
    └─────────────────────┘
    └┐
    ┌──────────────────┐
    [7: EXCHANGE_NODE]
    │Fragment: 1
    └──────────────────┘

    ┌──────────────────────┐
    [7: DataStreamSender]
    │Fragment: 2
    │MaxActiveTime: 3.505ms│
    └──────────────────────┘
    ┌┘
    ┌─────────────────────┐
    [3: AGGREGATION_NODE]
    │Fragment: 2
    └─────────────────────┘

    ┌───────────────────┐
    [2: HASH_JOIN_NODE]
    │Fragment: 2
    └───────────────────┘
    ┌────────────┴────────────┐
    ┌──────────────────┐ ┌──────────────────┐
    [5: EXCHANGE_NODE]│ │[6: EXCHANGE_NODE]
    │Fragment: 2 │ │Fragment: 2
    └──────────────────┘ └──────────────────┘
    │ │
    ┌─────────────────────┐ ┌────────────────────────┐
    [5: DataStreamSender]│ │[6: DataStreamSender]
    │Fragment: 4 │ │Fragment: 3
    │MaxActiveTime: 1.87ms│ │MaxActiveTime: 636.767us│
    └─────────────────────┘ └────────────────────────┘
    │ ┌┘
    ┌───────────────────┐ ┌───────────────────┐
    [0: OLAP_SCAN_NODE]│ │[1: OLAP_SCAN_NODE]
    │Fragment: 4 │ │Fragment: 3
    └───────────────────┘ └───────────────────┘
    │ │
    ┌─────────────┐ ┌─────────────┐
    [OlapScanner]│ │[OlapScanner]
    │Fragment: 4 │ │Fragment: 3
    └─────────────┘ └─────────────┘
    │ │
    ┌─────────────────┐ ┌─────────────────┐
    [SegmentIterator]│ │[SegmentIterator]
    │Fragment: 4 │ │Fragment: 3
    └─────────────────┘ └─────────────────┘

    1 row in set (0.02 sec)

    如上图,每个节点都标注了自己所属的 Fragment,并且在每个 Fragment 的 Sender节点,标注了该 Fragment 的执行耗时。这个耗时,是Fragment下所有 Instance 执行耗时中最长的一个。这个有助于我们从整体角度发现最耗时的 Fragment。

  2. 查看具体 Fragment 下的 Instance 列表

    比如我们发现 Fragment 1 耗时最长,则可以继续查看 Fragment 1 的 Instance 列表:

    mysql> show query profile "/c257c52f93e149ee-ace8ac14e8c9fef9/1";
    +-----------------------------------+-------------------+------------+
    | Instances | Host | ActiveTime |
    +-----------------------------------+-------------------+------------+
    | c257c52f93e149ee-ace8ac14e8c9ff03 | 10.200.00.01:9060 | 5.449ms |
    | c257c52f93e149ee-ace8ac14e8c9ff05 | 10.200.00.02:9060 | 5.367ms |
    | c257c52f93e149ee-ace8ac14e8c9ff04 | 10.200.00.03:9060 | 5.358ms |
    +-----------------------------------+-------------------+------------+

    这里展示了 Fragment 1 上所有的 3 个 Instance 所在的执行节点和耗时。

  3. 查看具体 Instance

    我们可以继续查看某一个具体的 Instance 上各个算子的详细 Profile:

    mysql> show query profile "/c257c52f93e149ee-ace8ac14e8c9fef9/1/c257c52f93e149ee-ace8ac14e8c9ff03"\G
    *************************** 1. row ***************************
    Instance:
    ┌───────────────────────────────────────┐
    [9: DataStreamSender]
    (Active: 37.222us, non-child: 0.40)
    - Counters: │
    - BytesSent: 0.00
    - IgnoreRows: 0
    - OverallThroughput: 0.0 /sec │
    - PeakMemoryUsage: 8.00 KB │
    - SerializeBatchTime: 0ns │
    - UncompressedRowBatchSize: 0.00
    └───────────────────────────────────────┘
    └┐

    ┌──────────────────────────────────┐
    [4: SORT_NODE]
    (Active: 5.421ms, non-child: 0.71)
    - Counters: │
    - PeakMemoryUsage: 12.00 KB │
    - RowsReturned: 0
    - RowsReturnedRate: 0
    └──────────────────────────────────┘
    ┌┘

    ┌───────────────────────────────────┐
    [8: AGGREGATION_NODE]
    (Active: 5.355ms, non-child: 10.68)
    - Counters: │
    - BuildTime: 3.701us │
    - GetResultsTime: 0ns │
    - HTResize: 0
    - HTResizeTime: 1.211us │
    - HashBuckets: 0
    - HashCollisions: 0
    - HashFailedProbe: 0
    - HashFilledBuckets: 0
    - HashProbe: 0
    - HashTravelLength: 0
    - LargestPartitionPercent: 0
    - MaxPartitionLevel: 0
    - NumRepartitions: 0
    - PartitionsCreated: 16
    - PeakMemoryUsage: 34.02 MB │
    - RowsProcessed: 0
    - RowsRepartitioned: 0
    - RowsReturned: 0
    - RowsReturnedRate: 0
    - SpilledPartitions: 0
    └───────────────────────────────────┘
    └┐

    ┌──────────────────────────────────────────┐
    [7: EXCHANGE_NODE]
    (Active: 4.360ms, non-child: 46.84)
    - Counters: │
    - BytesReceived: 0.00
    - ConvertRowBatchTime: 387ns │
    - DataArrivalWaitTime: 4.357ms │
    - DeserializeRowBatchTimer: 0ns │
    - FirstBatchArrivalWaitTime: 4.356ms│
    - PeakMemoryUsage: 0.00
    - RowsReturned: 0
    - RowsReturnedRate: 0
    - SendersBlockedTotalTimer(*): 0ns │
    └──────────────────────────────────────────┘

    上图展示了 Fragment 1 中,Instance c257c52f93e149ee-ace8ac14e8c9ff03 的各个算子的具体 Profile。

通过以上3个步骤,我们可以逐步排查一个SQL的性能瓶颈。