显示语句的查询执行计划。
语法:
EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ANALYZE | ESTIMATE | TABLE OVERRIDE | WHATIF] [setting = value, ...]
[
SELECT ... |
tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [TTL ...]
]
[FORMAT ...]示例:
EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV;Output: sum(number)
Union
├EXPLAIN 类型
AST— 抽象语法树。SYNTAX— 经 AST 层级优化后的查询文本。QUERY TREE— 经查询树层级优化后的查询树。PLAN— 查询执行计划。PIPELINE— 查询执行流水线。ANALYZE— 执行查询,并用测得的运行时指标为执行计划添加注释。ESTIMATE— 处理查询时,将从表中读取的预估行数、标记数和 parts 数量。TABLE OVERRIDE— 表函数 schema 上表覆盖的已验证结果。
EXPLAIN AST
转储查询的 AST。支持所有类型的查询,不仅限于 SELECT。
设置:
graph– 以 DOT 图描述语言定义的图形式输出 AST。默认值:0。
示例:
EXPLAIN AST SELECT 1;SelectWithUnionQuery (children 1)
ExpressionList (children 1)
SelectQuery (children 1)
ExpressionList (children 1)
Literal UInt64_1EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today(); explain
AlterQuery t1 (children 1)
ExpressionList (children 1)
AlterCommand 27 (children 1)
Function equals (children 1)
ExpressionList (children 2)
Identifier date
Function today (children 1)
ExpressionListEXPLAIN SYNTAX
显示查询在语法分析后的抽象语法树 (AST) 。
其过程是:解析查询、构建查询 AST 和查询树,并可选择运行查询分析器和优化阶段,然后再将查询树转换回查询 AST。
设置:
oneline– 以单行形式输出查询。默认值:0。run_query_tree_passes– 在转储查询树之前运行查询树处理阶段。默认值:0。query_tree_passes– 如果设置了run_query_tree_passes,则指定要运行的处理阶段数量。若未指定query_tree_passes,则会运行所有处理阶段。single_record– 将格式化后的查询作为单个多行记录返回,而不是每行一个记录。默认值:1(由explain_syntax_single_record设置控制) 。设置为0可恢复历史的每行一个记录输出,也可以将explain_syntax_single_record = 0(全局或在每个查询的SETTINGS中) 设置为0,或将compatibility设置为早于26.8的任意版本。
示例:
EXPLAIN SYNTAX SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;SELECT *
FROM system.numbers AS a, system.numbers AS b, system.numbers AS c
WHERE (a.number = b.number) AND (b.number = c.number)使用 run_query_tree_passes 时:
EXPLAIN SYNTAX run_query_tree_passes = 1 SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;SELECT
__table1.number AS `a.number`,
__table2.number AS `b.number`,
__table3.number AS `c.number`
FROM system.numbers AS __table1
ALL INNER JOIN system.numbers AS __table2 ON __table1.number = __table2.number
ALL INNER JOIN system.numbers AS __table3 ON __table2.number = __table3.numberEXPLAIN QUERY TREE
设置:
run_passes— 在转储查询树之前运行所有查询树处理阶段。默认值:1。dump_passes— 在转储查询树之前,先转储已使用处理阶段的信息。默认值:0。passes— 指定要运行的处理阶段数量。如果设置为-1,则运行所有处理阶段。默认值:-1。dump_tree— 显示查询树。默认值:1。dump_ast— 显示根据查询树生成的查询 AST。默认值:0。
示例:
EXPLAIN QUERY TREE SELECT id, value FROM test_table;QUERY id: 0
PROJECTION COLUMNS
id UInt64
value String
PROJECTION
LIST id: 1, nodes: 2
COLUMN id: 2, column_name: id, result_type: UInt64, source_id: 3
COLUMN id: 4, column_name: value, result_type: String, source_id: 3
JOIN TREE
TABLE id: 3, table_name: default.test_tableEXPLAIN PLAN
转储查询计划步骤。
设置:
optimize— 控制在显示查询计划之前是否应用查询计划优化。默认值:1。header— 打印步骤的输出请求头。默认值:0。description— 打印步骤说明。默认值:1。indexes— 显示使用到的索引、每个已应用索引过滤掉的 parts 数量,以及过滤掉的粒度数量。默认值:0。支持 MergeTree 表。从 ClickHouse >= v25.9 开始,此语句只有在配合SETTINGS use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0使用时,才会显示合理的输出。projections— 显示所有已分析的投影,以及它们基于投影主键条件对 part 级过滤的影响。对于每个投影,此部分都会包含统计信息,例如通过投影主键评估的 parts、行、标记和范围数量。它还会显示由于这种过滤而跳过了多少 data parts,而无需实际从投影本身读取数据。某个投影是否实际用于读取,还是仅用于过滤分析,可通过description字段判断。默认值:0。支持 MergeTree 表。actions— 打印步骤操作的详细信息。默认值:1。sorting— 为每个生成有序输出的计划步骤打印排序说明。默认值:0。keep_logical_steps— 对 joins 保留逻辑计划步骤,而不是将其转换为物理 join 实现。默认值:0。json— 以 JSON 格式将查询计划步骤打印为一行。默认值:0。建议使用 TabSeparatedRaw (TSVRaw) 格式,以避免不必要的转义。input_headers— 打印步骤的输入请求头。默认值:0。通常仅对开发者调试输入输出请求头不匹配相关问题时有用。column_structure— 除列名和类型外,还会打印请求头中列的结构。默认值:0。通常仅对开发者调试输入输出请求头不匹配相关问题时有用。distributed— 显示在远程节点上为分布式表或并行副本执行的查询计划。不支持与json一起使用。默认值:0。compact— 启用后,会在计划中隐藏表达式步骤以及详细操作信息 (输入、函数、别名和输出位置) 。仅在actions = 1时生效。默认值:1。pretty— 使用线框字符 (├──、└──、│) 而不是缩进来打印计划树,以便更直观地展示层级结构。还会以内联方式格式化 join 步骤属性。默认值:1。
示例:
EXPLAIN SELECT sum(number) FROM numbers(10) GROUP BY number % 4 LIMIT 1;Output: sum(number)
Limit (preliminary LIMIT)
│当 json = 1 时,查询计划会以 JSON 格式表示。每个节点都是一个字典,并且始终包含 Node Type、Node Id 和 Plans 键。Node Type 是表示步骤名称的字符串,Node Id 是唯一的步骤标识符 (步骤名称加上数字后缀,例如 Union_10) 。Plans 是一个包含子步骤描述的数组。根据节点类型和设置,还可能会添加其他可选键。
示例:
EXPLAIN json = 1, description = 0 SELECT 1 UNION ALL SELECT 2 FORMAT TSVRaw;[
{
"Plan": {
"Node Type": "Union",
"Node Id": "Union_10",
"Plans": [
{
"Node Type": "Expression",
"Node Id": "Expression_13",
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0"
}
]
},
{
"Node Type": "Expression",
"Node Id": "Expression_16",
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_4"
}
]
}
]
}
}
]当 description = 1 时,Description 键会添加到该步骤中:
{
"Node Type": "ReadFromStorage",
"Description": "SystemOne"
}当 header = 1 时,Header 键会以列数组的形式添加到该步骤中。
示例:
EXPLAIN json = 1, description = 0, header = 1 SELECT 1, 2 + dummy;[
{
"Plan": {
"Node Type": "Expression",
"Node Id": "Expression_5",
"Header": [
{
"Name": "1",
"Type": "UInt8"
},
{
"Name": "plus(2, dummy)",
"Type": "UInt16"
}
],
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0",
"Header": [
{
"Name": "dummy",
"Type": "UInt8"
}
]
}
]
}
}
]当 indexes = 1 时,会添加 Indexes 键。它包含一个由已使用索引组成的数组。每个索引都以 JSON 格式描述,包含 Type 键 (其值为字符串 Partition Min-Max、Partition、Statistics、PrimaryKey 或 Skip) 以及以下可选键:
Name— 索引名称 (目前仅用于Skip索引) 。Keys— 该索引使用的列数组。Condition— 使用的条件。Description— 索引描述 (目前仅用于Skip索引) 。Parts— 应用该索引后/前的 parts 数量。Granules— 应用该索引后/前的粒度数量。Ranges— 应用该索引后的粒度范围数量。
示例:
"Node Type": "ReadFromMergeTree",
"Indexes": [
{
"Type": "Partition Min-Max",
"Keys": ["y"],
"Condition": "(y in [1, +inf))",
"Parts": 4/5,
"Granules": 11/12
},
{
"Type": "Partition",
"Keys": ["y", "bitAnd(z, 3)"],
"Condition": "and((bitAnd(z, 3) not in [1, 1]), and((y in [1, +inf)), (bitAnd(z, 3) not in [1, 1])))",
"Parts": 3/4,
"Granules": 10/11
},
{
"Type": "PrimaryKey",
"Keys": ["x", "y"],
"Condition": "and((x in [11, +inf)), (y in [1, +inf)))",
"Parts": 2/3,
"Granules": 6/10,
"Search Algorithm": "generic exclusion search"
},
{
"Type": "Skip",
"Name": "t_minmax",
"Description": "minmax GRANULARITY 2",
"Parts": 1/2,
"Granules": 2/6
},
{
"Type": "Skip",
"Name": "t_set",
"Description": "set GRANULARITY 2",
"": 1/1,
"Granules": 1/2
}
]当 projections = 1 时,将添加 Projections 键。它包含一个已分析的 projections 数组。每个 projection 以 JSON 格式描述,包含以下键:
Name— 投影名称。Condition— 使用的投影主键条件。Description— 关于投影如何使用的描述 (例如 part 级过滤) 。Selected Parts— 该投影选中的 parts 数量。Selected Marks— 选中的标记数量。Selected Ranges— 选中的范围数量。Selected Rows— 选中行数。Filtered Parts— 由于 part 级过滤而跳过的 parts 数量。
示例:
"Node Type": "ReadFromMergeTree",
"Projections": [
{
"Name": "region_proj",
"Description": "Projection has been analyzed and is used for part-level filtering",
"Condition": "(region in ['us_west', 'us_west'])",
"Search Algorithm": "binary search",
"Selected Parts": 3,
"Selected Marks": 3,
"Selected Ranges": 3,
"Selected Rows": 3,
"Filtered Parts": 2
},
{
"Name": "user_id_proj",
"Description": "Projection has been analyzed and is used for part-level filtering",
"Condition": "(user_id in [107, 107])",
"Search Algorithm": "binary search",
"Selected Parts": 1,
"Selected Marks": 1,
"Selected Ranges": 1,
"Selected Rows": 1,
"Filtered Parts": 2
}
]当 actions = 1 时,添加的键取决于步骤类型。
示例:
EXPLAIN json = 1, actions = 1, description = 0 SELECT 1 FORMAT TSVRaw;[
{
"Plan": {
"Node Type": "Expression",
"Node Id": "Expression_5",
"Expression": {
"Inputs": [
{
"Name": "dummy",
"Type": "UInt8"
}
],
"Actions": [
{
"Node Type": "INPUT",
"Result Type": "UInt8",
"Result Name": "dummy",
"Arguments": [0],
"Removed Arguments": [0],
"Result": 0
},
{
"Node Type": "COLUMN",
"Result Type": "UInt8",
"Result Name": "1",
"Column": "Const(UInt8)",
"Arguments": [],
"Removed Arguments": [],
"Result": 1
}
],
"Outputs": [
{
"Name": "1",
"Type": "UInt8"
}
],
"Positions": [1]
},
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0"
}
]
}
}
]当 compact = 0 且 actions = 1 时,可以看到 Expression 步骤以及表达式的详细信息:
EXPLAIN actions = 1, compact = 0 SELECT sum(number) FROM numbers(10) GROUP BY number % 4;Output: sum(number)
Expression ((Project names + Projection))
│ Actions: INPUT : 0 -> sum(__table1.number) UInt64 : 0
│ INPUT :: 1 -> modulo(__table1.number, 4_UInt8) UInt8 : 1
│ ALIAS sum(__table1.number) :: 0 -> sum(number) UInt64 : 2
│ Positions: 2
└──Aggregating
│ Keys: number MOD 4
│ Aggregates: sum(number)
│ Skip merging: 0
└──Expression ((Before GROUP BY + Change column names to column identifiers))
│ Actions: INPUT : 0 -> number UInt64 : 0
│ COLUMN Const(UInt8) -> 4_UInt8 UInt8 : 1
│ ALIAS number :: 0 -> __table1.number UInt64 : 2
│ FUNCTION modulo(__table1.number : 2, 4_UInt8 :: 1) -> modulo(__table1.number, 4_UInt8) UInt8 : 0
│ Positions: 0 2
└──ReadFromSystemNumbers
Output: number当 distributed = 1 时,输出不仅包含本地查询计划,还包含将在远程节点上执行的查询计划。这对于分析和调试分布式查询非常有用。
分布式表示例:
EXPLAIN distributed=1 SELECT * FROM remote('127.0.0.{1,2}', numbers(2)) WHERE number = 1;Union
Expression ((Project names + (Projection + (Change column names to column identifiers + (Project names + Projection)))))
Filter ((WHERE + Change column names to column identifiers))
ReadFromSystemNumbers
Expression ((Project names + (Projection + Change column names to column identifiers)))
ReadFromRemote (Read from remote replica)
Expression ((Project names + Projection))
Filter ((WHERE + Change column names to column identifiers))
ReadFromSystemNumbers并行副本示例:
SET enable_parallel_replicas = 2, max_parallel_replicas = 2, cluster_for_parallel_replicas = 'default';
EXPLAIN distributed=1 SELECT sum(number) FROM test_table GROUP BY number % 4;Expression ((Project names + Projection))
MergingAggregated
Union
Aggregating
Expression ((Before GROUP BY + Change column names to column identifiers))
ReadFromMergeTree (default.test_table)
ReadFromRemoteParallelReplicas
BlocksMarshalling
Aggregating
Expression ((Before GROUP BY + Change column names to column identifiers))
ReadFromMergeTree (default.test_table)在这两个示例中,查询计划均展示了完整的执行流程,包括本地步骤和远程步骤。
当 pretty = 1 时,计划树将以线条字符代替缩进的方式展示,并为关键步骤显示额外信息:
- 查询输出列 会显示在计划顶部。
- 过滤器、聚合键、排序描述和窗口函数中的 表达式 会以人类可读的类 SQL 记法显示 (例如,使用
a + 1 > 5而不是greater(plus(a, 1), 5)) 。为便于理解,内部列标识符前缀 (例如__table1.) 会被移除。 - 源步骤 (例如
ReadFromMergeTree) 会显示其输出列。 - 过滤步骤 会以 SQL 记法显示过滤条件。存在运行时 join 过滤器时,它们会单独显示。
- 聚合步骤 会显示键以及聚合函数及其参数 (例如
sum(c)、count()) 。 - 来自元组字面量的 IN 集合 会显示其值 (大型集合会被截断) ,基于子查询的集合会标记为
subquery1、subquery2等,而来自Setengine 表的集合会显示表名。 - Join 步骤 会使用数学记法显示 join 关系、预估结果行数, 以及哪些输出列来自左侧或右侧。以下符号用于 表示不同的 JOIN types:
| 符号 | Join 类型 |
|---|---|
⋈ |
内连接 |
⟕ |
左连接 |
⟖ |
右连接 |
⟗ |
全连接 |
⋉ |
左半连接 |
⋊ |
右半连接 |
⋉ with strikethrough |
左反连接 |
⋊ with strikethrough |
右反连接 |
× |
交叉连接 |
例如,t1 ⟕ t2 表示表 t1 与 t2 之间的左连接。
表名后方括号中的数字 (例如 t1[100]) 表示预估行数,
前提是表统计信息可用。
pretty 选项与 compact = 1 搭配使用效果很好,它会隐藏 Expression 步骤和详细的动作信息,使执行计划更易于阅读。
一个详细的 JOIN 示例:
CREATE TABLE t1 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
CREATE TABLE t2 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
INSERT INTO t1 SELECT number, toString(number) FROM numbers(100);
INSERT INTO t2 SELECT number, toString(number) FROM numbers(100);
EXPLAIN actions = 1, compact = 1, pretty = 1
SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id FORMAT Raw;Output: id, value, id, value
Join (JOIN FillRightFirst)
│ t1[100] ⋈ t2[100]
│ Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│ Result rows: 100
│ Join conditions: id = id
│ Output:
│ Left: id, value
│ Right: id, value
├──ReadFromMergeTree (default.t1)
│ Read type: Default
│ Parts: 1 | Granules: 1
│ Output: id, value
│ Runtime filters: RF1(id, id from default.t2)
└──BuildRuntimeFilter (Build runtime join filter on id)
│ Filter id: RF1
│ Source table: default.t2
└──ReadFromMergeTree (default.t2)
Read type: Default
Parts: 1 | Granules: 1
Output: id, valueEXPLAIN PIPELINE
设置:
header— 为每个输出端口打印请求头。默认值:0。graph— 打印以 DOT 图描述语言表示的图。默认值:0。compact— 如果启用了graph设置,则以紧凑模式打印图。默认值:1。compact_repeated_processor_chains— 在文本输出中,通过显示一份事件链副本及其重复次数,将相邻的重复处理器事件链进行紧凑表示。当同一事件链多次出现时 (例如在 joins 中) ,这可以让并行管道更易于阅读。它不会影响图输出。默认值:0。
Resize 16 → 1
FillingRightJoinSide │
SimpleSquashingTransform │ × 16
Resize 1 → 16当 compact=0 且 graph=1 时,处理器名称会包含一个额外的后缀,用于标明处理器的唯一标识符。
示例:
EXPLAIN PIPELINE SELECT sum(number) FROM numbers_mt(100000) GROUP BY number % 4;(Union)
(Expression)
ExpressionTransform
(Expression)
ExpressionTransform
(Aggregating)
Resize 2 →EXPLAIN ANALYZE
EXPLAIN ANALYZE 会实际执行查询,丢弃结果行,并输出与 EXPLAIN PLAN 相同的计划树,同时为每个步骤标注其在运行时的实际执行情况。
设置:
EXPLAIN ANALYZE 接受与 EXPLAIN PLAN 相同的显示选项 (见 EXPLAIN PLAN 小节) 。
header— 见 EXPLAIN PLAN 小节。description— 见 EXPLAIN PLAN 小节。projections— 见 EXPLAIN PLAN 小节。sorting— 见 EXPLAIN PLAN 小节。input_headers— 见 EXPLAIN PLAN 小节。column_structure— 见 EXPLAIN PLAN 小节。actions— 见 EXPLAIN PLAN 小节。默认值:1。indexes— 见 EXPLAIN PLAN 小节。默认值:1。compact— 见 EXPLAIN PLAN 小节。默认值:1。pretty— 见 EXPLAIN PLAN 小节。默认值:1。processors— 对于EXPLAIN ANALYZE,会为每个阶段额外输出一行,显示各处理器耗时的分布:min、median、max和sum。这有助于发现并行处理器之间的负载不均。默认值:0。matches— 对于EXPLAIN ANALYZE,当无法从 join 产生的结果中推导出matched、match rate和fanout指标时,会使 join 步骤执行生成这些指标所需的额外记录工作。可以推导出时,无需此选项也会报告这些指标。请参见 Join steps。默认值:0。
示例:
EXPLAIN ANALYZE SELECT number % 10 AS k, count() FROM numbers_mt(1000000) GROUP BY k;Query summary:
Time: 10.72 ms (planning 6.45 ms · execution 4.26 ms)
Read: 1.00 million rows, 8.00 MB (234.49 million rows/s., 1.88 GB/s.)
Peak memory: 28.98 KiB
Output: number MOD 10, count()
Expression ((Project names + Projection))
│ I/O: rows 10 → 10 · 90 B → 90 B
│ time 21.82 us (0.5%) · parallelism 0.98/1
└──Aggregating
│ Keys: number MOD 10
│ Aggregates: count()
│ Skip merging: 0
│ I/O: rows 1.00 million → 10 (0.00%) · 1.00 MB → 90 B
│ Stage (partial aggregation): time 868.45 us (20.4%) · parallelism 3.80/15
│ Stage (final aggregation): time 445.27 us (10.4%) · parallelism 1.11/16
└──Expression ((Before GROUP BY + Change column names to column identifiers))
│ I/O: rows 1.00 million → 1.00 million · 8.00 MB → 1.00 MB
│ time 677.07 us (15.9%) · parallelism 4.31/15
└──ReadFromSystemNumbers
Output: number
I/O: rows 0 → 1.00 million · 0 B → 8.00 MB
time 993.94 us (23.3%) · parallelism 7.52/15我们来看看输出。首先看一下请求头。
Query summary:
Time: <total> (planning <planning> · execution <execution>)
Read: <rows> rows, <bytes> (<rows/s>, <bytes/s>)
Peak memory: <peak>Time— 总时间,分为规划阶段 (即创建计划 + 优化计划 + 构建管道) 和执行阶段 (运行管道) 。Read— 从表中读取的行数和未压缩字节数,以及吞吐量 —— 与普通查询 footer 中显示为 "Processed" 的数字相同。Peak memory— 查询使用的峰值内存占用。
现在我们来看看查询计划中新出现的这些行。
I/O: rows <in> → <out> (<selectivity>%) · <bytes_in> → <bytes_out>
[Stage (<stage>): ]time <t> (<share>%) · parallelism <avg>/<max>行数和字节数会在整个步骤范围内统一报告一次 (I/O 行) 。时间和并行度则会按步骤的各个阶段,在后续缩进行中分别报告。
rows <in> → <out>— 进入和离开该步骤的行数; (<selectivity>%) 表示该步骤对数据的过滤 (out/in) 或扩张程度;当输入行数等于输出行数,或输入行数为0时会隐藏。<bytes_in> → <bytes_out>— 流经该步骤的内存中未压缩字节数 (当两者都为零时省略) 。time <t> (<share>%)— 该阶段处于活跃状态的挂钟时间,以及其占查询执行时间的比例 (即不包含 build 时间) 。请注意,由于阶段和步骤会并发运行,这些占比相加可能超过 100%。parallelism <avg>/<max>— 该阶段内同时工作的 CPU 线程平均数量,以及它可使用的最大数量。数值接近最大值表示该阶段并行化良好;接近 1 则表示它大多是串行运行的。Stage (<stage>)— 阶段名称。只有单个阶段的步骤会直接打印时间行,而不带Stage (...)标签。包含多个阶段的步骤则会为每个阶段分别打印一行带标签的内容,例如Aggregating会显示Stage (partial aggregation)和Stage (final aggregation),而 hash 连接 会显示Stage (build)和Stage (probe)。
Join 步骤
对于 JOIN 步骤,EXPLAIN ANALYZE 会输出每一侧的参与行——Left 和 Right——后面跟着所有特定于该 JOIN 实现的行。Left 和 Right 对应 SQL 中的逻辑左右侧。在大多数情况下,Left 也是 JOIN 的探测侧,Right 则是 JOIN 的构建侧。不过,由于 JOIN 执行期间可能发生交换,情况并非总是如此。涵盖了 join_algorithm 的所有取值 (hash、parallel_hash、grace_hash、partial_merge、full_sorting_merge、parallel_full_sorting_merge、direct) ,以及该设置无法选择的两种实现:CROSS 或 COMMA JOIN、任何不含键相等条件的 ON 子句,以及 Join 表引擎。大多数实现都会报告两侧;有些仅报告其物化的一侧 (例如,direct 只输出 Left:) 。
各侧的行采用相同的形态:
Left: rows <left_rows> · matched <matched_left_rows> · match rate <match_rate>% · fanout <fanout>
Right: rows <right_rows> · matched <matched_right_rows> · match rate <match_rate>% · fanout <fanout>对于每一侧,EXPLAIN ANALYZE 会报告:
rows <rows>— 该侧经过 JOIN 的行总数。matched <matched_rows>— 该侧在另一侧找到至少一个 JOIN 匹配行的行数。这里统计的是行,而不是键:如果某个键在右侧出现三次且能够匹配,则这三行右侧行都会计为已匹配。match rate <match_rate>%— 该侧已匹配行的占比,计算方式为100 * <matched_rows> / <rows>。fanout <fanout>— 该侧平均每个已匹配行产生的输出行数。
无法精确推导的数值会报告为 not collected,而不是 0。match rate 和 fanout 都由 matched 推导而来,因此没有 matched 的一侧会将这三项均报告为 not collected。
扇出
fanout 用于衡量行数的倍增情况:
matched output rows = <output_rows> - <NULL-padded rows of both sides>
fanout = <matched output rows> / <matched_rows of that side>外JOIN会为保留侧中每个未找到匹配行的行输出一个以 NULL 填充的结果行。这些行会被扣除,以免拉低该比率。只有保留侧会出现此类行——RIGHT 和 FULL 为右侧,LEFT 和 FULL 为左侧:
fanout = 0— 匹配行完全没有产生结果行,这正是ANTIJOIN的行为:它只输出未找到匹配行的行。fanout = 1— 完整的 1:1 JOIN;每个匹配行恰好产生一个结果行。fanout > 1— 1:N JOIN;另一侧的重复键导致行数倍增。两侧同时出现较大值,表明发生了非预期的 Cartesian 膨胀。
何时需要 matches = 1 才能获取数值
这些数值大多来自 JOIN 本身就会生成的数据,普通的 EXPLAIN ANALYZE 即会报告。其余数值需要执行 JOIN 原本不会进行的额外记录操作,因此只有在使用 EXPLAIN ANALYZE matches = 1 时才会报告。具体哪些数值属于这种情况取决于算法;对于哈希家族,有以下两种情况:
ALL INNER和ALL LEFT的右侧,因为需要标记每个匹配的右侧行;ALL LEFT和ALL FULL的左侧,但仅限于查询未从 右表选择任何内容,且ON子句只是简单的键相等条件时。否则,探测过程已经记录了 哪些左侧行匹配——无论是为了物化右侧列,还是为了评估剩余 条件——因此即使不使用该选项,计数也完全准确。
出于相同原因,partial_merge 对四种 ALL 类型的右侧需要此选项。
full_sorting_merge 和 parallel_full_sorting_merge 对 ANY 类型的两侧需要此选项。ALL
类型则无需额外操作。
matches = 1 并不能让所有组合都可收集。JOIN 能报告哪一侧,取决于该 JOIN 本身必须执行哪些操作,因此既取决于算法,也取决于类型和严格性。
哈希家族。 hash、parallel_hash 和 grace_hash 的行为始终一致:
| Join | 匹配的左侧 | 匹配的右侧 |
|---|---|---|
ALL INNER, ALL LEFT, ALL RIGHT, ALL FULL |
是 | 是 |
SEMI LEFT, ANTI LEFT |
是 | 否 |
ANY RIGHT, ANTI RIGHT |
否 | 是 |
ASOF (inner) |
是 | 否 |
SEMI RIGHT |
否 | 否 |
ANY INNER, ANY LEFT, ASOF LEFT |
否 | 否 |
当 JOIN 在哈希表中每个键只保留一行时,右侧便不可收集;ANY、SEMI 和 ANTI JOIN 正是如此:重复的右侧行从不存储,因此无法计数。当 JOIN 会抑制输出其匹配行已被另一个左侧行占用的左侧行时,左侧便不可收集,因为输出的行数会少于实际匹配的行数。
启用 any_join_distinct_right_table_keys 会将 ANY 切换为旧版 RightAny 语义,即每个左侧行输出一行,因此可保留两个计数。此时,ANY RIGHT 和 ANY FULL 会报告两侧,而 ANY INNER 会被重写为 SEMI LEFT。
Join 表引擎遵循相同的表格规则,并使用引擎中声明的类型和严格性:Join(ALL, INNER, …) 报告两侧,Join(ANY, LEFT, …) 则两侧均不报告。
合并算法。 full_sorting_merge 和 parallel_full_sorting_merge 支持四种 ALL 类型、ANY INNER、ANY LEFT、ANY RIGHT、ASOF 和 ASOF LEFT。除 ASOF 和 ASOF LEFT 外,它们会为每种类型报告两侧;对于后两者,右侧为 not collected,且无需 matches = 1——它们会遍历两个已排序的输入,并在处理相等范围时看到其中的每一行,因此之后无需重建任何信息。
partial_merge 支持 ALL INNER、ALL LEFT、ALL RIGHT、ALL FULL、ANY INNER、ANY LEFT 和 SEMI LEFT。它会为四种 ALL 类型报告两侧,其中右侧需要 matches = 1;对于 ANY INNER、ANY LEFT 和 SEMI LEFT,右侧为 not collected。
direct。 仅报告左侧。右侧是从不物化为行的键值存储,因此根本没有 Right: 行。
CROSS、COMMA 和常量 ON。 两侧均不报告,如上文所述。
当两种算法都报告数值时,结果是一致的。合并算法只是掌握了更多信息;对于何谓匹配,它们并无分歧。
算法特定行
下面来看各 JOIN 实现额外添加的行。
对于 hash 和 parallel_hash JOIN 以及 Join 表引擎,Hash table: 行描述了根据右表构建的哈希表:
Hash table: unique keys <unique_keys> · memory <peak_memory>unique keys <unique_keys>— 构建阶段哈希表中存储的唯一键数量。memory <peak_memory>— 构建阶段哈希表的峰值内存占用。
对于 grace_hash JOIN,Hash table: 行还会说明该 JOIN 如何适应内存限制,Spill: 行则会说明数据是否已落盘:
Hash table: unique keys <unique_keys> · memory <peak_memory> · buckets <buckets> · rehashes <rehashes>
Spill: yes · left spilled <left_spilled_bytes> · right spilled <right_spilled_bytes>buckets <buckets>— grace hash JOIN 执行结束时的最终桶数量。该值始终是2的幂。rehashes <rehashes>— 为满足内存限制而将桶数量翻倍的次数。Spill:— 一个yes/no标志,表示是否发生了落盘。若发生落盘,left spilled <left_spilled_bytes>和right spilled <right_spilled_bytes>分别报告左侧 (probe) 和右侧 (build) 落盘的压缩字节数;若未发生落盘,该行即为Spill: no。
对于 partial_merge JOIN,Right: 行会提供右表缓冲和排序方式的额外信息,排序时间显示在 Stage (build) 和 Stage (probe) 行中:
Right: rows <right_rows> · matched <matched_right_rows> · size <right_size> · blocks <right_blocks> · storage <in-memory|external> · match rate <match_rate>% · fanout <fanout>
Stage (build): time <t> (<share>%) · parallelism <avg>/<max> · sort time <build_sort_time> · sort share <build_sort_share>%
Stage (probe): time <t> (<share>%) · parallelism <avg>/<max> · sort time <probe_sort_time> · sort share <probe_sort_share>%size <right_size>— 右表各块占用的内存大小。blocks <right_blocks>— 右表被缓冲为的块数。storage <in-memory|external>— 右表是能够完全放入内存 (in-memory) ,还是必须落盘 (external) 。当值为external时,还会通过spilled <spilled_bytes>报告写入磁盘的压缩字节数。sort time <sort_time>— 对右表排序 (在构建阶段) 以及对每个传入的左块排序 (在探测阶段) 所花费的时间。sort share <sort_share>%—sort time占该阶段自身忙碌时间 (其处理器耗时之和) 的比例;不同于阶段time的百分比,后者占整个查询执行时间的比例。
对于 full_sorting_merge JOIN,仅输出通用的 Left: 和 Right: 行。
对于 direct JOIN,仅输出 Left: 行,因为右侧是直接查找的键值存储,而非物化为行。
对于 CROSS 或 COMMA JOIN,以及任何不含键相等条件的 ON 部分,Buffer: 行会说明右表如何保存在内存中,Spill: 行则报告其是否落盘:
Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>memory <peak_memory>— 缓冲右表占用的峰值内存。compressed <yes|no>— 是否至少有一个缓冲块经过压缩;读取器随后会解压所有已存储的块。Spill:— 与grace_hash相同的yes/no标志;right spilled <right_spilled_bytes>则报告落盘的压缩字节数。
此处两侧均显示 matched not collected:常量谓词要么让每个左行与每个右行匹配,要么完全不匹配,因此无法确定具体哪些行发生了匹配。
对于与 Join 表引擎进行的 JOIN,会同时报告两侧的信息,并附带描述预构建表的 Hash table: 行。右侧统计的是存储在引擎中的行数,而非某次查询中构建的行数。
每个处理器的耗时
当 processors = 1 时,会在每个阶段下方额外打印一行,显示该阶段各处理器的耗时分布:
Time per processor (<n>): min <t> · median <t> · max <t> · sum <t><n> 是该阶段中的处理器数量。median 和 max 之间差距较大,说明并行处理器之间存在负载不均。
EXPLAIN ESTIMATE
显示在处理查询时,将从表中读取的预估行数、标记数和 parts 数量。适用于 MergeTree 家族的表。
示例
创建表:
CREATE TABLE ttt (i Int64) ENGINE = MergeTree() ORDER BY i SETTINGS index_granularity = 16, write_final_mark = 0;
INSERT INTO ttt SELECT number FROM numbers(128);
OPTIMIZE TABLE ttt;EXPLAIN ESTIMATE SELECT * FROM ttt;┌─database─┬─table─┬─parts─┬─rows─┬─marks─┐
│ default │ ttt │ 1 │ 128 │ 8 │
└──────────┴───────┴───────┴──────┴───────┘EXPLAIN WHATIF
在不将索引物化到磁盘上的情况下,估算假设性跳过索引对 SELECT 查询的潜在收益。使用 CREATE HYPOTHETICAL INDEX 定义一个或多个候选项,然后运行 EXPLAIN WHATIF SELECT ...,即可查看每个候选项的以下信息:是否适用、预估读取的标记数、预估字节数以及跳过比率。
语法
EXPLAIN WHATIF [empirical = 0] SELECT ...设置
empirical—1(默认值) 会在内存中对经过基线剪枝的粒度运行索引,以测量跳过比率 (上限) 。0则会跳过该路径。无论哪种情况,如果 empirical 未产生结果 (已禁用,或索引无法在内存中求值) ,估算器都会回退到列统计信息;如果两者都不可用,则最终回退到仅包含适用性的摘要。
输出
Baseline (after PK + partition + existing indexes):
table: db.t
parts: 1
marks: 100
est_bytes: 1.50 MiB (only when the query reads rows)
With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 15.00 KiB (only when baseline bytes are known)
skip_ratio: 99.0%
Estimation:
source: empirical | statistical | applicability_only
empirical_status: ok | unsupported | disabled
empirical_reason: <reason> (only when empirical_status = unsupported)
sampled_parts: 50 / 100 (only when source = empirical)
sampled_marks: 50 / 100 (only when source = empirical)
elapsed_us: 631 (only when source = empirical)source— 估算的来源。empirical:在内存中基于经过基线剪枝后的粒度构建索引,并统计该索引原本可以跳过的粒度数。这是一个上界——请参阅CREATE HYPOTHETICAL INDEX中的限制。statistical:根据列统计信息推导得出。当经验估算被禁用 (empirical = 0) ,或经验估算无法得出结果,且相关列已定义列统计信息时使用。applicability_only:该索引适用于该谓词,但经验估算和统计估算都未得出结果 (例如empirical = 0且未定义列统计信息) 。作为保守上界,会报告skip_ratio: 0.0%。
empirical_reason— 经验估算无法运行的原因。仅在empirical_status: unsupported时显示。例如,非零的merge_tree_min_rows_for_seek或merge_tree_min_bytes_for_seek会使实际读取合并标记范围,而按粒度计数未对此建模,因此估算会回退到statistical或applicability_only。sampled_parts/sampled_marks—<baseline-pruned> / <total in the table>。显示在经过 PK、分区和现有索引剪枝后,表中有多少比例的数据被保留下来,即假设索引的输入。est_bytes— 读取字节数的估算值,由表的平均行大小推导而来,因此只是近似值,并会随存储和压缩情况而变化。只有当查询读取行时才会显示 baseline 行;只有在已知 baseline 字节估算值时,才会显示各候选项对应的行。
该设置以内联方式写在 WHATIF 与 SELECT 之间——没有 SETTINGS 关键字 (这与其他 EXPLAIN 变体接受选项的方式一致) 。
如果该表没有定义任何假设索引,EXPLAIN WHATIF 会报告 status: not_applicable,并提示你创建一个。
组合行 (多个候选项)
当以经验方式评估两个或更多候选项时,EXPLAIN WHATIF 会在各候选项对应的行后追加一个名为 (combined: idx_a, idx_b, ...) 的额外块。它报告的是同时拥有所有这些索引时的联合收益:实际读取只有在某个粒度通过了每个跳过索引时才会保留该粒度,因此组合估算就是各候选项保留下来的粒度的交集。因此,它的 skip_ratio 至少与表现最佳的单个候选项一样高——互补的索引一起会剪枝掉更多数据,而冗余的索引则不会带来变化。
只有 source: empirical 的候选项会参与计算,因为组合行是通过对它们各自按粒度划分的存活集合求交集构建出来的。估算为 statistical 或 applicability_only 的候选项没有按粒度划分的数据,因此会被排除;因此,只有当至少两个候选项生成了经验估算时,组合块才会出现,否则将被省略 (例如在 empirical = 0 时) 。它的估算字段与单个候选项的经验块相同,但 elapsed_us 为 0——组合估算是根据各候选项的扫描结果推导得出的,而不是一次新的扫描。合成的 (combined: ...) 名称仅用作报告标签,不能与 force_data_skipping_indices 一起使用。
经验示例
CREATE TABLE t (a UInt64, b UInt64) ENGINE = MergeTree ORDER BY a
SETTINGS index_granularity = 100;
INSERT INTO t SELECT number, number FROM numbers(10000);
CREATE HYPOTHETICAL INDEX idx_b ON t (b) TYPE minmax GRANULARITY 1;
EXPLAIN WHATIF SELECT * FROM t WHERE b = 42;Baseline (after PK + partition + existing indexes):
table: default.t
parts: 1
marks: 100
est_bytes: 85.52 KiB
With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 875.00 B
skip_ratio: 99.0%
Estimation:
source: empirical
empirical_status: ok
sampled_parts: 1 / 1
sampled_marks: 100 / 100假设使用 minmax,可将 100 个标记裁减到只剩 1 个——skip_ratio: 99.0%。 (est_bytes 是根据平均行大小估算得出的,因此具体数值会有所不同。)
统计示例
列统计信息默认处于关闭状态。要触发 statistical 路径,请先在相关列上定义统计信息,然后等待物化变更完成:
ALTER TABLE t ADD STATISTICS b TYPE tdigest;
ALTER TABLE t MATERIALIZE STATISTICS b SETTINGS mutations_sync = 1;然后禁用经验路径,使估算器回退到列统计信息:
EXPLAIN WHATIF empirical = 0 SELECT * FROM t WHERE b < 10;With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 1.66 KiB
skip_ratio: 99.9%
Estimation:
source: statistical
empirical_status: disabled该数值来自 b < 10 的列统计信息选择性 (10000 行中约 10 行) ,并以 skip_ratio 上界的形式给出。不存在 sampled_parts / sampled_marks——未读取任何数据。
如果这两种路径都不可用 (例如 empirical = 0 且未定义列统计信息) ,估算器会报告 source: applicability_only,以及保守的 skip_ratio: 0.0%。
EXPLAIN TABLE OVERRIDE
显示通过 table function 访问的表 schema 上应用表覆盖后的结果。 还会进行一些验证;如果该覆盖会导致某种失败,则会抛出异常。
示例
假设你有一个如下所示的远程 MySQL 表:
CREATE TABLE db.tbl (
id INT PRIMARY KEY,
created DATETIME DEFAULT now()
)EXPLAIN TABLE OVERRIDE mysql('127.0.0.1:3306', 'db', 'tbl', 'root', 'clickhouse')
PARTITION BY toYYYYMM(assumeNotNull(created))┌─explain─────────────────────────────────────────────────┐
│ PARTITION BY uses columns: `created` Nullable(DateTime) │
└─────────────────────────────────────────────────────────┘