> ## 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.

# Выберите подход к оптимизации

> Используйте данные о медленном запросе к ClickHouse, чтобы выбрать подходящий способ оптимизации

Используйте данные [журнала запросов](/docs/ru/reference/system-tables/query_log), контролируемые сравнения и [планы запросов](/docs/ru/reference/statements/explain), чтобы оценить подходы к оптимизации, устраняющие выявленное узкое место.

<div id="before-you-begin">
  ## Перед началом работы
</div>

Начните с воспроизводимой исходной точки и гипотезы об узком месте. Если вы ещё не выявили его, начните с руководств [Диагностика медленных запросов](/docs/ru/guides/clickhouse/performance-and-monitoring/diagnose-slow-queries) и [Выявление узких мест запросов](/docs/ru/guides/clickhouse/performance-and-monitoring/isolate-query-bottlenecks).

В примерах этого руководства используется таблица `nyc_taxi.trips_small_inferred`. Чтобы выполнить их без изменений, создайте и заполните таблицу, если ещё не сделали этого:

<Accordion title="Настройка демонстрационного набора данных">
  <Note>
    Размер исходного файла Parquet составляет примерно 5,8 ГБ. Его загрузка может занять несколько минут в зависимости от скорости сети и доступных ресурсов.
  </Note>

  ```sql theme={null}
  CREATE DATABASE IF NOT EXISTS nyc_taxi;
  USE nyc_taxi;

  CREATE TABLE nyc_taxi.trips_small_inferred
  ORDER BY () EMPTY
  AS SELECT *
  FROM s3(
      'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/clickhouse-academy/nyc_taxi_2009-2010.parquet',
      NOSIGN,
      Parquet
  );

  INSERT INTO nyc_taxi.trips_small_inferred
  SELECT *
  FROM s3(
      'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/clickhouse-academy/nyc_taxi_2009-2010.parquet',
      NOSIGN,
      Parquet
  );
  ```
</Accordion>

<div id="choose-an-approach">
  ## Выберите подход
</div>

На основе собранных данных определите, с чего начать. Выбирайте наименее специализированное изменение, которое решает проблему:

| Признак                                                                  | Начните с                                                                       | Ожидаемый эффект                                                         |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Запрос читает широкие или ненужные ему столбцы                           | [Сократите объём читаемых данных](#reduce-the-data-read)                        | Объём прочитанных данных, использование памяти и вычислительная нагрузка |
| Избирательный фильтр всё ещё читает много [частей или гранул](/docs/ru/parts) | [Согласуйте структуру данных с запросом](#align-the-data-layout-with-the-query) | Количество прочитанных строк и гранул                                    |
| В запросе преобладают повторяющиеся преобразования или агрегации         | [Предварительно вычисляйте повторяющиеся операции](#precompute-repeatable-work) | Вычисления, выполняемые во время запроса                                 |

Если собранные данные не соответствуют ни одной из этих категорий, вернитесь к плану запроса, а не пытайтесь искусственно подогнать запрос под один из подходов.

<div id="reduce-the-data-read">
  ## Сократите объём считываемых данных
</div>

* **Используйте, когда:** запрос читает широкие столбцы или ненужные ему столбцы.
* **Изменение:** сократите размер или количество столбцов, считываемых запросом.
* **Проверка:** сравните `read_bytes`, использование памяти и длительность при одинаковых условиях.

ClickHouse считывает только столбцы, необходимые для запроса, но ему всё равно приходится считывать, распаковывать и обрабатывать выбранные данные. Проверьте выбранные столбцы и их типы. [Вывод схемы](/docs/ru/concepts/features/interfaces/schema-inference) — хорошая отправная точка, однако выведенные типы могут быть шире или менее строгими, чем требуется данным в продакшне.

<div id="review-column-types">
  ### Проверка типов столбцов
</div>

<span id="choose-precise-types" />

**Выбирайте подходящие типы**

Выбирайте типы, обеспечивающие необходимый для рабочей нагрузки диапазон и точность, но не занимающие больше места, чем требуется. Для таких значений используйте числовые типы и типы даты вместо универсального [`String`](/docs/ru/reference/data-types/string), а также выбирайте наименьший [знаковый или беззнаковый числовой тип](/docs/ru/reference/data-types/int-uint), который безопасно покрывает ожидаемый диапазон. Для временных столбцов используйте [`Date`](/docs/ru/reference/data-types/date) или [`DateTime`](/docs/ru/reference/data-types/datetime), если только вам не нужен более широкий диапазон или дробная точность [`Date32`](/docs/ru/reference/data-types/date32) либо [`DateTime64`](/docs/ru/reference/data-types/datetime64).

<span id="use-nullable-columns-deliberately" />

**Используйте столбцы с типом Nullable обоснованно**

Столбец [`Nullable`](/docs/ru/reference/data-types/nullable) помимо значений хранит отдельную маску NULL, которую ClickHouse также должен читать и обрабатывать. Используйте его, если различие между значением NULL и значением типа по умолчанию существенно. Если столбец гарантированно содержит значение, тип без Nullable позволяет избежать этой дополнительной обработки.

Перед изменением столбца проверьте исходные данные и путь ингестии, а не исходите из того, что наблюдаемые данные без NULL всегда будут оставаться такими. [Практический пример оптимизации](/docs/ru/guides/clickhouse/performance-and-monitoring/query-optimization-example#nullable) показывает, как определить столбцы, содержащие значения NULL, и измерить влияние изменения схемы.

<span id="use-dictionary-encoding-for-repeated-values" />

**Используйте кодирование с использованием словаря для повторяющихся значений**

[`LowCardinality`](/docs/ru/reference/data-types/lowcardinality) использует кодирование с использованием словаря и часто эффективно для строковых столбцов, например со значениями status, кодами стран или другими измерениями, в которых уникальных значений значительно меньше, чем строк. Около 10 000 уникальных значений — полезная отправная точка для поиска кандидатов, а не жёсткий предел. Избегайте идентификаторов и других преимущественно уникальных столбцов и сравнивайте показатели до и после изменения типа.

Более подробные рекомендации см. в разделе [Выбор типов данных](/docs/ru/best-practices/select-data-types).

<div id="read-only-the-required-columns">
  ### Читайте только необходимые столбцы
</div>

Поскольку ClickHouse хранит данные по столбцам, выбор меньшего количества столбцов напрямую сокращает объём считываемых данных. Перечисляйте необходимые столбцы вместо `SELECT *`, особенно при работе с широкими таблицами или запросами, которые возвращают лишь небольшую часть данных из каждой строки.

Используйте `read_bytes` из [`system.query_log`](/docs/ru/reference/system-tables/query_log), чтобы сравнить объём данных, считанных до и после сокращения списка выбранных столбцов. Если значение `read_bytes` остаётся высоким, проверьте план запроса на наличие выражений, фильтров, JOIN или вложенных запросов, которым по-прежнему требуются дополнительные столбцы.

Например, если для панели мониторинга нужны только время посадки, тип оплаты и общая сумма, выбирайте эти столбцы, а не всю строку:

```sql theme={null}
SELECT
    pickup_datetime,
    payment_type,
    total_amount
FROM nyc_taxi.trips_small_inferred
WHERE pickup_datetime >= '2009-01-01'
  AND pickup_datetime < '2009-04-01'
LIMIT 1000;
```

Сравните этот запрос с запросом с тем же фильтром и ограничением, но с использованием `SELECT *`. Число возвращённых строк не изменится, однако `read_bytes` должен отражать меньший объём данных, считанных из столбцов.

<div id="align-the-data-layout-with-the-query">
  ## Согласуйте структуру данных с запросом
</div>

* **Используйте, когда:** селективный фильтр по-прежнему считывает много частей или гранул.
* **Изменение:** Согласуйте физическую структуру данных с фильтрами, используемыми в регулярно выполняемых запросах.
* **Проверьте:** Сравните части и гранулы, выбранные [`EXPLAIN indexes = 1`](/docs/ru/reference/statements/explain), затем проверьте `read_rows`, `read_bytes` и длительность.

<div id="start-with-the-ordering-key">
  ### Начните с ключа сортировки
</div>

Для [таблиц семейства `MergeTree`](/docs/ru/reference/engines/table-engines/mergetree-family/) ключ сортировки определяет порядок размещения строк на диске. По умолчанию он также служит первичным ключом, на основе которого строится [разреженный первичный индекс](/docs/ru/primary-indexes). В отличие от первичного ключа в OLTP-базе данных, первичный ключ ClickHouse не обеспечивает уникальность. Его преимущество в производительности заключается в том, что он позволяет ClickHouse пропускать гранулы, не соответствующие фильтрам запроса.

Отдавайте приоритет столбцам, которые часто используются в селективных фильтрах, и учитывайте их порядок в ключе. Группировка связанных значений также может улучшить сжатие. Если порядок группировки или сортировки в запросе соответствует ключу, ClickHouse может использовать оптимизации обработки данных в порядке сортировки для `GROUP BY` или `ORDER BY`.

Сравните части и гранулы, выбранные `EXPLAIN indexes = 1`, до и после тестирования другого ключа сортировки. Также сравните `read_rows`, `read_bytes` и длительность при одинаковых условиях. Подробные рекомендации см. в разделе [Выбор первичного ключа](/docs/ru/best-practices/choosing-a-primary-key).

В таблице примера используется `ORDER BY ()`, поэтому следующий селективный фильтр по дате не может исключать гранулы с помощью ключа сортировки:

```sql theme={null}
EXPLAIN indexes = 1
SELECT
    payment_type,
    count()
FROM nyc_taxi.trips_small_inferred
WHERE pickup_datetime >= '2009-01-01'
  AND pickup_datetime < '2009-04-01'
GROUP BY payment_type
SETTINGS
    use_query_condition_cache = 0,
    use_skip_indexes_on_data_read = 0;
```

<Note>
  В ClickHouse 25.9 и более поздних версиях эти настройки обеспечивают, что `EXPLAIN` отображает используемые индексы, а также исключаемые ими части и гранулы.
</Note>

Используйте этот вывод в качестве основы для сравнения. Чтобы завершить сравнение, выполните действия из раздела [Примените изменение ключа сортировки](/docs/ru/guides/clickhouse/performance-and-monitoring/query-optimization-example#apply-the-ordering-key-change) практического примера: создайте таблицу с ключом сортировки, включающим `pickup_datetime`, а затем выполните для неё тот же `EXPLAIN`. В разделе первичного ключа плана должно отображаться меньше выбранных гранул, прежде чем оценивать общее изменение по длительности или использованию памяти.

<Tip>
  [`PREWHERE`](/docs/ru/optimize/prewhere) может сократить объём считываемых значений столбцов, не изменяя количество обработанных строк. При включённой настройке `optimize_move_to_prewhere` (по умолчанию) ClickHouse автоматически перемещает подходящие условия из `WHERE` в `PREWHERE`. Прежде чем добавлять `PREWHERE` вручную, изучите план и оценивайте его влияние по `read_bytes`, а также `read_rows`.
</Tip>

<div id="evaluate-additional-indexing-and-data-layout-options">
  ### Оцените дополнительные варианты индексирования и структуры данных
</div>

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

<span id="partition-for-data-management-and-pruning" />

**Партиционирование для управления данными и отсечения**

[Партиционирование](/docs/ru/best-practices/choosing-a-partitioning-key) — это прежде всего механизм управления данными для таких операций, как хранение, перемещение и удаление. Оно может сократить объем работы при выполнении запроса, если фильтры позволяют ClickHouse исключать целые партиции, однако не должно быть основным способом ускорения запросов.

Например, месячные партиции позволяют удалять данные за целые месяцы, если срок хранения также задается по месяцам. Используйте партиционирование, только если ключ партиционирования соответствует требованиям к жизненному циклу данных или хорошо изученному шаблону доступа. Поддерживайте низкую мощность ключа: ключ высокой мощности создает множество частей, которые нельзя объединять между партициями, и может снизить производительность. Используйте `EXPLAIN indexes = 1`, чтобы убедиться, что запрос действительно отсекает партиции.

<span id="add-a-data-skipping-index-for-a-localized-filter" />

**Добавьте индекс пропуска данных для локализованного фильтра**

[Индекс пропуска данных](/docs/ru/best-practices/use-data-skipping-indices-where-appropriate) хранит метаданные, позволяющие ClickHouse не читать блоки, которые заведомо не соответствуют фильтру. Он наиболее полезен, когда ключ сортировки не поддерживает важный фильтр, а совпадающие значения достаточно локализованы внутри блоков.

Например, индекс bloom filter может помочь при поиске по равенству, когда в большинстве блоков отсутствует искомое значение. Используйте индексы пропуска данных после анализа типов данных и ключа сортировки. Индекс, который редко позволяет исключить блок, увеличивает затраты на хранение и вычисления, почти не сокращая объем работы. Протестируйте тип индекса и гранулярность на репрезентативных данных, затем используйте `EXPLAIN indexes = 1`, чтобы сравнить выбранные гранулы и проверить `read_rows`, `read_bytes` и длительность.

<span id="use-projections-selectively" />

**Используйте проекции избирательно**

[Проекции](/docs/ru/data-modeling/projections) хранят альтернативные структуры данных вместе с таблицей. Они могут предоставлять другой ключ сортировки или предварительно вычисленный результат, а ClickHouse может выбрать подходящую проекцию без необходимости явно указывать ее в запросе.

Например, проекция с сортировкой по `payment_type` может поддерживать регулярно используемый фильтр, который не поддерживает сортировка базовой таблицы. Используйте небольшое число проекций для важных шаблонов доступа, которые базовая сортировка не может эффективно обслуживать.

Проекции хранят дополнительные индексные данные или данные столбцов и требуют дополнительной работы при вставке и слиянии; проекция по всем столбцам дублирует хранимые в ней столбцы. Активное использование проекций также может увеличить объем работы, необходимый для выбора оптимальной проекции во время выполнения запроса. Для крупных развертываний со множеством различных шаблонов доступа обычно проще эксплуатировать меньшее число проекций или отдельные специализированные таблицы. При выборе между этими механизмами см. [Materialized views versus projections](/docs/ru/managing-data/materialized-views-versus-projections).

Добавьте альтернативную сортировку для запросов, фильтрующих по типу платежа и времени посадки, продолжая обращаться к исходной таблице:

```sql theme={null}
ALTER TABLE nyc_taxi.trips_small_inferred
ADD PROJECTION trips_by_payment_type
(
    SELECT
        payment_type,
        pickup_datetime,
        trip_distance,
        total_amount
    ORDER BY (payment_type, pickup_datetime)
);

ALTER TABLE nyc_taxi.trips_small_inferred
MATERIALIZE PROJECTION trips_by_payment_type;
```

Материализация проекции заполняет её существующими данными; при последующих вставках она обновляется автоматически. Повторите типичный запрос к исходной таблице и с помощью `EXPLAIN projections = 1` убедитесь, что ClickHouse выбирает проекцию и считывает меньше строк или байтов. Прежде чем широко применять этот подход, также оцените дополнительные затраты на вставку и место в хранилище.

<div id="precompute-repeatable-work">
  ## Предварительно вычисляйте повторяющиеся операции
</div>

* **Используйте, когда:** Одни и те же преобразования или агрегации регулярно занимают значительную часть времени выполнения запроса.
* **Изменение:** Перенесите повторяющиеся вычисления на этап ингестии, планового обновления или в специально созданную структуру данных.
* **Проверка:** Убедитесь, что запрос обрабатывает меньший объём результатов и выполняет меньше вычислений во время выполнения, а нагрузка на ингестию или обновление остаётся приемлемой.

Выбирайте вариант в зависимости от того, как должен поддерживаться результат и как к нему будет осуществляться доступ. Эти варианты не исключают друг друга:

| Когда вам нужно                                             | Начните с                                                       |
| ----------------------------------------------------------- | --------------------------------------------------------------- |
| Результаты, обновляющиеся по мере поступления данных        | [Incremental materialized view](#incremental-materialized-view) |
| Периодический пересчёт при допустимой неактуальности данных | [Refreshable materialized view](#refreshable-materialized-view) |
| Независимая схема, ключ сортировки или жизненный цикл       | [Специализированная таблица](#purpose-built-table)              |

В каждом разделе описана базовая реализация, основной эксплуатационный компромисс и способ проверить результат.

<div id="incremental-materialized-view">
  ### Incremental materialized view
</div>

Используйте [incremental materialized view](/docs/ru/materialized-view/incremental-materialized-view), когда необходимо поддерживать в актуальном состоянии регулярно применяемую фильтрацию, преобразование или агрегацию по мере поступления данных. Оно обрабатывает каждый новый вставленный блок и записывает преобразованный результат в целевую таблицу. Компромисс — дополнительная нагрузка при ингестии и необходимость явно указать целевую таблицу.

Например, панель мониторинга, которая регулярно подсчитывает поездки по дням, может считывать данные из небольшой агрегатной таблицы вместо группировки исходных данных для каждого запроса:

```sql theme={null}
CREATE TABLE nyc_taxi.trips_by_day
(
    pickup_date Date,
    trip_count UInt64
)
ENGINE = SummingMergeTree
ORDER BY pickup_date;

CREATE MATERIALIZED VIEW nyc_taxi.trips_by_day_mv
TO nyc_taxi.trips_by_day
AS SELECT
    toDate(assumeNotNull(pickup_datetime)) AS pickup_date,
    count() AS trip_count
FROM nyc_taxi.trips_small_inferred
WHERE pickup_datetime IS NOT NULL
GROUP BY pickup_date;
```

Выполните запрос к целевой таблице с `sum(trip_count)`, сгруппировав по `pickup_date`, чтобы строки, ожидающие фонового слияния, объединялись во время выполнения запроса. Представление обрабатывает только новые вставки, поэтому отдельно выполните дозагрузку существующих исходных данных. Проверьте изменения, сравнив длительность и количество прочитанных строк с исходной агрегацией, затем убедитесь, что дополнительные затраты на вставку приемлемы.

<div id="refreshable-materialized-view">
  ### refreshable materialized view
</div>

Используйте [refreshable materialized view](/docs/ru/materialized-view/refreshable-materialized-view), если допустимы слегка устаревшие результаты, а полный результат можно пересчитывать с приемлемой периодичностью. Оно повторно выполняет свой запрос по расписанию. Компромисс — между актуальностью результатов и стоимостью каждого обновления.

Например, отчёт может каждый час пересчитывать общие суммы поездок по типам оплаты:

```sql theme={null}
CREATE TABLE nyc_taxi.trips_by_payment_type
(
    payment_type Int64,
    trip_count UInt64
)
ENGINE = MergeTree
ORDER BY payment_type;

CREATE MATERIALIZED VIEW nyc_taxi.trips_by_payment_type_mv
REFRESH EVERY 1 HOUR
TO nyc_taxi.trips_by_payment_type
AS SELECT
    assumeNotNull(source.payment_type) AS payment_type,
    count() AS trip_count
FROM nyc_taxi.trips_small_inferred AS source
WHERE source.payment_type IS NOT NULL
GROUP BY payment_type;
```

Отчёт использует предварительно вычисленный целевой результат, а ClickHouse обновляет полный результат по расписанию. Проверьте изменение, сравнив длительность выполнения его запроса с исходной агрегацией, затем изучите [`system.view_refreshes`](/docs/ru/reference/system-tables/view_refreshes), чтобы убедиться, что длительность обновления, status и частота соответствуют рабочей нагрузке.

<div id="purpose-built-table">
  ### Специализированная таблица
</div>

Используйте специализированную таблицу, если для отдельной рабочей нагрузки требуются существенно иные схема, ключ сортировки или жизненный цикл. Она позволяет явно контролировать физическую структуру и может быть понятнее, чем поддерживать множество проекций. Это требует дополнительного хранилища и управления конвейером. Повторяющиеся JOIN или преобразования также можно перенести в конвейер ингестии, если это позволяют исходные данные и требования к актуальности. Подробные рекомендации по проектированию см. в разделах [Использование materialized views](/docs/ru/best-practices/use-materialized-views) и [Денормализация данных](/docs/ru/data-modeling/denormalization).

Например, создайте более узкую таблицу, упорядоченную для панели мониторинга, которая фильтрует поездки по типу оплаты и времени посадки:

```sql theme={null}
CREATE TABLE nyc_taxi.trips_for_payment_dashboard
ENGINE = MergeTree
ORDER BY (payment_type, pickup_datetime)
AS SELECT
    assumeNotNull(source.payment_type) AS payment_type,
    assumeNotNull(source.pickup_datetime) AS pickup_datetime,
    trip_distance,
    total_amount
FROM nyc_taxi.trips_small_inferred AS source
WHERE source.payment_type IS NOT NULL
  AND source.pickup_datetime IS NOT NULL;
```

Этот пример исключает значения ключа сортировки NULL и удаляет `Nullable` из этих двух целевых столбцов. Убедитесь, что такой подход соответствует требованиям к данным рабочей нагрузки. Панель мониторинга должна явно обращаться к этой таблице, а конвейер ингестии — поддерживать её в актуальном состоянии. Проверьте изменение, сравнив с запросом к исходной таблице число прочитанных строк и байтов, использование памяти и длительность. Учтите также дополнительные затраты на хранилище и обслуживание конвейера.

<div id="next-steps">
  ## Следующие шаги
</div>

При оценке изменений повторяйте исходные измерения в сопоставимых условиях. Убедитесь, что изменение сокращает объём целевой работы, не перенося узкое место в другое звено.

Перейдите к [практическому примеру оптимизации](/docs/ru/guides/clickhouse/performance-and-monitoring/query-optimization-example), чтобы ознакомиться с изменениями схемы и ключа сортировки, измеренными относительно исходного базового уровня.
