> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> Документация по EXPLAIN

# EXPLAIN

Показывает план выполнения оператора SQL.

<div class="vimeo-container">
  <Frame>
    <iframe
      src="//www.youtube.com/embed/hP6G2Nlz_cA"
      frameborder="0"
      allow="autoplay;
fullscreen;
picture-in-picture"
      allowfullscreen
    />
  </Frame>
</div>

Синтаксис:

```sql theme={null}
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 ...]
```

Пример:

```sql theme={null}
EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV;
```

```sql theme={null}
Output: sum(number)

Union
├──Aggregating
│  │  Keys:
│  │  Aggregates: sum(number)
│  │  Skip merging: 0
│  └──ReadFromSystemNumbers
│        Output: number
└──Sorting (Sorting for ORDER BY)
   │  Sort description: sum(number) ASC
   └──Aggregating
      │  Keys:
      │  Aggregates: sum(number)
      │  Skip merging: 0
      └──ReadFromSystemNumbers
            Output: number
```

<h2 id="explain-types">
  Типы EXPLAIN
</h2>

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

<h3 id="explain-ast">
  EXPLAIN AST
</h3>

Выводит AST запроса. Поддерживает все типы запросов, а не только `SELECT`.

Настройки:

* `graph` – Выводит AST в виде графа, описанного на языке описания графов [DOT](https://en.wikipedia.org/wiki/DOT_\(graph_description_language\)). По умолчанию: 0.

Примеры:

```sql theme={null}
EXPLAIN AST SELECT 1;
```

```sql theme={null}
SelectWithUnionQuery (children 1)
 ExpressionList (children 1)
  SelectQuery (children 1)
   ExpressionList (children 1)
    Literal UInt64_1
```

```sql theme={null}
EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today();
```

```sql theme={null}
  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)
        ExpressionList
```

<h3 id="explain-syntax">
  EXPLAIN SYNTAX
</h3>

Показывает абстрактное синтаксическое дерево (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`.

Примеры:

```sql title="Query" theme={null}
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;
```

```sql title="Response" theme={null}
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`:

```sql title="Query" theme={null}
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;
```

```sql title="Response" theme={null}
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.number
```

<h3 id="explain-query-tree">
  EXPLAIN QUERY TREE
</h3>

Настройки:

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

Пример:

```sql theme={null}
EXPLAIN QUERY TREE SELECT id, value FROM test_table;
```

```sql theme={null}
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_table
```

<h3 id="explain-plan">
  EXPLAIN PLAN
</h3>

Выводит шаги плана запроса.

Настройки:

* `optimize` — Управляет тем, применять ли оптимизации плана запроса перед его отображением. Значение по умолчанию: 1.
* `header` — Выводит заголовок для шага. Значение по умолчанию: 0.
* `description` — Выводит описание шага. Значение по умолчанию: 1.
* `indexes` — Показывает используемые индексы, количество отфильтрованных частей и количество отфильтрованных гранул для каждого применённого индекса. Значение по умолчанию: 0. Поддерживается для таблиц [MergeTree](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree). Начиная с ClickHouse >= v25.9, этот оператор показывает осмысленный результат только при использовании с `SETTINGS use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0`.
* `projections` — Показывает все проанализированные проекции и их влияние на фильтрацию на уровне частей на основе условий по первичному ключу проекции. Для каждой проекции в этом разделе приводится статистика, включая количество частей, строк, меток и диапазонов, оценённых с использованием первичного ключа проекции. Также показывается, сколько частей данных было пропущено благодаря этой фильтрации без чтения из самой проекции. Была ли проекция действительно использована для чтения или только проанализирована для фильтрации, можно определить по полю `description`. Значение по умолчанию: 0. Поддерживается для таблиц [MergeTree](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree).
* `actions` — Выводит подробную информацию о действиях шага. Значение по умолчанию: 1.
* `sorting` — Выводит описание сортировки для каждого шага плана, который формирует отсортированный вывод. Значение по умолчанию: 0.
* `keep_logical_steps` — Сохраняет логические шаги плана для JOIN вместо преобразования их в физические реализации JOIN. Значение по умолчанию: 0.
* `json` — Выводит шаги плана запроса как строку в формате [JSON](/docs/ru/reference/formats/JSON/JSON). Значение по умолчанию: 0. Чтобы избежать лишнего экранирования, рекомендуется использовать формат [TabSeparatedRaw (TSVRaw)](/docs/ru/reference/formats/TabSeparated/TabSeparatedRaw).
* `input_headers` — Выводит входные заголовки для шага. Значение по умолчанию: 0. В основном полезно только разработчикам для отладки проблем, связанных с несоответствием входных и выходных заголовков.
* `column_structure` — Также выводит структуру столбцов в заголовках помимо их имени и типа. Значение по умолчанию: 0. В основном полезно только разработчикам для отладки проблем, связанных с несоответствием входных и выходных заголовков.
* `distributed` — Показывает планы запроса, выполняемые на удалённых узлах для distributed таблиц или параллельных реплик. Не поддерживается вместе с `json`. Значение по умолчанию: 0.
* `compact` — Если включено, скрывает из плана шаги выражений и подробную информацию о действиях (входы, функции, псевдонимы и позиции вывода). Действует только при `actions = 1`. Значение по умолчанию: 1.
* `pretty` — Выводит дерево плана с использованием символов построения линий (├──, └──, │) вместо отступов для наглядного отображения иерархии. Также форматирует свойства шага JOIN в одну строку. Значение по умолчанию: 1.

<Note>
  По умолчанию `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`.
</Note>

Пример:

```sql theme={null}
EXPLAIN SELECT sum(number) FROM numbers(10) GROUP BY number % 4  LIMIT 1;
```

```sql theme={null}
Output: sum(number)

Limit (preliminary LIMIT)
│  Limit 1
│  Offset 0
└──Aggregating
   │  Keys: number MOD 4
   │  Aggregates: sum(number)
   │  Skip merging: 0
   └──ReadFromSystemNumbers
         Output: number
```

<Note>
  Оценка стоимости шагов и запроса не поддерживается.
</Note>

При `json = 1` план запроса представляется в формате JSON. Каждый узел — это словарь, который всегда содержит ключи `Node Type`, `Node Id` и `Plans`. `Node Type` — строка с именем шага, а `Node Id` — уникальный идентификатор шага (имя шага с числовым суффиксом, например `Union_10`). `Plans` — массив с описаниями дочерних шагов. В зависимости от типа узла и настроек могут добавляться и другие необязательные ключи.

Пример:

```sql theme={null}
EXPLAIN json = 1, description = 0 SELECT 1 UNION ALL SELECT 2 FORMAT TSVRaw;
```

```json theme={null}
[
  {
    "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`:

```json theme={null}
{
  "Node Type": "ReadFromStorage",
  "Description": "SystemOne"
}
```

При `header` = 1 в шаг добавляется ключ `Header` в виде массива столбцов.

Пример:

```sql theme={null}
EXPLAIN json = 1, description = 0, header = 1 SELECT 1, 2 + dummy;
```

```json theme={null}
[
  {
    "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`) и следующими необязательными ключами:

Индекс `Statistics` использует статистику столбцов по каждой части (минимальные/максимальные значения, а также количество значений `NULL` для столбцов `Nullable`), чтобы пропускать части, которые не могут соответствовать фильтру запроса.

* `Name` — имя индекса (в настоящее время используется только для индексов `Skip`).
* `Keys` — массив столбцов, используемых индексом.
* `Condition` — используемое условие.
* `Description` — описание индекса (в настоящее время используется только для индексов `Skip`).
* `Parts` — количество частей после/до применения индекса.
* `Granules` — количество гранул после/до применения индекса.
* `Ranges` — количество диапазонов гранул после применения индекса.

Пример:

```json theme={null}
"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` — Количество частей, пропущенных из-за фильтрации на уровне частей.

Пример:

```json theme={null}
"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 добавляемые ключи зависят от типа шага.

Пример:

```sql theme={null}
EXPLAIN json = 1, actions = 1, description = 0 SELECT 1 FORMAT TSVRaw;
```

```json theme={null}
[
  {
    "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` вместе с подробной информацией о выражениях:

```sql theme={null}
EXPLAIN actions = 1, compact = 0 SELECT sum(number) FROM numbers(10) GROUP BY number % 4;
```

```text theme={null}
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 вывод включает не только локальный план запроса, но и планы запросов, которые будут выполняться на удалённых узлах. Это полезно для анализа и отладки распределённых запросов.

<Note>
  `distributed` отображается только в формате legacy (без `pretty`), поскольку вывод `pretty` не встраивает планы удалённых сегментов в дерево плана. По этой причине включение `distributed` автоматически отключает связанные с `pretty` значения по умолчанию (`actions`, `compact` и `pretty`) независимо от `explain_query_plan_default`. Вы по-прежнему можете задать `actions=1` вручную. Параметр `distributed` также не поддерживается вместе с `json`.
</Note>

Пример с distributed таблицей:

```sql theme={null}
EXPLAIN distributed=1 SELECT * FROM remote('127.0.0.{1,2}', numbers(2)) WHERE number = 1;
```

```sql theme={null}
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
```

Пример с параллельными репликами:

```sql theme={null}
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;
```

```sql theme={null}
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
  используются следующие символы:

| Символ | Тип JOIN |
| - | - |
| `⋈` | Внутренний JOIN |
| `⟕` | Левый JOIN |
| `⟖` | Правый JOIN |
| `⟗` | Полный JOIN |
| `⋉` | Левый SEMI JOIN |
| `⋊` | Правый SEMI JOIN |
| `⋉` с зачёркиванием | Левый ANTI JOIN |
| `⋊` с зачёркиванием | Правый ANTI JOIN |
| `×` | CROSS JOIN |

Например, `t1 ⟕ t2` означает левый JOIN между таблицами `t1` и `t2`.
Число в квадратных скобках после имени таблицы (например, `t1[100]`) указывает оценочное количество строк,
если доступна статистика таблицы.

Под отношением JOIN каждый шаг JOIN выводит оценки, сформированные для него оптимизатором порядка JOIN:

```txt theme={null}
Cost: estimated <cost>
Selectivity: estimated (NDV) <selectivity>
Output rows: estimated <rows>
Left: rows estimated <left_rows>
Right: rows estimated <right_rows>
```

* `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`](/docs/ru/reference/settings/session-settings/query-plan#query_plan_optimize_join_order_limit)
равен `0` — или когда нет оснований для оценки.

Строки `Input (left):` и `Input (right):` перечисляют столбцы, которые каждая сторона передаёт в JOIN.

Параметр `pretty` хорошо работает вместе с `compact = 1`, который скрывает шаги `Expression` и подробную информацию о действиях, упрощая чтение плана.

Подробный пример с JOIN. Параметр
[`join_runtime_filter_min_probe_rows`](/docs/ru/reference/settings/session-settings/join-runtime#join_runtime_filter_min_probe_rows)
понижен только для того, чтобы даже такая небольшая таблица создавала runtime-фильтр JOIN:

```sql theme={null}
SET join_runtime_filter_min_probe_rows = 10;

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;
```

```text theme={null}
Output: id, value, id, value

Join (JOIN FillRightFirst)
│  t1[100] ⋈ t2[100]
│  Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│  Cost: estimated 100.00
│  Selectivity: estimated (NDV) 0.01
│  Output rows: estimated 100.00
│  Left: rows estimated 100.00
│  Right: rows estimated 100.00
│  Join conditions: id = id
│  Input (left): id, value
│  Input (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, value
```

<h3 id="explain-pipeline">
  EXPLAIN PIPELINE
</h3>

Настройки:

* `header` — Выводит заголовок для каждого выходного порта. По умолчанию: 0.
* `graph` — Выводит граф, описанный на языке описания графов [DOT](https://en.wikipedia.org/wiki/DOT_\(graph_description_language\)). По умолчанию: 0.
* `compact` — Выводит граф в компактном режиме, если включена настройка `graph`. По умолчанию: 1.
* `compact_repeated_processor_chains` — Объединяет соседние повторяющиеся цепочки процессоров в текстовом выводе, показывая одну копию цепочки с числом повторений. Это может упростить чтение параллельных конвейеров, когда одна и та же цепочка встречается много раз, например при JOIN. На вывод графа это не влияет. По умолчанию: 0.

```text theme={null}
Resize 16 → 1
  FillingRightJoinSide          │
    SimpleSquashingTransform    │ × 16
      Resize 1 → 16
```

Если `compact=0` и `graph=1`, имена процессоров будут содержать дополнительный суффикс с уникальным идентификатором процессора.

Пример:

```sql theme={null}
EXPLAIN PIPELINE SELECT sum(number) FROM numbers_mt(100000) GROUP BY number % 4;
```

```sql theme={null}
(Union)
(Expression)
ExpressionTransform
  (Expression)
  ExpressionTransform
    (Aggregating)
    Resize 2 → 1
      AggregatingTransform × 2
        (Expression)
        ExpressionTransform × 2
          (SettingQuotaAndLimits)
            (ReadFromStorage)
            NumbersRange × 2 0 → 1
```

<h3 id="explain-analyze">
  EXPLAIN ANALYZE
</h3>

`EXPLAIN ANALYZE` действительно выполняет запрос, отбрасывает строки результата и выводит то же дерево плана, что и `EXPLAIN PLAN`, добавляя к каждому шагу сведения о том, что реально произошло во время выполнения.

Настройки:

`EXPLAIN ANALYZE` поддерживает те же параметры отображения, что и `EXPLAIN PLAN` (они описаны в разделе [EXPLAIN PLAN](#explain-plan)).

* `header` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `description` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `projections` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `sorting` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `input_headers` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `column_structure` — см. раздел [EXPLAIN PLAN](#explain-plan).
* `actions` — см. раздел [EXPLAIN PLAN](#explain-plan). По умолчанию: 1.
* `indexes` — см. раздел [EXPLAIN PLAN](#explain-plan). По умолчанию: 1.
* `compact` — см. раздел [EXPLAIN PLAN](#explain-plan). По умолчанию: 1.
* `pretty` — см. раздел [EXPLAIN PLAN](#explain-plan). По умолчанию: 1.
* `processors` — для `EXPLAIN ANALYZE` выводит дополнительную строку для каждой стадии с распределением времени выполнения по каждому процессору: `min`, `median`, `max` и `sum`. Это полезно для выявления перекоса нагрузки между параллельными процессорами. По умолчанию: 0.
* `matches` — для `EXPLAIN ANALYZE` заставляет шаги JOIN выполнять дополнительный учёт, необходимый для метрик `matched`, `match rate` и `fanout` в случаях, когда эти значения невозможно вывести из результатов JOIN. Когда это возможно, они выводятся и без этой опции. См. [Шаги JOIN](#explain-analyze-join-steps). По умолчанию: 0.
* `time` — для `EXPLAIN ANALYZE` выводит для каждого шага строки `Time` и `Concurrency` с фактическим (астрономическим) временем выполнения и уровнем параллелизма шага и его ветви. См. [Фактическое время выполнения и уровни параллелизма шагов и ветвей](#explain-analyze-concurrency). По умолчанию: 1.

<Note>
  Поскольку `EXPLAIN ANALYZE` действительно выполняет обёрнутый запрос, он ведёт себя как
  этот запрос — и, в отличие от форм `EXPLAIN`, которые не выполняют запрос, — в нескольких отношениях:

  * **Квоты и ограничения.** Он учитывается в тех же [квоты](/docs/ru/concepts/features/configuration/server-config/quotas)
    и подпадает под те же [ограничения](/docs/ru/concepts/features/configuration/settings/query-complexity)
    (например, `query_selects`, `read_rows`), что и при прямом выполнении запроса. Источники, освобождённые от квот на этапе планирования
    (такие как `system.one`), не учитываются.
  * **Неуспешные транзакции.** Внутри [transaction](/docs/ru/concepts/features/operations/insert/transactions),
    которая уже завершилась ошибкой (`ROLLED_BACK`), он отклоняется с `INVALID_TRANSACTION`,
    так же как и обычный `SELECT` — сначала выполните `ROLLBACK`.
  * **Потоковые чтения.** При потоковом чтении (`FROM ... STREAM`) он отклоняется с
    `NOT_IMPLEMENTED`, потому что такое чтение никогда не завершается.
  * **Распределённые запросы.** Он не поддерживается для запросов, выполняемых в
    режиме [distributed](/docs/ru/reference/engines/table-engines/special/distributed).
</Note>

Пример:

```sql theme={null}
EXPLAIN ANALYZE SELECT number % 10 AS k, count() FROM numbers_mt(1000000) GROUP BY k;
```

```text theme={null}
Query summary:
  Time:        22.65 ms (planning 7.98 ms · execution 14.66 ms)
  Execution:   in steps 3.46 ms (23.60%) · outside steps 16.60 us (0.11%) · idle 11.19 ms (76.29%)
  Read:        1.00 million rows, 8.00 MB (68.20 million rows/s., 545.59 MB/s.)
  Peak memory: 16.00 KiB

Output: number MOD 10, count()

Expression ((Project names + Projection))
│  I/O: rows 10 → 10 · 90 B → 90 B
│  Time: step 12.09 us (0.08%) · branch 3.46 ms (23.60%)
│  Concurrency: step 1.00/16 · branch 4.96/16
│    time 12.36 us (0.08%) · 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
   │  Time: step 2.03 ms (13.85%) · branch 3.45 ms (23.52%)
   │  Concurrency: step 5.93/16 · branch 4.97/16
   │    Stage (partial aggregation): time 1.69 ms (11.50%) · parallelism 1.80/15
   │    Stage (final aggregation): time 351.39 us (2.40%) · parallelism 1.10/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: step 2.02 ms (13.78%) · branch 2.76 ms (18.81%)
      │  Concurrency: step 7.13/16 · branch 5.87/16
      │    time 2.02 ms (13.81%) · parallelism 2.26/11
      └──ReadFromSystemNumbers
            Output: number
            I/O: rows 0 → 1.00 million · 0 B → 8.00 MB
            Time: step 2.58 ms (17.57%) · branch 2.58 ms (17.57%)
            Concurrency: step 5.98/16 · branch 5.98/16
              time 2.58 ms (17.58%) · parallelism 3.55/15
```

Давайте разберём вывод. Сначала посмотрим на заголовок.

```txt theme={null}
   Query summary:
     Time:        <total> (planning <planning> · execution <execution>)
     Execution:   in steps <t> (<share>%) · outside steps <t> (<share>%) · idle <t> (<share>%)
     Read:        <rows> rows, <bytes> (<rows/s>, <bytes/s>)
     Peak memory: <peak>
```

* `Time` — общее время, разделённое на этапы планирования (то есть создание плана + оптимизация плана + построение конвейера) и выполнения (запуск конвейера).
* `Execution` — не выводится при `time = 0`. Разбивает время выполнения на `in steps`, `outside steps` и `idle`. В сумме эти части дают `execution`. См. [Фактическое время выполнения шагов и ветвей и уровни параллелизма](#explain-analyze-concurrency).
* `Read` — строки и несжатые байты, прочитанные из таблиц, с указанием пропускной способности — те же числа, которые нижний колонтитул обычного запроса показывает как "Processed".
* `Peak memory` — пиковое потребление памяти запросом.

Теперь рассмотрим новые строки, которые появляются в плане запроса.

```txt theme={null}
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)`.

<Note>
  ClickHouse распараллеливает не только выполнение задач внутри шага плана, но и выполнение самих шагов плана. Метрика `parallelism` отражает только работу этого шага. Другие шаги могут выполняться параллельно, поэтому это число не показывает, как параллелизм шага соотносится со всем запросом. Чтобы оценить параллелизм всего запроса, смотрите метрику `Concurrency`, выводимую при `time = 1` ([Фактическое время и уровни параллелизма шагов и ветвей](#explain-analyze-concurrency)).
</Note>

<Note>
  Максимальное число в `parallelism` вычисляется как минимум из:

  1. общего числа задач внутри шага плана;
  2. максимального числа потоков обработки запроса, заданного в `max_threads`.
</Note>

<h4 id="explain-analyze-join-steps">
  Шаги JOIN
</h4>

Для шага JOIN `EXPLAIN ANALYZE` выводит строки, сопоставляющие оценки оптимизатора порядка JOIN с тем, что произошло в действительности (см. [Оценочные и фактические метрики JOIN](#explain-analyze-join-estimation)), а также строки *участия* для каждой стороны — `Left` и `Right`, — за которыми следуют строки, относящиеся к конкретной реализации JOIN. `Left` и `Right` соответствуют логическим сторонам в SQL. В большинстве случаев `Left` также является стороной проверки JOIN, а `Right` — стороной построения (build). Однако так бывает не всегда, поскольку во время выполнения JOIN стороны могут меняться местами. Охвачены все значения [`join_algorithm`](/docs/ru/reference/settings/session-settings/join#join_algorithm) (`hash`, `parallel_hash`, `grace_hash`, `partial_merge`, `full_sorting_merge`, `parallel_full_sorting_merge`, `direct`), а также три реализации, которые нельзя выбрать этой настройкой: JOIN типа `CROSS` или `COMMA`, [блочный JOIN с вложенными циклами](/docs/ru/reference/statements/select/join#join-with-an-arbitrary-on-condition), используемый для секции `ON`, которая не определяет ключ JOIN, и движок таблицы [`Join`](/docs/ru/reference/engines/table-engines/special/join). Большинство из них выводят сведения об обеих сторонах; некоторые — только о стороне, которую материализуют (например, `direct` выводит только `Left:`).

Строки для каждой стороны имеют одинаковую структуру:

```txt theme={null}
Left:  rows estimated <estimated_left_rows>  · rows <left_rows>  · matched <matched_left_rows>  · match rate <match_rate>% · fanout <fanout>
Right: rows estimated <estimated_right_rows> · rows <right_rows> · matched <matched_right_rows> · match rate <match_rate>% · fanout <fanout>
```

Для каждой стороны `EXPLAIN ANALYZE` выводит:

* `rows estimated <estimated_rows>` — оценка числа строк этой стороны, сделанная оптимизатором порядка JOIN; выводится для сравнения с фактическим значением `rows` рядом; `no stats`, если оптимизатор не выдал оценку (см. [Оценочные и фактические метрики JOIN](#explain-analyze-join-estimation)).
* `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`.

<h4 id="explain-analyze-join-estimation">
  Оценочные и фактические метрики JOIN
</h4>

Шаг JOIN содержит оценки оптимизатора порядка JOIN — те же самые, что показывает `EXPLAIN PLAN` (см. раздел [EXPLAIN PLAN](#explain-plan)), — а `EXPLAIN ANALYZE` выводит каждую из них рядом с измеренным значением:

```txt theme={null}
Cost: estimated <cost> · actual <cost>
Selectivity: estimated (NDV) <selectivity> · actual (cartesian) <selectivity>
Output rows: estimated <rows> · actual <rows> · q-error <ratio>
```

* `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`](/docs/ru/reference/settings/session-settings/query-plan#query_plan_optimize_join_order_limit) имеет значение `0`. Это отличается от `not collected`, которым помечается фактическое значение, которое не удалось измерить во время выполнения. JOIN с предварительно заполненной правой стороной (движок таблицы [`Join`](/docs/ru/reference/engines/table-engines/special/join), `direct` JOIN) не проходят через оптимизатор порядка JOIN и выводят только строки участия.

Шаг JOIN также выводит строки `Input (left):` и `Input (right):` со столбцами, которые каждая сторона передаёт в JOIN.

Для таблиц из примера JOIN в разделе [EXPLAIN PLAN](#explain-plan) шаг JOIN команды `EXPLAIN ANALYZE SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id` выглядит так:

```text theme={null}
Join (JOIN FillRightFirst)
│  t1[100] ⋈ t2[100]
│  Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│  Join conditions: id = id
│  Cost: estimated 100.00 · actual 100.00
│  Selectivity: estimated (NDV) 0.01 · actual (cartesian) 0.01
│  Output rows: estimated 100.00 · actual 100.00 · q-error 1.00
│  Left: rows estimated 100.00 · rows 100.00 · matched 100.00 · match rate 100.00% · fanout 1.00
│  Right: rows estimated 100.00 · rows 100.00 · matched not collected · match rate not collected · fanout not collected
│  Hash table: unique keys 100.00 · memory 6.27 KB
│  Input (left): id, value
│  Input (right): id, value
│  I/O: rows 200 → 100 (50.00%) · 3.58 KB → 3.58 KB
│    Stage (build): time 127.45 us (1.2%) · parallelism 0.99/1
│    Stage (probe): time 124.99 us (1.2%) · parallelism 0.99/1
```

<h4 id="explain-analyze-fanout">
  Разветвление
</h4>

`fanout` показывает кратность строк:

```txt theme={null}
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` — сопоставленные строки вообще не дали строк результата, как и при `ANTI` JOIN: он выводит только строки, не нашедшие пары.
* `fanout = 1` — корректное соединение 1:1; каждая сопоставленная строка дала ровно одну строку результата.
* `fanout > 1` — соединение 1:N; дублирующиеся ключи на другой стороне размножили строки. Большое значение сразу с обеих сторон — признак непреднамеренного разрастания декартова произведения.

<h4 id="explain-analyze-matches">
  Когда для подсчёта требуется `matches = 1`
</h4>

Большинство этих чисел получается из данных, которые 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` ничего не требуется.

<Note>
  По умолчанию опция отключена, поскольку дополнительный учёт требует ресурсов и снижает точность измерений. Работа выполняется внутри цикла проверки, а её объём растёт с числом строк результата. Используйте `matches = 1`, когда необходимо узнать точное число совпадений, найденных левыми и правыми строками на другой стороне.
</Note>

`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`](/docs/ru/reference/settings/session-settings/other#any_join_distinct_right_table_keys) переключает `ANY` на прежнюю семантику `RightAny`, при которой выводится одна строка для каждой левой строки и поэтому сохраняются оба счётчика. В этом случае `ANY RIGHT` и `ANY FULL` выводят **обе** стороны, а `ANY INNER` переписывается в `SEMI LEFT`.

Движок таблицы [`Join`](/docs/ru/reference/engines/table-engines/special/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`.** Ни одна из сторон, как описано [выше](#explain-analyze-join-algorithm-lines).

**Блочный JOIN с вложенными циклами.** Оператор, который принимает секцию `ON`, не определяющую ключ соединения, если только один из предшествующих путей не забирает это условие себе (см. [`allow_block_nested_loop_join`](/docs/ru/reference/settings/session-settings/allow)). Он поддерживает все виды и строгости, кроме `ASOF`, `PASTE` и `ANY FULL`:

| JOIN | `matched` слева | `matched` справа |
| - | - | - |
| `ALL FULL` | да | да |
| `ALL LEFT`, `ANY LEFT`, `SEMI LEFT`, `ANTI LEFT`, `ANY INNER` | да | нет |
| `ALL RIGHT`, `ANY RIGHT`, `SEMI RIGHT`, `ANTI RIGHT` | с `matches = 1` | да |
| `ALL INNER` | с `matches = 1` | нет |

Значение для левой стороны выводится без этого параметра везде, где оператор и так отслеживает, какие левые строки нашли соответствие: это нужно каждому виду, сохраняющему несопоставленные левые строки, а также каждой строгости, которая берёт не более одной пары на левую строку. Значение для правой стороны выводится везде, где оператор хранит флаги соответствия, — именно они требуются для `RIGHT`, `FULL` и для `SEMI`/`ANTI`, управляемых правой стороной. `ANY INNER` тоже хранит эти флаги, но лишь для того, чтобы закрепить правую строку за единственной левой: оператор перестаёт забирать строки для левой строки, у которой уже есть пара, поэтому флаги отмечают пары результата, а не все соответствия, и в вывод не попадают. `ALL INNER` попадает в этот оператор только при отключённом `hash`, поскольку в противном случае условие превращается в `CROSS JOIN` с фильтром.

Включение [`any_join_distinct_right_table_keys`](/docs/ru/reference/settings/session-settings/other#any_join_distinct_right_table_keys) и здесь переключает `ANY` на семантику `RightAny`, при которой левая сторона выводится для всех видов, а правая — для `RIGHT` и `FULL`; `ANY INNER` переписывается в `SEMI LEFT`.

Если число выводят оба алгоритма, эти числа совпадают. Алгоритмы слияния просто располагают бо́льшим объёмом информации, но в том, что считать соответствием, они не расходятся.

<h4 id="explain-analyze-join-algorithm-lines">
  Строки для конкретных алгоритмов
</h4>

Рассмотрим строки, которые добавляет каждая реализация JOIN.

Для JOIN `hash` и `parallel_hash`, а также для движка таблицы `Join` строка `Hash table:` описывает хеш-таблицу, построенную на основе правой таблицы:

```txt theme={null}
Hash table: unique keys <unique_keys> · memory <peak_memory>
```

* `unique keys <unique_keys>` — количество уникальных ключей, хранящихся в хеш-таблице на этапе построения.
* `memory <peak_memory>` — пиковое потребление памяти хеш-таблицей на этапе построения.

Для JOIN `grace_hash` строка `Hash table:` также показывает, как JOIN адаптировался к ограничению памяти, а строка `Spill:` — были ли данные сброшены на диск:

```txt theme={null}
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 (проверки)`:

```txt theme={null}
Right: rows estimated <estimated_right_rows> · 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`, а также для `ALL INNER JOIN`, у которого в секции `ON` нет равенства ключей (планировщик преобразует такой JOIN в `CROSS JOIN` с этим условием в качестве фильтра), строка `Buffer:` описывает, как правая таблица хранилась в памяти, а строка `Spill:` сообщает, была ли она записана на диск:

```txt theme={null}
Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>
```

* `memory <peak_memory>` — пиковое потребление памяти буферизованной правой таблицей.
* `compressed <yes|no>` — был ли сжат хотя бы один буферизованный блок; в этом случае считыватель распаковывает каждый сохранённый блок.
* `Spill:` — тот же флаг `yes`/`no`, что и для `grace_hash`; `right spilled <right_spilled_bytes>` указывает объём сжатых байтов, записанных на диск.

Здесь для обеих сторон указано `matched not collected`: постоянный предикат либо сопоставляет каждую левую строку с каждой правой строкой, либо не сопоставляет ни одной, поэтому определить, какие именно строки совпали, невозможно.

Блочный JOIN с вложенными циклами, который обрабатывает все остальные секции `ON` без условия равенства ключей, выводит те же две строки о материализуемой им правой таблице:

```txt theme={null}
Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>
```

* `memory <peak_memory>` — пиковый объём памяти, занимаемый сохранённой правой таблицей. Значение равно `0`, если каждый блок записывался на диск сразу по поступлении, поскольку ни один из них не хранился в памяти.
* `compressed <yes|no>` — был ли сжат хотя бы один сохранённый блок; в этом случае считыватели распаковывают каждый сохранённый блок.
* `Spill:` — тот же флаг `yes`/`no`, что и для `grace_hash`; при этом `right spilled <right_spilled_bytes>` показывает объём сжатых данных (в байтах), записанных на диск.

В отличие от случая `CROSS`, здесь `matched` может отображаться для обеих сторон, поскольку оператор вычисляет реальное условие для каждой проверяемой пары; для какой именно стороны — указано в таблице [выше](#explain-analyze-matches).

Для JOIN с движком таблицы [`Join`](/docs/ru/reference/engines/table-engines/special/join) отображаются обе стороны, а также строка `Hash table:`, описывающая предварительно созданную таблицу. Для правой стороны указывается число строк, хранящихся в движке, а не число строк, созданных при построении для отдельного запроса.

<h4 id="explain-analyze-processors">
  Время по процессорам
</h4>

При `processors = 1` под каждой стадией выводится дополнительная строка, показывающая распределение затраченного времени между процессорами этой стадии:

```txt theme={null}
Time per processor (<n>): min <t> · median <t> · max <t> · sum <t>
```

`<n>` — это количество процессоров на стадии. Большой разрыв между `median` и `max` указывает на неравномерное распределение нагрузки между параллельными процессорами.

<h4 id="explain-analyze-concurrency">
  Реальное время выполнения шагов и ветвей и уровни параллелизма
</h4>

Если не указано `time = 0`, для каждого шага плана запроса выводятся ещё две строки:

```txt theme={null}
Time: step <t> (<share>%) · branch <t> (<share>%)
Concurrency: step <n>/<m> · branch <n>/<m>
```

Здесь `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` также значительна.

<h3 id="explain-estimate">
  EXPLAIN ESTIMATE
</h3>

Показывает оценочное количество строк, меток и частей, которые будут прочитаны из таблиц при обработке запроса. Работает с таблицами семейства [MergeTree](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree).

**Пример**

Создание таблицы:

```sql title="Query" theme={null}
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;
```

```sql title="Query" theme={null}
EXPLAIN ESTIMATE SELECT * FROM ttt;
```

```text title="Response" theme={null}
┌─database─┬─table─┬─parts─┬─rows─┬─marks─┐
│ default  │ ttt   │     1 │  128 │     8 │
└──────────┴───────┴───────┴──────┴───────┘
```

<h3 id="explain-whatif">
  EXPLAIN WHATIF
</h3>

Оценивает, насколько гипотетический индекс пропуска данных ускорил бы `SELECT`-запрос, *не* создавая индекс на диске. Определите одного или нескольких кандидатов с помощью [`CREATE HYPOTHETICAL INDEX`](/docs/ru/reference/statements/hypothetical-index#create-hypothetical-index), затем выполните `EXPLAIN WHATIF SELECT ...`, чтобы для каждого кандидата узнать, применим ли он, сколько меток и байт предположительно потребуется прочитать и какова доля пропускаемых данных.

Гипотетические проекции, определённые с помощью [`CREATE HYPOTHETICAL PROJECTION`](/docs/ru/reference/statements/hypothetical-projection#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` не приводит к ошибке выполнения оператора.

**Синтаксис**

```sql theme={null}
EXPLAIN WHATIF [empirical = 0] SELECT ...
```

**Настройки**

* `projection_scan_budget_rows` — сколько строк может прочитать оценка проекции, прежде чем перейти к выборке гранул. Значение по умолчанию: `10000000`. `0` означает отсутствие ограничения; `max_rows_to_read` может уменьшить это значение. Выборка считывает как минимум около 30 гранул и не менее одной гранулы из каждой части; если `max_rows_to_read` этого не допускает, оценка получает статус `unsupported`.
* `empirical` — `1` (по умолчанию) применяет индекс к гранулам, оставшимся после базового отсечения, в оперативной памяти, чтобы определить долю пропущенных гранул (верхнюю границу оценки). `0` пропускает этот этап. Если эмпирическая оценка не даёт результата (отключена или индекс невозможно оценить в оперативной памяти), оценщик переходит к [статистике столбцов](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree#column-statistics). Если и она недоступна, выводится только сводка о применимости индекса.

**Вывод**

```text theme={null}
Baseline (after PK + partition + existing indexes):
  table:       db.t
  parts:       1
  marks:       100
  rows:        10000
  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`](/docs/ru/reference/statements/hypothetical-index#limitations).
  * `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`.

**Эмпирический пример**

```sql theme={null}
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;
```

```text theme={null}
Baseline (after PK + partition + existing indexes):
  table:       default.t
  parts:       1
  marks:       100
  rows:        10000
  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` — это оценка на основе среднего размера строки, поэтому точное значение может отличаться.)

**Пример статистической оценки**

Статистика [столбцов](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree#column-statistics) по умолчанию отключена. Чтобы задействовать вариант `statistical`, сначала задайте её для нужных столбцов и дождитесь завершения мутации `materialize`:

```sql theme={null}
ALTER TABLE t ADD STATISTICS b TYPE tdigest;
ALTER TABLE t MATERIALIZE STATISTICS b SETTINGS mutations_sync = 1;
```

Затем отключите эмпирический режим, чтобы оценщик снова использовал статистику по столбцам:

```sql theme={null}
EXPLAIN WHATIF empirical = 0 SELECT * FROM t WHERE b < 10;
```

```text theme={null}
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%`.

**Гипотетические проекции**

Обычная проекция оценивается таким же образом — относительно частей, которые запрос считывал бы после отсечения по первичному ключу и партициям:

```sql theme={null}
CREATE TABLE t (a UInt64, b UInt64, v UInt64) ENGINE = MergeTree ORDER BY a SETTINGS index_granularity = 100;
INSERT INTO t SELECT number, number % 100, number FROM numbers(10000);
CREATE HYPOTHETICAL PROJECTION p_b ON t (SELECT a, b, v ORDER BY b);
EXPLAIN WHATIF SELECT count() FROM t WHERE b = 42;
```

```text theme={null}
Baseline (after PK + partition + existing indexes):
  table:       default.t
  parts:       1
  marks:       100
  rows:        10000
  est_bytes:   80.79 KiB

With p_b (normal projection, hypothetical):
  status:       applicable
  marks:        2
  rows:         200
  read_ratio:   0.02x
  verdict:      chosen
  reason:       2 marks would be read instead of 100 from the base table

Estimation:
  source:           empirical
  empirical_status: ok
  sampled_parts:    1 / 1
  sampled_marks:    100 / 100
  elapsed_us:       1126
```

`marks` и `rows` показывают, сколько меток и строк будет затронуто при чтении самой проекции, а не их долю от чтения базовой таблицы, поскольку гранула проекции содержит другие строки, чем гранула базовой таблицы. `read_ratio` — это отношение числа этих меток к числу меток базовой таблицы: `0.02x` означает в пятьдесят раз меньше работы, а `15x` — в пятнадцать раз больше. `verdict` определяется по правилу самого оптимизатора: число меток должно быть меньше, чем при чтении базовой таблицы, либо равным ему, если проекция обеспечивает порядок, заданный в `ORDER BY` запроса. Поэтому кандидат может быть `applicable`, но не быть выбранным.

<h3 id="explain-table-override">
  EXPLAIN TABLE OVERRIDE
</h3>

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

**Пример**

Предположим, у вас есть удалённая таблица MySQL следующего вида:

```sql title="Query" theme={null}
CREATE TABLE db.tbl (
    id INT PRIMARY KEY,
    created DATETIME DEFAULT now()
)
```

```sql title="Query" theme={null}
EXPLAIN TABLE OVERRIDE mysql('127.0.0.1:3306', 'db', 'tbl', 'root', 'clickhouse')
PARTITION BY toYYYYMM(assumeNotNull(created))
```

```text title="Response" theme={null}
┌─explain─────────────────────────────────────────────────┐
│ PARTITION BY uses columns: `created` Nullable(DateTime) │
└─────────────────────────────────────────────────────────┘
```

<Note>
  Проверка не является исчерпывающей, поэтому успешный запрос не гарантирует, что переопределение не приведёт к проблемам.
</Note>
