[HINT_EXPLAIN]

首发版本

1.30.16、2.00.4

通过在 select / exec 关键字后添加 [HINT_EXPLAIN] 来显示 SQL 语句的执行过程,便于 SQL 查询中实时监测查询的速度和执行的顺序。形式如下:

select [HINT_EXPLAIN] * from tb where id > 20

注意:添加 [HINT_EXPLAIN] 后的查询语句仅返回一个 JSON 字符串显示了 SQL 的执行过程,不返回查询结果;对于 UPDATE 或者 DELETE 语句,目前还不支持查看执行计划。

[HINT_EXPLAIN] 支持普通查询(查询内存表或非分区表)和分布式查询(查询分区表),分布式查询比普通查询的 explain 多了 map, mergereduce 标签。下面以一个分布式查询为例,来说明 JSON 字符串内各标签的组成和含义。

返回的 JSON 字符串的最外层包含 measuremenetexplain 两个标签。

measuremenet

"measurement":"microsecond"

表示 SQL 查询执行计划的开销时间的单位为微秒。内部所有 cost 标签表示的耗时,均以微秒为单位。

explain

explain 内包含的标签从上往下的顺序代表该 explain 对应的 SQL 语句的执行顺序。由于 SQL 查询可能包含嵌套子句,因此 explain 可能会嵌套在其它子标签中。

explain 的构成如下:

  • rows:查询返回的记录数。

  • cost:查询耗时。

{
    "measurement":"microsecond",
    "explain": {
        /* 可能包含的其他子标签模块 */
        "rows": 20000,
        "cost": 100
    }
}

分布式查询包含三个阶段 map → merge → reduce,通常这三个标签包含在根标签 explain 中, 例如:

{
    "measurement":"microsecond",
    "explain":{
        "from":{...},
        "map":{...},
        "merge":{...},
        "reduce":{...},
        "rows":10,
        "cost":23530
    }
}

通常,系统会对某些查询条件进行优化。单机模式创建一个以 date 列为分区的分区表,查询该表的 date 列在单个分区的全部数据。

select [HINT_EXPLAIN] * from t1 where date = 2022.01.01

此时,该查询语句会被优化,返回如下的结果:

{
    "measurement":"microsecond",
    "explain":{
        "from":{ ... },
        "optimize":{ ... },
        "rows":185,
        "cost":987
    }
}

from

其中 from 标签,说明了 SQL 语句中 from 子句解析后的执行过程。

根据 from 子句的不同, from 标签模块下可能包含着不同的子模块。以下列举了几种可能的场景:

(1) from 后接一个表对象,例如 select * from pt

"from": {
    "cost": 3  // from 子句的耗时
}

(2) from 子句嵌套 SQL 语句,例如 select * from select * from pt

"from": {
    "cost": 3,  // from 子句的耗时
    "detail": {
        "sql": "select * from pt",  // 嵌套的SQL语句
        "explain": { ... } // 嵌套的SQL语句的explain
    }
}

可以看到由于包含嵌套的子句, explain 标签嵌套在了 from 标签中。

(3) from 跟表连接相关的子句,例如 select * from lsj(pt1, pt2, "date")

"from": {
"cost": 3,  // from 子句的耗时
"detail": "materialize for JOIN" // 表示子句中包含 join 操作
}

注意:这里的 from 子句为 lsj(pt1, pt2, "date"),此时的 cost 耗时只是获取待连接的表 pt1, pt2 数据源的耗时,两表并未发生 join。

map

map 阶段会统计 SQL 查询涉及到的所有分区(包括本地节点 local 和远程节点 remote 的分区),然后生成相应的子查询,并把子查询分发到涉及到的所有分区,进行并行查询。

下面是一个 map 标签模块中可能包含的子模块:

  • generate_reshuffle_query:是分布式 join 独有的标签,包含进行 generate_reshuffle_query 过程的信息。generate_reshuffle_query 是进行分布式 join 前将数据按照 join 列连续存储在内存区的一个操作。如果 join 列是分区列,则不会进行该操作。(130版本不支持分布式 join)

  • partitions : 本次查询的分区信息,其子标签 local 和 remote 分别表示查询的分区位于本地节点的数量和查询的分区位于远程节点的数量。

  • cost:map 阶段完成所有过程所消耗的总时间(此时所有子查询都执行完成)。

  • detail:map 阶段查询子句的执行细节。

    a. most:耗时最多的子查询的信息。
    b. least:耗时最少的子查询的信息。
"map": {
    "generate_reshuffle_query": {
        "cost": 2
    },
    "partitions": {
        "local": 10,
        "remote": 5,
    },
    "cost": 100,
    "detail": {
        "most": {
        "sql": "select time,id,value from pt [partition = /iot/1]",
        "explain": { ... }
        },
        "least": {
        "sql": "select time,id,value from pt [partition = /iot/4]",
        "explain": { ... },
        }
    }
}

如果 map 阶段进行了查询优化,会返回区别于上面的执行计划。此时,不包含 mergereduce 阶段。

optimize

查询优化会显示在 optimize 标签模块中。

optimize:查询优化的细节。组成如下所示:

field:可以是被优化的字段,如:"where", "join", "group";也可以是优化的场景说明,如 "single partition query"。

"optimize": {
    "cost": 3,
    "field": ["where", "join", "group"],
    "sql": "..."  // 优化之后的SQL语句
}

以下列举了几种可能优化的场景:

(1) 只查询单个分区数据,其 map 阶段的执行过程如下:

"map": {
    "partitions": {
        "local": 1,   // 或者0
        "remote": 0,   // 或者1
    },
    "cost": 100,
    "optimize": {
        "field": "single partition query",
        "sql": "...",
        "explain": { ... }
    }
}

单分区情况下,查询任务只涉及到对应分区的单个节点,因此不需要进行 merge

(2) context by + csort + limit

对于 context by 和 csort 以及 limit 配合使用的 SQL 查询,系统内部进行了查询优化:

select * from pt where device in ["a", "b"] context by device csort time limit n

其中 pt 可以是单分区表或者组合分区的分区表。优化需满足以下几个条件:

1. context by 指定的列在 where 子句中进行了条件过滤。 比如上例中的 device, where 子句中筛选了device in ["a", "b"] 或 device = "a" 的数据。

2. csort 指定的列(如例中的 time)是分区列, 且分区方式是 VALUE 或 RANGE。

3. 包含 limit 子句,n 为整数(可以为负数)。

4. csort, context by 只能指定单列。

5. context by 指定的列也需要一起输出(通过 select 语句指定该列)。

此时返回的结果中, map 标签模块的组成如下所示:

"map":{
    "optimize":{
        "cost":4,  // 完成优化耗费的时间
        "field":"optimize for CONTEXT BY + CSORT + LIMIT: partial parallel execution."
    },
    "cost":1082  // 完成map阶段耗费的时间
}

其中,根据 SQL 语句和表的分区方式, field 可以为:

  • optimize for CONTEXT BY + CSORT + LIMIT: partial parallel execution.

  • optimize for CONTEXT BY + CSORT + LIMIT: serial execution.

merge

merge 阶段会将 map 阶段分配到各个节点的子查询的结果进行合并。

下面是一个 merge 标签模块中可能包含的子模块:

  • rowmerge 后的结果的总行数。

  • costmerge 阶段消耗的时间。

  • detailmerge 阶段查询子句的执行细节。

    a. most:返回行数最多的子查询的信息。
    b. least:返回行数最少的子查询的信息。
"merge": {
    "row": 10000,
    "cost": 50,
    "detail": {
        "most": {  // 返回行数最多的SubQuery
        "sql": "select time,id,value from pt [partition = /iot/6]",
        "explain": { ...}
        },
        "least": {
        "sql": "select time,id,value from pt [partition = /iot/9]",
        "explain": { ...}
        }
    }
}

reduce

reduce 阶段通常是对子查询返回的结果做收尾处理,通常是对 merge 的结果表做最后一次查询。执行计划是否包含 reduce 阶段视具体情况而定。

下面介绍一个 reduce 标签模块中可能包含的子模块:

"reduce": {
    "sql": "...",  // Final Query
    "explain": { ... }
}

除了上述在分布式查询各个阶段说明的标签模块以外,实际运行的结果中还可能包含一部分其他 SQL 相关的子标签模块。显示如下:

注意:130版本无 sortKey 标签。

join, csort, sort以及pivotBy标签模块,只包含 cost 指标。

"join": {
"cost": 10
}

groupBy

  • sortKey:是否利用了表的 sortColumn 来进行 groupBy。如果该值为true, 则没有 algo 标签。

  • algo:分组算法, 可以为: "hash", "vectorize", "sort"。

  • fillinterval 插值填充过程的标签。

"groupBy":{
    "sortKey":false,
    "algo":"hash",
    "cost":8
}

// group by 搭配 interval 使用
"groupBy": {
    "sortKey": false,
    "algo": "hash",
    "fill": {
        "cost": 23
    },
    "cost": 248
}

contextBy

  • sortKey:是否利用了表的 sortColumn 来做 contextBy。

"contextBy":{
    "sortKey":false,
    "cost":1994
}

通过打印 SQL 执行过程,分析 SQL 语句执行中每一部分的耗时,可以帮助我们优化 SQL 语句,提升执行效率。