查询分析

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

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

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

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

    1. Sort
    2. F1
    3. └────┘
    4. ┌───────────┐
    5. Aggregation
    6. F1
    7. └───────────┘
    8. ┌────┐
    9. Join
    10. F1
    11. └────┘
    12. ┌──────┴────┐
    13. ┌──────┐ ┌────────────┐
    14. Scan-1 ExchangeNode
    15. F1 F1
    16. └──────┘ └────────────┘
    17. ┌──────────────┐
    18. DataStreamDink
    19. F2
    20. └──────────────┘
    21. ┌──────┐
    22. Scan-2
    23. F2
    24. └──────┘

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

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

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

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

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

    1. 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;
    2. +---------------------------------------------------------------------------------------------------------------------------------+
    3. | Explain String |
    4. +---------------------------------------------------------------------------------------------------------------------------------+
    5. | |
    6. | ┌───────────────┐ |
    7. | │[9: ResultSink]│ |
    8. | │[Fragment: 4] |
    9. | RESULT SINK |
    10. | └───────────────┘ |
    11. | |
    12. | ┌─────────────────────┐ |
    13. | │[9: MERGING-EXCHANGE]│ |
    14. | │[Fragment: 4] |
    15. | └─────────────────────┘ |
    16. | |
    17. | ┌───────────────────┐ |
    18. | │[9: DataStreamSink]│ |
    19. | │[Fragment: 3] |
    20. | STREAM DATA SINK |
    21. | EXCHANGE ID: 09 |
    22. | UNPARTITIONED |
    23. | └───────────────────┘ |
    24. | |
    25. | ┌─────────────┐ |
    26. | │[4: TOP-N] |
    27. | │[Fragment: 3]│ |
    28. | └─────────────┘ |
    29. | |
    30. | ┌───────────────────────────────┐ |
    31. | │[8: AGGREGATE (merge finalize)]│ |
    32. | │[Fragment: 3] |
    33. | |
    34. | ┌─────────────┐ |
    35. | │[Fragment: 3]│ |
    36. | └─────────────┘ |
    37. | |
    38. | ┌───────────────────┐ |
    39. | │[7: DataStreamSink]│ |
    40. | │[Fragment: 2] |
    41. | STREAM DATA SINK |
    42. | EXCHANGE ID: 07 |
    43. | HASH_PARTITIONED |
    44. | └───────────────────┘ |
    45. | |
    46. | ┌─────────────────────────────────┐ |
    47. | │[3: AGGREGATE (update serialize)]│ |
    48. | │[Fragment: 2] |
    49. | STREAMING |
    50. | └─────────────────────────────────┘ |
    51. | |
    52. | ┌─────────────────────────────────┐ |
    53. | │[2: HASH JOIN] |
    54. | │[Fragment: 2] |
    55. | join op: INNER JOIN (PARTITIONED)│ |
    56. | └─────────────────────────────────┘ |
    57. | ┌──────────┴──────────┐ |
    58. | ┌─────────────┐ ┌─────────────┐ |
    59. | │[5: EXCHANGE]│ │[6: EXCHANGE]│ |
    60. | │[Fragment: 2]│ │[Fragment: 2]│ |
    61. | └─────────────┘ └─────────────┘ |
    62. | |
    63. | ┌───────────────────┐ ┌───────────────────┐ |
    64. | │[5: DataStreamSink]│ │[6: DataStreamSink]│ |
    65. | │[Fragment: 0] │[Fragment: 1] |
    66. | STREAM DATA SINK STREAM DATA SINK |
    67. | EXCHANGE ID: 05 EXCHANGE ID: 06 |
    68. | HASH_PARTITIONED HASH_PARTITIONED |
    69. | └───────────────────┘ └───────────────────┘ |
    70. | |
    71. | ┌─────────────────┐ ┌─────────────────┐ |
    72. | │[0: OlapScanNode]│ │[1: OlapScanNode]│ |
    73. | │[Fragment: 0] │[Fragment: 1] |
    74. | TABLE: tbl1 TABLE: tbl2 |
    75. | └─────────────────┘ └─────────────────┘ |
    76. +---------------------------------------------------------------------------------------------------------------------------------+

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

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

    1. SET is_report_success=true;

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

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

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

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

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

    1. 查看整体执行计划树

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

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

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

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

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

    3. 查看具体 Instance

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

      1. *************************** 1. row ***************************
      2. Instance:
      3. ┌───────────────────────────────────────┐
      4. │[9: DataStreamSender]
      5. │(Active: 37.222us, non-child: 0.40)
      6. - Counters:
      7. - BytesSent: 0.00
      8. - IgnoreRows: 0
      9. - OverallThroughput: 0.0 /sec
      10. - PeakMemoryUsage: 8.00 KB
      11. - SerializeBatchTime: 0ns
      12. - UncompressedRowBatchSize: 0.00
      13. └───────────────────────────────────────┘
      14. └┐
      15. ┌──────────────────────────────────┐
      16. │[4: SORT_NODE]
      17. │(Active: 5.421ms, non-child: 0.71)│
      18. - Counters:
      19. - PeakMemoryUsage: 12.00 KB
      20. - RowsReturned: 0
      21. - RowsReturnedRate: 0
      22. └──────────────────────────────────┘
      23. ┌┘
      24. ┌───────────────────────────────────┐
      25. │[8: AGGREGATION_NODE]
      26. │(Active: 5.355ms, non-child: 10.68)│
      27. - Counters:
      28. - BuildTime: 3.701us
      29. - GetResultsTime: 0ns
      30. - HTResize: 0
      31. - HTResizeTime: 1.211us
      32. - HashBuckets: 0
      33. - HashCollisions: 0
      34. - HashFailedProbe: 0
      35. - HashFilledBuckets: 0
      36. - HashProbe: 0
      37. - HashTravelLength: 0
      38. - LargestPartitionPercent: 0
      39. - MaxPartitionLevel: 0
      40. - NumRepartitions: 0
      41. - PartitionsCreated: 16
      42. - PeakMemoryUsage: 34.02 MB
      43. - RowsProcessed: 0
      44. - RowsRepartitioned: 0
      45. - RowsReturned: 0
      46. - RowsReturnedRate: 0
      47. - SpilledPartitions: 0
      48. └───────────────────────────────────┘
      49. └┐
      50. ┌──────────────────────────────────────────┐
      51. │[7: EXCHANGE_NODE]
      52. │(Active: 4.360ms, non-child: 46.84)
      53. - Counters:
      54. - BytesReceived: 0.00
      55. - ConvertRowBatchTime: 387ns
      56. - DataArrivalWaitTime: 4.357ms
      57. - DeserializeRowBatchTimer: 0ns
      58. - FirstBatchArrivalWaitTime: 4.356ms
      59. - PeakMemoryUsage: 0.00
      60. - RowsReturned: 0
      61. - RowsReturnedRate: 0

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

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