Skip to main content
Показывает план выполнения оператора SQL.
Синтаксис:
Пример:

Типы EXPLAIN

  • AST — Абстрактное синтаксическое дерево.
  • SYNTAX — Текст запроса после оптимизаций на уровне AST.
  • QUERY TREE — Дерево запроса после оптимизаций на уровне дерева запроса.
  • PLAN — План выполнения запроса.
  • PIPELINE — Конвейер выполнения запроса.
  • ANALYZE — Выполняет запрос и дополняет план выполнения измеренными метриками времени выполнения.
  • ESTIMATE — Оценочное количество строк, меток и частей, которые будут прочитаны из таблиц при обработке запроса.
  • TABLE OVERRIDE — Провалидированный результат переопределения таблицы в схеме табличной функции.

EXPLAIN AST

Выводит AST запроса. Поддерживает все типы запросов, а не только SELECT. Настройки:
  • graph – Выводит AST в виде графа, описанного на языке описания графов DOT. По умолчанию: 0.
Примеры:

EXPLAIN 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.
Примеры:
Query
Response
С параметром run_query_tree_passes:
Query
Response

EXPLAIN QUERY TREE

Настройки:
  • run_passes — Выполнить все проходы по дереву запроса перед его выводом. По умолчанию: 1.
  • dump_passes — Вывести информацию об использованных проходах по дереву запроса перед выводом дерева запроса. По умолчанию: 0.
  • passes — Указывает, сколько проходов по дереву запроса выполнить. Если задано значение -1, выполняются все проходы по дереву запроса. По умолчанию: -1.
  • dump_tree — Показать дерево запроса. По умолчанию: 1.
  • dump_ast — Показать AST запроса, сгенерированное из дерева запроса. По умолчанию: 0.
Пример:

EXPLAIN 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_query_plan_default = 'pretty', поэтому actions, compact и pretty инициализируются значением 1, а план отображается в компактном, наглядном виде с аннотациями действий. Явное указание любого из этих параметров в операторе EXPLAIN (например, EXPLAIN actions = 0, compact = 0, pretty = 0 SELECT ...) всегда переопределяет значение по умолчанию.До ClickHouse 26.7 значениями по умолчанию для actions, compact и pretty были 0. Этот вывод по-прежнему можно получить, установив explain_query_plan_default = 'legacy' (глобально или в SETTINGS для отдельного запроса) либо задав compatibility любой версии старше 26.7.Параметры json и distributed не включают значения по умолчанию для pretty (actions, compact и pretty), даже когда explain_query_plan_default = 'pretty'. Чтобы включить подробности о действиях в их вывод, вручную задайте actions = 1.
Пример:
Оценка стоимости шагов и запроса не поддерживается.
При json = 1 план запроса представляется в формате JSON. Каждый узел — это словарь, который всегда содержит ключи Node Type, Node Id и Plans. Node Type — строка с именем шага, а Node Id — уникальный идентификатор шага (имя шага с числовым суффиксом, например Union_10). Plans — массив с описаниями дочерних шагов. В зависимости от типа узла и настроек могут добавляться и другие необязательные ключи. Пример:
При description = 1 в шаг добавляется ключ Description:
При header = 1 в шаг добавляется ключ Header в виде массива столбцов. Пример:
При indexes = 1 добавляется ключ Indexes. Он содержит массив использованных индексов. Каждый индекс описывается в формате JSON с ключом Type (строка Partition Min-Max, Partition, Statistics, PrimaryKey или Skip) и следующими необязательными ключами: Индекс Statistics использует статистику столбцов по каждой части (минимальные/максимальные значения, а также количество значений NULL для столбцов Nullable), чтобы пропускать части, которые не могут соответствовать фильтру запроса.
  • Name — имя индекса (в настоящее время используется только для индексов Skip).
  • Keys — массив столбцов, используемых индексом.
  • Condition — используемое условие.
  • Description — описание индекса (в настоящее время используется только для индексов Skip).
  • Parts — количество частей после/до применения индекса.
  • Granules — количество гранул после/до применения индекса.
  • Ranges — количество диапазонов гранул после применения индекса.
Пример:
При projections = 1 добавляется ключ Projections. Он содержит массив проанализированных проекций. Каждая проекция описывается в формате JSON со следующими ключами:
  • Name — Имя проекции.
  • Condition — Используемое условие по первичному ключу проекции.
  • Description — Описание того, как используется проекция (например, для фильтрации на уровне частей).
  • Selected Parts — Количество частей, выбранных проекцией.
  • Selected Marks — Количество выбранных меток.
  • Selected Ranges — Количество выбранных диапазонов.
  • Selected Rows — Количество выбранных строк.
  • Filtered Parts — Количество частей, пропущенных из-за фильтрации на уровне частей.
Пример:
При actions = 1 добавляемые ключи зависят от типа шага. Пример:
При compact = 0 и actions = 1 отображаются шаги Expression вместе с подробной информацией о выражениях:
При distributed = 1 вывод включает не только локальный план запроса, но и планы запросов, которые будут выполняться на удалённых узлах. Это полезно для анализа и отладки распределённых запросов.
distributed отображается только в формате legacy (без pretty), поскольку вывод pretty не встраивает планы удалённых сегментов в дерево плана. По этой причине включение distributed автоматически отключает связанные с pretty значения по умолчанию (actions, compact и pretty) независимо от explain_query_plan_default. Вы по-прежнему можете задать actions=1 вручную. Параметр distributed также не поддерживается вместе с json.
Пример с distributed таблицей:
Пример с параллельными репликами:
В обоих примерах план запроса отображает полный поток выполнения, включая локальные и удалённые этапы. При 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 используются следующие символы:
Например, t1 ⟕ t2 означает левый JOIN между таблицами t1 и t2. Число в квадратных скобках после имени таблицы (например, t1[100]) указывает оценочное количество строк, если доступна статистика таблицы. Под отношением JOIN каждый шаг JOIN выводит оценки, сформированные для него оптимизатором порядка JOIN:
  • Cost — стоимость всего поддерева JOIN под этим шагом; именно это значение оптимизатор минимизирует при сравнении возможных порядков JOIN. Стоимость JOIN — это оценочное число совпавших пар строк, <selectivity> * <left_rows> * <right_rows>, плюс стоимость его входных данных.
  • Selectivity — оценочная доля декартова произведения двух сторон, которая проходит условие JOIN. Она выводится из количества уникальных значений (NDV) ключей JOIN: равенство ключей сохраняет примерно 1 / max(NDV_left, NDV_right) пар, и используется наименьшая доля среди условий JOIN.
  • Output rows — оценочное число строк, создаваемых JOIN: <selectivity> * <left_rows> * <right_rows> для внутреннего JOIN, не менее <left_rows> для LEFT, <right_rows> для RIGHT и <left_rows> + <right_rows> для FULL, поскольку внешний JOIN сохраняет каждую строку сохраняемой стороны. Когда SEMI или ANTI JOIN участвует в переупорядочивании, его оценка представляет собой долю от сохраняемой стороны: <preserved_rows> * min(1, <selectivity> * <other_rows>) для SEMI и оставшиеся строки для ANTI; в противном случае используется формула, соответствующая типу JOIN.
  • Left / Right — оценочное число строк, поступающих в JOIN с каждой стороны.
Значение, которое оптимизатор не смог оценить, обозначается как no stats. Это происходит, когда оптимизация порядка JOIN не выполнялась — например, когда query_plan_optimize_join_order_limit равен 0 — или когда нет оснований для оценки. Строки Input (left): и Input (right): перечисляют столбцы, которые каждая сторона передаёт в JOIN. Параметр pretty хорошо работает вместе с compact = 1, который скрывает шаги Expression и подробную информацию о действиях, упрощая чтение плана. Подробный пример с JOIN. Параметр join_runtime_filter_min_probe_rows понижен только для того, чтобы даже такая небольшая таблица создавала runtime-фильтр JOIN:

EXPLAIN PIPELINE

Настройки:
  • header — Выводит заголовок для каждого выходного порта. По умолчанию: 0.
  • graph — Выводит граф, описанный на языке описания графов DOT. По умолчанию: 0.
  • compact — Выводит граф в компактном режиме, если включена настройка graph. По умолчанию: 1.
  • compact_repeated_processor_chains — Объединяет соседние повторяющиеся цепочки процессоров в текстовом выводе, показывая одну копию цепочки с числом повторений. Это может упростить чтение параллельных конвейеров, когда одна и та же цепочка встречается много раз, например при JOIN. На вывод графа это не влияет. По умолчанию: 0.
Если compact=0 и graph=1, имена процессоров будут содержать дополнительный суффикс с уникальным идентификатором процессора. Пример:

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.
  • time — для EXPLAIN ANALYZE выводит для каждого шага строки Time и Concurrency с фактическим (астрономическим) временем выполнения и уровнем параллелизма шага и его ветви. См. Фактическое время выполнения и уровни параллелизма шагов и ветвей. По умолчанию: 1.
Поскольку EXPLAIN ANALYZE действительно выполняет обёрнутый запрос, он ведёт себя как этот запрос — и, в отличие от форм EXPLAIN, которые не выполняют запрос, — в нескольких отношениях:
  • Квоты и ограничения. Он учитывается в тех же квоты и подпадает под те же ограничения (например, query_selects, read_rows), что и при прямом выполнении запроса. Источники, освобождённые от квот на этапе планирования (такие как system.one), не учитываются.
  • Неуспешные транзакции. Внутри transaction, которая уже завершилась ошибкой (ROLLED_BACK), он отклоняется с INVALID_TRANSACTION, так же как и обычный SELECT — сначала выполните ROLLBACK.
  • Потоковые чтения. При потоковом чтении (FROM ... STREAM) он отклоняется с NOT_IMPLEMENTED, потому что такое чтение никогда не завершается.
  • Распределённые запросы. Он не поддерживается для запросов, выполняемых в режиме distributed.
Пример:
Давайте разберём вывод. Сначала посмотрим на заголовок.
  • Time — общее время, разделённое на этапы планирования (то есть создание плана + оптимизация плана + построение конвейера) и выполнения (запуск конвейера).
  • Execution — не выводится при time = 0. Разбивает время выполнения на in steps, outside steps и idle. В сумме эти части дают execution. См. Фактическое время выполнения шагов и ветвей и уровни параллелизма.
  • Read — строки и несжатые байты, прочитанные из таблиц, с указанием пропускной способности — те же числа, которые нижний колонтитул обычного запроса показывает как “Processed”.
  • Peak memory — пиковое потребление памяти запросом.
Теперь рассмотрим новые строки, которые появляются в плане запроса.
Строки и байты указываются один раз для всего шага (строка 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).
ClickHouse распараллеливает не только выполнение задач внутри шага плана, но и выполнение самих шагов плана. Метрика parallelism отражает только работу этого шага. Другие шаги могут выполняться параллельно, поэтому это число не показывает, как параллелизм шага соотносится со всем запросом. Чтобы оценить параллелизм всего запроса, смотрите метрику Concurrency, выводимую при time = 1 (Фактическое время и уровни параллелизма шагов и ветвей).
Максимальное число в parallelism вычисляется как минимум из:
  1. общего числа задач внутри шага плана;
  2. максимального числа потоков обработки запроса, заданного в max_threads.

Шаги JOIN

Для шага JOIN EXPLAIN ANALYZE выводит строки, сопоставляющие оценки оптимизатора порядка JOIN с тем, что произошло в действительности (см. Оценочные и фактические метрики JOIN), а также строки участия для каждой стороны — Left и Right, — за которыми следуют строки, относящиеся к конкретной реализации JOIN. Left и Right соответствуют логическим сторонам в SQL. В большинстве случаев Left также является стороной проверки JOIN, а Right — стороной построения (build). Однако так бывает не всегда, поскольку во время выполнения JOIN стороны могут меняться местами. Охвачены все значения join_algorithm (hash, parallel_hash, grace_hash, partial_merge, full_sorting_merge, parallel_full_sorting_merge, direct), а также три реализации, которые нельзя выбрать этой настройкой: JOIN типа CROSS или COMMA, блочный JOIN с вложенными циклами, используемый для секции ON, которая не определяет ключ JOIN, и движок таблицы Join. Большинство из них выводят сведения об обеих сторонах; некоторые — только о стороне, которую материализуют (например, direct выводит только Left:). Строки для каждой стороны имеют одинаковую структуру:
Для каждой стороны EXPLAIN ANALYZE выводит:
  • rows estimated <estimated_rows> — оценка числа строк этой стороны, сделанная оптимизатором порядка JOIN; выводится для сравнения с фактическим значением rows рядом; no stats, если оптимизатор не выдал оценку (см. Оценочные и фактические метрики JOIN).
  • 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.

Оценочные и фактические метрики JOIN

Шаг JOIN содержит оценки оптимизатора порядка JOIN — те же самые, что показывает EXPLAIN PLAN (см. раздел EXPLAIN PLAN), — а EXPLAIN ANALYZE выводит каждую из них рядом с измеренным значением:
  • Cost — оба значения учитывают совпавшие строки результата. Оценка — это рассчитанная оптимизатором стоимость поддерева JOIN: <selectivity> * <left_rows> * <right_rows> плюс стоимость его входных данных. Фактическое значение измеряется аналогично: совпавшие строки результата этого JOIN плюс фактическая стоимость всех JOIN ниже по дереву, относящихся к тому же кластеру переупорядочивания.
  • Selectivity — оценка выводится из числа уникальных значений ключей JOIN; фактическое значение — измеренная доля декартова произведения, попавшая в результат: <matched output rows> / (<left rows> * <right rows>), поскольку именно её пытается оценить оптимизатор.
  • Output rows — оценочное и фактическое число строк, созданных JOIN. Когда оба значения ненулевые, q-error выводит max(estimated / actual, actual / estimated) — стандартную меру качества оценки мощности: 1.00 означает идеальную оценку, а большое значение — что оптимизатор выбрал порядок JOIN на основе существенно неверной оценки мощности.
Оценка, которая не была выполнена, обозначается как no stats — например, если оптимизация порядка JOIN не запускалась, поскольку query_plan_optimize_join_order_limit имеет значение 0. Это отличается от not collected, которым помечается фактическое значение, которое не удалось измерить во время выполнения. JOIN с предварительно заполненной правой стороной (движок таблицы Join, direct JOIN) не проходят через оптимизатор порядка JOIN и выводят только строки участия. Шаг JOIN также выводит строки Input (left): и Input (right): со столбцами, которые каждая сторона передаёт в JOIN. Для таблиц из примера JOIN в разделе EXPLAIN PLAN шаг JOIN команды EXPLAIN ANALYZE SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id выглядит так:

Разветвление

fanout показывает кратность строк:
Внешнее соединение выводит по одной строке результата, дополненной значениями NULL, для каждой строки сохраняемой стороны, не нашедшей пары. Эти строки вычитаются, чтобы не искажать соотношение. Они есть только у сохраняемой стороны: у правой для RIGHT и FULL, у левой для LEFT и FULL:
  • fanout = 0 — сопоставленные строки вообще не дали строк результата, как и при ANTI JOIN: он выводит только строки, не нашедшие пары.
  • 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, когда необходимо узнать точное число совпадений, найденных левыми и правыми строками на другой стороне.
matches = 1 не позволяет собирать данные для любой комбинации. Возможность вывода данных по той или иной стороне JOIN определяется тем, что этот JOIN в любом случае должен делать, поэтому зависит как от алгоритма, так и от вида и строгости. Семейство хеш-алгоритмов. hash, parallel_hash и grace_hash всегда дают одинаковый результат: Правая сторона недоступна, если 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 с вложенными циклами. Оператор, который принимает секцию ON, не определяющую ключ соединения, если только один из предшествующих путей не забирает это условие себе (см. allow_block_nested_loop_join). Он поддерживает все виды и строгости, кроме ASOF, PASTE и ANY FULL: Значение для левой стороны выводится без этого параметра везде, где оператор и так отслеживает, какие левые строки нашли соответствие: это нужно каждому виду, сохраняющему несопоставленные левые строки, а также каждой строгости, которая берёт не более одной пары на левую строку. Значение для правой стороны выводится везде, где оператор хранит флаги соответствия, — именно они требуются для RIGHT, FULL и для SEMI/ANTI, управляемых правой стороной. ANY INNER тоже хранит эти флаги, но лишь для того, чтобы закрепить правую строку за единственной левой: оператор перестаёт забирать строки для левой строки, у которой уже есть пара, поэтому флаги отмечают пары результата, а не все соответствия, и в вывод не попадают. ALL INNER попадает в этот оператор только при отключённом hash, поскольку в противном случае условие превращается в CROSS JOIN с фильтром. Включение any_join_distinct_right_table_keys и здесь переключает ANY на семантику RightAny, при которой левая сторона выводится для всех видов, а правая — для RIGHT и FULL; ANY INNER переписывается в SEMI LEFT. Если число выводят оба алгоритма, эти числа совпадают. Алгоритмы слияния просто располагают бо́льшим объёмом информации, но в том, что считать соответствием, они не расходятся.

Строки для конкретных алгоритмов

Рассмотрим строки, которые добавляет каждая реализация JOIN. Для JOIN hash и parallel_hash, а также для движка таблицы Join строка Hash table: описывает хеш-таблицу, построенную на основе правой таблицы:
  • unique keys <unique_keys> — количество уникальных ключей, хранящихся в хеш-таблице на этапе построения.
  • memory <peak_memory> — пиковое потребление памяти хеш-таблицей на этапе построения.
Для JOIN grace_hash строка Hash table: также показывает, как JOIN адаптировался к ограничению памяти, а строка Spill: — были ли данные сброшены на диск:
  • 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 (проверки):
  • 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, а также для ALL INNER JOIN, у которого в секции ON нет равенства ключей (планировщик преобразует такой JOIN в CROSS JOIN с этим условием в качестве фильтра), строка Buffer: описывает, как правая таблица хранилась в памяти, а строка Spill: сообщает, была ли она записана на диск:
  • memory <peak_memory> — пиковое потребление памяти буферизованной правой таблицей.
  • compressed <yes|no> — был ли сжат хотя бы один буферизованный блок; в этом случае считыватель распаковывает каждый сохранённый блок.
  • Spill: — тот же флаг yes/no, что и для grace_hash; right spilled <right_spilled_bytes> указывает объём сжатых байтов, записанных на диск.
Здесь для обеих сторон указано matched not collected: постоянный предикат либо сопоставляет каждую левую строку с каждой правой строкой, либо не сопоставляет ни одной, поэтому определить, какие именно строки совпали, невозможно. Блочный JOIN с вложенными циклами, который обрабатывает все остальные секции ON без условия равенства ключей, выводит те же две строки о материализуемой им правой таблице:
  • memory <peak_memory> — пиковый объём памяти, занимаемый сохранённой правой таблицей. Значение равно 0, если каждый блок записывался на диск сразу по поступлении, поскольку ни один из них не хранился в памяти.
  • compressed <yes|no> — был ли сжат хотя бы один сохранённый блок; в этом случае считыватели распаковывают каждый сохранённый блок.
  • Spill: — тот же флаг yes/no, что и для grace_hash; при этом right spilled <right_spilled_bytes> показывает объём сжатых данных (в байтах), записанных на диск.
В отличие от случая CROSS, здесь matched может отображаться для обеих сторон, поскольку оператор вычисляет реальное условие для каждой проверяемой пары; для какой именно стороны — указано в таблице выше. Для JOIN с движком таблицы Join отображаются обе стороны, а также строка Hash table:, описывающая предварительно созданную таблицу. Для правой стороны указывается число строк, хранящихся в движке, а не число строк, созданных при построении для отдельного запроса.

Время по процессорам

При processors = 1 под каждой стадией выводится дополнительная строка, показывающая распределение затраченного времени между процессорами этой стадии:
<n> — это количество процессоров на стадии. Большой разрыв между median и max указывает на неравномерное распределение нагрузки между параллельными процессорами.

Реальное время выполнения шагов и ветвей и уровни параллелизма

Если не указано time = 0, для каждого шага плана запроса выводятся ещё две строки:
Здесь step означает сам шаг, а branch — поддерево, корнем которого является этот шаг: сам шаг и все расположенные ниже него шаги плана.
  • Time — время, в течение которого хотя бы один поток работал над этим шагом (step) или любым шагом его поддерева (branch). <share> — доля этого времени в общем времени выполнения запроса, выраженная в процентах.
  • Concurrency — среднее число потоков, занятых выполнением любого шага запроса, а не только данного, пока был активен шаг (step) или его поддерево (branch). <m> — максимальное число потоков, которое мог использовать запрос. Для шагов без собственных процессоров, например Union, когда у его входов уже одинаковые заголовки, вместо значения step выводится Unknown: измерять нечего. Значение branch при этом всё равно выводится.
Обе метрики рассчитываются на основе интервалов работы. Каждый раз, когда поток завершает часть работы, исполнитель фиксирует интервал: время начала, время окончания и шаг, к которому относится работа. Time шага — длительность объединения интервалов его работы. Промежутки, когда ни один поток не работал над шагом, не учитываются. Для расчёта Time ветви сначала объединяются интервалы всех шагов поддерева. branch корневого шага охватывает весь план, но его доля обычно меньше 100%. Строка Execution в сводке запроса показывает, на что ушло остальное время выполнения (см. сводку запроса в примере выше):
  • in steps — объединение интервалов всех шагов плана. Это время branch корневого шага; доля совпадает.
  • outside steps — время, когда хотя бы один поток выполнял работу процессора, но ни один из работавших процессоров не относился к шагам этого плана: приёмник выходных данных, Resize, преобразующие операции и шаги планов, недостижимых из данного плана.
  • idle — время, когда ни один поток не выполнял работу процессора: во время запуска и остановки исполнителя, а также когда все потоки ожидали.
Доли Time, доли time по стадиям и доли Execution рассчитываются с одним и тем же знаменателем, поэтому их можно сравнивать между собой. Concurrency — суммарное время занятости всех потоков запроса в пределах этого объединения, делённое на длительность объединения. В отличие от метрики parallelism для отдельных стадий, которая учитывает только потоки самой стадии, здесь учитываются потоки, занятые любым шагом запроса. Эта метрика отвечает на вопрос: “насколько были загружены потоки запроса в целом, пока был активен этот шаг (или его поддерево)?” Значение, близкое к <m>, означает, что во время работы шага запрос выполнялся с максимальным параллелизмом. Значение, близкое к 1, означает, что в этот период над запросом работал только один поток: для step этот поток работал над самим шагом, поэтому шаг становился точкой последовательного выполнения; для branch поток работал где-то в поддереве, не обязательно над этим шагом. Такой шаг становится настоящим узким местом, только если его доля Time также значительна.

EXPLAIN ESTIMATE

Показывает оценочное количество строк, меток и частей, которые будут прочитаны из таблиц при обработке запроса. Работает с таблицами семейства MergeTree. Пример Создание таблицы:
Query
Query
Response

EXPLAIN WHATIF

Оценивает, насколько гипотетический индекс пропуска данных ускорил бы SELECT-запрос, не создавая индекс на диске. Определите одного или нескольких кандидатов с помощью CREATE HYPOTHETICAL INDEX, затем выполните EXPLAIN WHATIF SELECT ..., чтобы для каждого кандидата узнать, применим ли он, сколько меток и байт предположительно потребуется прочитать и какова доля пропускаемых данных. Гипотетические проекции, определённые с помощью CREATE HYPOTHETICAL PROJECTION, также рассматриваются как кандидаты. Для оценки обычной проекции её первичный индекс строится в памяти по частям, которые считывал бы запрос. Затем ненужные диапазоны исключаются так же, как при чтении материализованной проекции. В отчёте приводятся число меток и строк, которые потребовалось бы прочитать, значение read_ratio по отношению к чтению базовой таблицы и verdict — выбрал бы оптимизатор эту проекцию или нет. При force_optimize_projection = 1 или prefer_optimize_projection = 1 оптимизатор использует любую пригодную проекцию, поэтому проекция, которую он не выбрал бы по стоимости, отмечается как chosen (forced). Если результат зависит от того, как была бы организована часть проекции, marks_span содержит диапазон значений, а решение, которое может оказаться любым, отмечается как too close to call. Для больших таблиц оценка выполняется по выборке гранул (см. projection_scan_budget_rows). Агрегирующие проекции, проекции с условием WHERE, собственными индексами пропуска данных, ключом по порядку коммитов или виртуальному столбцу либо сохраняемым _block_number, а также проекции, в которых отсутствует хотя бы один столбец, считываемый запросом, отмечаются как not_applicable. Настройки force_optimize_projection_name и preferred_optimize_projection_name игнорируются, а force_optimize_projection не приводит к ошибке выполнения оператора. Синтаксис
Настройки
  • projection_scan_budget_rows — сколько строк может прочитать оценка проекции, прежде чем перейти к выборке гранул. Значение по умолчанию: 10000000. 0 означает отсутствие ограничения; max_rows_to_read может уменьшить это значение. Выборка считывает как минимум около 30 гранул и не менее одной гранулы из каждой части; если max_rows_to_read этого не допускает, оценка получает статус unsupported.
  • empirical — 1 (по умолчанию) применяет индекс к гранулам, оставшимся после базового отсечения, в оперативной памяти, чтобы определить долю пропущенных гранул (верхнюю границу оценки). 0 пропускает этот этап. Если эмпирическая оценка не даёт результата (отключена или индекс невозможно оценить в оперативной памяти), оценщик переходит к статистике столбцов. Если и она недоступна, выводится только сводка о применимости индекса.
Вывод
  • source — как была получена оценка.
    • empirical: индекс строится в памяти по гранулам, оставшимся после базового отсечения, и подсчитывается, сколько гранул индекс мог бы пропустить. Это верхняя граница — см. ограничения в 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>. Показывает, какая доля таблицы осталась после отсечения по PK, партициям и существующим индексам, то есть какие данные поступают на вход гипотетическому индексу. Для проекции sampled_marks имеет вид <marks read> / <marks in the parts the query reads>; если первое число меньше второго, оценка получена по выборке.
  • 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. Эмпирический пример
Гипотетический индекс minmax сократил бы количество читаемых меток со 100 до 1 — skip_ratio: 99.0%. (est_bytes — это оценка на основе среднего размера строки, поэтому точное значение может отличаться.) Пример статистической оценки Статистика столбцов по умолчанию отключена. Чтобы задействовать вариант statistical, сначала задайте её для нужных столбцов и дождитесь завершения мутации materialize:
Затем отключите эмпирический режим, чтобы оценщик снова использовал статистику по столбцам:
Это число берётся из селективности статистики столбца для b < 10 (примерно 10 строк из 10000) и приводится как верхняя граница для skip_ratio. Значения sampled_parts / sampled_marks отсутствуют — данные не считывались. Если ни один из вариантов недоступен (например, empirical = 0 и статистика столбцов не определена), оценщик возвращает source: applicability_only и консервативное значение skip_ratio: 0.0%. Гипотетические проекции Обычная проекция оценивается таким же образом — относительно частей, которые запрос считывал бы после отсечения по первичному ключу и партициям:
marks и rows показывают, сколько меток и строк будет затронуто при чтении самой проекции, а не их долю от чтения базовой таблицы, поскольку гранула проекции содержит другие строки, чем гранула базовой таблицы. read_ratio — это отношение числа этих меток к числу меток базовой таблицы: 0.02x означает в пятьдесят раз меньше работы, а 15x — в пятнадцать раз больше. verdict определяется по правилу самого оптимизатора: число меток должно быть меньше, чем при чтении базовой таблицы, либо равным ему, если проекция обеспечивает порядок, заданный в ORDER BY запроса. Поэтому кандидат может быть applicable, но не быть выбранным.

EXPLAIN TABLE OVERRIDE

Показывает результат переопределения таблицы в схеме таблицы, к которой обращаются через табличную функцию. Также выполняет проверку и генерирует исключение, если такое переопределение привело бы к какой-либо ошибке. Пример Предположим, у вас есть удалённая таблица MySQL следующего вида:
Query
Query
Response
Проверка не является исчерпывающей, поэтому успешный запрос не гарантирует, что переопределение не приведёт к проблемам.