Показывает план выполнения оператора SQL.
Синтаксис:
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— Оценочное количество строк, меток и частей, которые будут прочитаны из таблиц при обработке запроса.TABLE OVERRIDE— Провалидированный результат переопределения таблицы в схеме табличной функции.
EXPLAIN AST
Выводит AST запроса. Поддерживает все типы запросов, а не только SELECT.
Настройки:
graph– Выводит AST в виде графа, описанного на языке описания графов DOT. По умолчанию: 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для каждого запроса), либо установите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— Показывает используемые индексы, количество отфильтрованных частей и количество отфильтрованных гранул для каждого применённого индекса. Значение по умолчанию: 0. Поддерживается для таблиц MergeTree. Начиная с ClickHouse >= v25.9, этот оператор показывает осмысленный результат только при использовании сSETTINGS use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0.projections— Показывает все проанализированные проекции и их влияние на фильтрацию на уровне частей на основе условий по первичному ключу проекции. Для каждой проекции в этом разделе приводится статистика, включая количество частей, строк, меток и диапазонов, оценённых с использованием первичного ключа проекции. Также показывается, сколько частей данных было пропущено благодаря этой фильтрации без чтения из самой проекции. Была ли проекция действительно использована для чтения или только проанализирована для фильтрации, можно определить по полюdescription. Значение по умолчанию: 0. Поддерживается для таблиц MergeTree.actions— Выводит подробную информацию о действиях шага. Значение по умолчанию: 1.sorting— Выводит описание сортировки для каждого шага плана, который формирует отсортированный вывод. Значение по умолчанию: 0.keep_logical_steps— Сохраняет логические шаги плана для JOIN вместо преобразования их в физические реализации JOIN. Значение по умолчанию: 0.json— Выводит шаги плана запроса как строку в формате JSON. Значение по умолчанию: 0. Чтобы избежать лишнего экранирования, рекомендуется использовать формат TabSeparatedRaw (TSVRaw).input_headers— Выводит входные заголовки для шага. Значение по умолчанию: 0. В основном полезно только разработчикам для отладки проблем, связанных с несоответствием входных и выходных заголовков.column_structure— Также выводит структуру столбцов в заголовках помимо их имени и типа. Значение по умолчанию: 0. В основном полезно только разработчикам для отладки проблем, связанных с несоответствием входных и выходных заголовков.distributed— Показывает планы запроса, выполняемые на удалённых узлах для 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— количество частей после/до применения индекса.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. Он содержит массив проанализированных проекций. Каждая проекция описывается в формате JSON со следующими ключами:
Name— Имя проекции.Condition— Используемое условие по первичному ключу проекции.Description— Описание того, как используется проекция (например, для фильтрации на уровне частей).Selected Parts— Количество частей, выбранных проекцией.Selected Marks— Количество выбранных меток.Selected Ranges— Количество выбранных диапазонов.Selected Rows— Количество выбранных строк.Filtered 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 вывод включает не только локальный план запроса, но и планы запросов, которые будут выполняться на удалённых узлах. Это полезно для анализа и отладки распределённых запросов.
Пример с distributed таблицей:
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-нотации. Если присутствуют runtime-фильтры JOIN, они показываются отдельно.
- Шаги агрегации отображают ключи и агрегатные функции с их аргументами (например,
sum(c),count()). - Множества
IN, заданные кортежными литералами, показывают свои значения (усечённые для больших множеств), множества на основе подзапросов помечаются какsubquery1,subquery2и т. д., а множества из таблиц с движкомSetпоказывают имя таблицы. - Шаги JOIN отображают отношение JOIN в математической нотации, оценочное количество строк в результате, а также то, какие выходные столбцы поступают с левой, а какие — с правой стороны. Для представления различных типов JOIN используются следующие символы:
| Символ | Тип JOIN |
|---|---|
⋈ |
Inner JOIN |
⟕ |
Left JOIN |
⟖ |
Right JOIN |
⟗ |
Full JOIN |
⋉ |
Left Semi JOIN |
⋊ |
Right Semi JOIN |
⋉ with strikethrough |
Left Anti JOIN |
⋊ with strikethrough |
Right Anti JOIN |
× |
Cross JOIN |
Например, t1 ⟕ t2 означает Left JOIN между таблицами 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— Объединяет соседние повторяющиеся цепочки процессоров в текстовом выводе, показывая одну копию цепочки с числом повторений. Это может упростить чтение параллельных конвейеров, когда одна и та же цепочка встречается много раз, например при JOIN. На вывод графа это не влияет. По умолчанию: 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. По умолчанию: 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— строки и несжатые байты, прочитанные из таблиц, с указанием пропускной способности — те же числа, которые нижний колонтитул обычного запроса показывает как "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>%)— фактическое время, в течение которого стадия была активна, и её доля от времени выполнения запроса (то есть без времени сборки). Обратите внимание: сумма долей может превышать 100%, потому что стадии и шаги выполняются параллельно.parallelism <avg>/<max>— среднее число потоков CPU, одновременно работающих в пределах этой стадии, из максимально возможного числа. Значение, близкое к максимуму, означает, что стадия хорошо распараллелена; близкое к 1 — что она выполнялась в основном последовательно.Stage (<stage>)— имя стадии. Для шага с одной стадией строка времени выводится сразу, без меткиStage (...). Для шагов с несколькими стадиями выводится по одной помеченной строке на каждую стадию; например, дляAggregatingпоказываютсяStage (partial aggregation)иStage (final aggregation), а для hash JOIN —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), а также две реализации, которые нельзя выбрать этой настройкой: JOIN типа 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, поэтому для стороны, у которой оно отсутствует, все три значения указываются как not collected.
Разветвление
fanout показывает кратность строк:
matched output rows = <output_rows> - <NULL-padded rows of both sides>
fanout = <matched output rows> / <matched_rows of that side>Внешнее соединение выводит по одной строке результата, дополненной значениями NULL, для каждой строки сохраняемой стороны, не нашедшей пары. Эти строки вычитаются, чтобы не искажать соотношение. Они есть только у сохраняемой стороны: у правой для RIGHT и FULL, у левой для LEFT и FULL:
fanout = 0— сопоставленные строки вообще не дали строк результата, как и приANTIJOIN: он выводит только строки, не нашедшие пары.fanout = 1— корректное соединение 1:1; каждая сопоставленная строка дала ровно одну строку результата.fanout > 1— соединение 1:N; дублирующиеся ключи на другой стороне размножили строки. Большое значение сразу с обеих сторон — признак непреднамеренного разрастания декартова произведения.
Когда для подсчёта требуется 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 | matched слева |
matched справа |
|---|---|---|
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 хранит в своей хеш-таблице только одну строку на ключ, как это делают JOIN ANY, SEMI и ANTI: дублирующиеся правые строки никогда не сохраняются, поэтому их невозможно подсчитать. Левая сторона недоступна, когда 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.
Для JOIN hash и parallel_hash, а также для движка таблицы Join строка Hash table: описывает хеш-таблицу, построенную на основе правой таблицы:
Hash table: unique keys <unique_keys> · memory <peak_memory>unique keys <unique_keys>— количество уникальных ключей, хранящихся в хеш-таблице на этапе построения.memory <peak_memory>— пиковое потребление памяти хеш-таблицей на этапе построения.
Для JOIN grace_hash строка 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>показывают объём сжатых данных, записанных на диск с левой (проверки) и правой (build) сторон; если ничего не записывалось на диск, строка имеет видSpill: no.
Для JOIN partial_merge строка Right: содержит дополнительную информацию о буферизации и сортировке правой таблицы, а время сортировки указано в строках Stage (build) и Stage (проверки):
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>— время, затраченное на сортировку правой таблицы (на стадии build) и каждого входящего левого блока (на стадии проверки).sort share <sort_share>%— доляsort timeв собственном времени занятости этой стадии (сумме времени выполнения её процессоров), в отличие от процентаtimeстадии, который представляет собой долю общего времени выполнения запроса.
Для JOIN full_sorting_merge выводятся только общие строки Left: и Right:.
Для JOIN direct выводится только строка Left:, поскольку правая сторона представляет собой хранилище ключ-значение, поиск в котором выполняется напрямую, а не материализуется в строки.
Для JOIN CROSS или COMMA, а также для любого раздела ON без равенства ключей строка Buffer: описывает, как правая таблица хранилась в памяти, а строка Spill: сообщает, была ли она записана на диск:
Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>memory <peak_memory>— пиковое потребление памяти буферизованной правой таблицей.compressed <yes|no>— был ли сжат хотя бы один буферизованный блок; в этом случае reader распаковывает каждый сохранённый блок.Spill:— тот же флагyes/no, что и дляgrace_hash;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
Показывает оценочное количество строк, меток и частей, которые будут прочитаны из таблиц при обработке запроса. Работает с таблицами семейства 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: индекс строится в памяти по гранулам, оставшимся после базового pruning, и подсчитывается, сколько гранул индекс мог бы пропустить. Это верхняя граница — см. ограничения вCREATE HYPOTHETICAL INDEX.statistical: вычисляется на основе статистики столбцов. Используется, когда empirical отключён (empirical = 0) или empirical не смог дать результат, а для соответствующих столбцов задана статистика.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>. Показывает, какая доля таблицы осталась после pruning по PK, партициям и существующим индексам, то есть какие данные поступают на вход гипотетическому индексу.est_bytes— оценка количества прочитанных байтов, полученная на основе среднего размера строки в таблице, поэтому она приблизительна и зависит от хранилища и сжатия. Строка baseline появляется только тогда, когда запрос читает строки; строка для каждого кандидата — только когда известна базовая оценка объёма в байтах.
Настройка записывается inline между 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, сначала задайте её для нужных столбцов и дождитесь завершения мутации materialize:
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 (примерно 10 строк из 10000) и приводится как верхняя граница для skip_ratio. Значения sampled_parts / sampled_marks отсутствуют — данные не считывались.
Если ни один из вариантов недоступен (например, empirical = 0 и статистика столбцов не определена), оценщик возвращает source: applicability_only и консервативное значение skip_ratio: 0.0%.
EXPLAIN TABLE OVERRIDE
Показывает результат переопределения таблицы в схеме таблицы, к которой обращаются через табличную функцию. Также выполняет проверку и генерирует исключение, если такое переопределение привело бы к какой-либо ошибке.
Пример
Предположим, у вас есть удалённая таблица 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) │
└─────────────────────────────────────────────────────────┘