Проекции
Введение
ClickHouse предлагает различные механизмы ускорения аналитических запросов на больших объемах данных для сценариев в реальном времени. Одним из таких механизмов для ускорения ваших запросов является использование Проекций. Проекции помогают оптимизировать запросы, создавая переупорядочивание данных по интересующим атрибутам. Это может быть:
- Полное переупорядочивание
- Подмножество исходной таблицы с другим порядком
- Предварительно рассчитанная агрегация (аналогично материализованному представлению), но с порядком, соответствующим агрегации.
Как работают Проекции?
Практически проекцию можно рассматривать как дополнительную, скрытую таблицу по отношению к оригинальной таблице. Проекция может иметь другой порядок строк и, следовательно, другой первичный индекс, чем у оригинальной таблицы, и она может автоматически и инкрементно предварительно вычислять агрегированные значения. В результате использование Проекций предоставляет два "регулятора настройки" для ускорения выполнения запросов:
- Правильное использование первичных индексов
- Предварительное вычисление агрегатов
Проекции отчасти аналогичны Материализованным представлениям, которые также позволяют иметь несколько порядков строк и предварительно вычислять агрегации во время вставки. Проекции автоматически обновляются и синхронизируются с оригинальной таблицей, в отличие от Материализованных представлений, которые обновляются явно. Когда запрос нацелен на оригинальную таблицу, ClickHouse автоматически выбирает первичные ключи и выбирает таблицу, которая может сгенерировать тот же правильный результат, но требует считывания наименьшего объема данных, как показано на рисунке ниже:

Более умное хранение с _part_offset
Начиная с версии 25.5, ClickHouse поддерживает виртуальную колонку _part_offset
в проекциях, что предлагает новый способ определения проекции.
Теперь существует два способа определения проекции:
-
Хранить полные колонки (исходное поведение): Проекция содержит полные данные и может читаться напрямую, обеспечивая лучшую производительность, когда фильтры совпадают с порядком сортировки проекции.
-
Хранить только ключ сортировки +
_part_offset
: Проекция работает как индекс. ClickHouse использует первичный индекс проекции для нахождения совпадающих строк, но считывает фактические данные из базовой таблицы. Это снижает накладные расходы на хранение за счет немного большего ввода-вывода во время выполнения запроса.
Указанные подходы также могут быть комбинированы, храня некоторые колонки в проекции, а другие - косвенно через _part_offset
.
Когда использовать Проекции?
Проекции являются привлекательной функцией для новых пользователей, так как они автоматически поддерживаются по мере вставки данных. Более того, запросы могут быть отправлены в одну таблицу, где проекции используются, где это возможно, для ускорения времени отклика.
Это противоречит Материализованным Представлениям, где пользователю необходимо выбирать подходящую оптимизированную целевую таблицу или переписывать свой запрос, в зависимости от фильтров. Это увеличивает нагрузку на пользовательские приложения и увеличивает сложность на стороне клиента.
Несмотря на эти преимущества, у проекций есть некоторые неявные ограничения, о которых пользователи должны быть осведомлены и, следовательно, их следует развертывать экономно.
- Проекции не позволяют использовать разные TTL для исходной таблицы и (скрытой) целевой таблицы, тогда как материализованные представления допускают разные TTL.
- Легкие обновления и удаления не поддерживаются для таблиц с проекциями.
- Материализованные представления могут быть связаны: целевая таблица одного материализованного представления может быть исходной таблицей другого материализованного представления и так далее. Это невозможно с проекциями.
- Проекции не поддерживают соединения, тогда как Материализованные Представления поддерживают.
- Проекции не поддерживают фильтры (оператор
WHERE
), тогда как Материализованные Представления поддерживают.
Мы рекомендуем использовать проекции, когда:
- Требуется полное переупорядочивание данных. Хотя выражение в проекции может теоретически использовать
GROUP BY
, материализованные представления более эффективны для поддержания агрегатов. Оптимизатор запросов также будет более склонен использовать проекции, которые используют простое переупорядочивание, т.е.SELECT * ORDER BY x
. Пользователи могут выбрать подмножество колонок в этом выражении для уменьшения объема хранения. - Пользователи готовы к потенциальному увеличению объема хранения и накладным расходам на запись данных дважды. Проверьте влияние на скорость вставки и оцените накладные расходы на хранение.
Примеры
Фильтрация по колонкам, которые не входят в первичный ключ
В этом примере мы покажем, как добавить проекцию в таблицу. Мы также рассмотрим, как проекция может быть использована для ускорения запросов, которые фильтруют по колонкам, которые не находятся в первичном ключе таблицы.
Для этого примера мы будем использовать набор данных New York Taxi Data, доступный на sql.clickhouse.com, который отсортирован по pickup_datetime
.
Давайте напишем простой запрос, чтобы найти все идентификаторы поездок, за которые пассажиры дали водителям больше $200:
Обратите внимание, что поскольку мы фильтруем по tip_amount
, который не входит в ORDER BY
, ClickHouse пришлось выполнить полное сканирование таблицы. Давайте ускорим этот запрос.
Чтобы сохранить исходную таблицу и результаты, мы создадим новую таблицу и скопируем данные с помощью INSERT INTO SELECT
:
Чтобы добавить проекцию, мы используем оператор ALTER TABLE
вместе с оператором ADD PROJECTION
:
Необходимо после добавления проекции использовать оператор MATERIALIZE PROJECTION
, чтобы данные в ней были физически упорядочены и переписаны в соответствии с указанным выше запросом:
Теперь давайте снова запустим запрос, теперь, когда мы добавили проекцию:
Обратите внимание, насколько существенно мы смогли сократить время выполнения запроса и уменьшить количество просканированных строк.
Мы можем подтвердить, что наш запрос действительно использовал созданную нами проекцию, запросив таблицу system.query_log
:
Использование проекций для ускорения запросов по цене в Великобритании
Чтобы продемонстрировать, как проекции могут быть использованы для ускорения производительности запросов, давайте рассмотрим пример с использованием реального набора данных. Для этого примера мы будем использовать таблицу из нашего учебника по цены на недвижимость в Великобритании с 30.03 миллиона строк. Этот набор данных также доступен в нашей sql.clickhouse.com среде.
Если вы хотите увидеть, как была создана таблица и вставлены данные, вы можете обратиться к странице "Набор данных по ценам на недвижимость в Великобритании".
Мы можем выполнить два простых запроса на этом наборе данных. Первый список графств в Лондоне, где самые высокие цены, а второй рассчитывает среднюю цену для графств:
Обратите внимание, что, несмотря на то, что оба запроса выполняются очень быстро, произошло полное сканирование всей таблицы из 30.03 миллиона строк, из-за того, что ни town
, ни price
не были в нашем операторе ORDER BY
, когда мы создавали таблицу:
Давайте посмотрим, можем ли мы ускорить этот запрос, используя проекции.
Чтобы сохранить оригинальную таблицу и результаты, мы снова создадим новую таблицу и скопируем данные, используя INSERT INTO SELECT
:
Мы создаем и заполняем проекцию prj_oby_town_price
, которая производит добавочную (скрытую) таблицу с первичным индексом, упорядочивая по городу и цене, чтобы оптимизировать запрос, который перечисляет графства в конкретном городе для самых высоких цен:
Настройка mutations_sync
используется для принудительного выполнения синхронно.
Мы создаем и заполняем проекцию prj_gby_county
– дополнительную (скрытую) таблицу, которая инкрементно предварительно вычисляет агрегированные значения avg(price) для всех 130 графств Великобритании:
Если в проекции, как в проекции prj_gby_county
выше, используется оператор GROUP BY
, то базовый движок хранения для (скрытой) таблицы становится AggregatingMergeTree
, и все агрегатные функции преобразуются в AggregateFunction
. Это гарантирует правильную инкрементную агрегацию данных.
На рисунке ниже представлена визуализация основной таблицы uk_price_paid_with_projections
и ее двух проекций:

Если мы теперь снова запустим запрос, который перечисляет графства в Лондоне для трех самых высоких оплаченных цен, мы увидим улучшение производительности запроса:
Аналогично, для запроса, который перечисляет графства Великобритании с тремя самыми высокими средними ценами:
Обратите внимание, что оба запроса нацелены на оригинальную таблицу и что оба запроса привели к полному сканированию таблицы (все 30.03 миллиона строк были считаны с диска) до того, как мы создали две проекции.
Также обратите внимание, что запрос, который перечисляет графства в Лондоне для трех самых высоких оплаченных цен, считывает 2.17 миллиона строк. Когда мы использовали напрямую вторую таблицу, оптимизированную для этого запроса, с диска было считано только 81.92 тысячи строк.
Причина различия в том, что в настоящее время оптимизация optimize_read_in_order
, упомянутая выше, не поддерживается для проекций.
Мы проверяем таблицу system.query_log
, чтобы видеть, что ClickHouse автоматически использовал две проекции для двух вышеприведенных запросов (см. столбец проекций ниже):
Другие примеры
Следующие примеры используют тот же набор данных по ценам в Великобритании, сравнивая запросы с проекциями и без них.
Чтобы сохранить нашу оригинальную таблицу (и производительность), мы снова создаем копию таблицы, используя CREATE AS
и INSERT INTO SELECT
.
Создание Проекции
Давайте создадим агрегатную проекцию по измерениям toYear(date)
, district
и town
:
Заполните проекцию для имеющихся данных. (Без материализации проекция будет создана только для вновь вставленных данных):
Следующие запросы сравнивают производительность с проекциями и без. Чтобы отключить использование проекций, мы используем настройку optimize_use_projections
, которая по умолчанию включена.
Запрос 1. Средняя цена за год
Результаты должны быть одинаковыми, но производительность лучше во втором примере!
Запрос 2. Средняя цена за год в Лондоне
Запрос 3. Самые дорогие районы
Условие (date >= '2020-01-01') необходимо изменить так, чтобы оно соответствовало измерению проекции (toYear(date) >= 2020)
:
Снова результат одинаковый, но обратите внимание на улучшение производительности запроса для второго запроса.
Объединение проекций в одном запросе
Начиная с версии 25.6, основываясь на поддержке _part_offset
, представленной в предыдущей версии, ClickHouse теперь может использовать несколько проекций для ускорения одного запроса с несколькими фильтрами.
Важно отметить, что ClickHouse все еще считывает данные только из одной проекции (или основной таблицы), но может использовать первичные индексы других проекций для отсечения ненужных частей перед считыванием. Это особенно полезно для запросов, которые фильтруют по нескольким колонкам, каждая из которых потенциально может совпадать с другой проекцией.
В настоящее время этот механизм только отсекает целые части. Прореживание на уровне гранул еще не поддерживается.
Чтобы продемонстрировать это, мы определим таблицу (с проекциями, использующими колонки _part_offset
) и вставим пять примерных строк, соответствующих диаграммам выше.
Затем мы вставим данные в таблицу:
Примечание: таблица использует пользовательские настройки для иллюстрации, такие как гранулы на одну строку и отключенные слияния частей, которые не рекомендуются для использования в производственной среде.
Эта настройка приводит к следующему:
- Пять отдельных частей (по одной на вставленную строку)
- Один первичный индексный записей на строку (в основной таблице и каждой проекции)
- Каждая часть содержит ровно одну строку
С этой настройкой мы выполняем запрос, фильтруя по как region
, так и user_id
. Поскольку первичный индекс базовой таблицы построен из event_date
и id
, он здесь неэффективен, поэтому ClickHouse использует:
region_proj
для отсечения частей по регионуuser_id_proj
для дальнейшего отсечения поuser_id
Это поведение видно при использовании EXPLAIN projections = 1
, который показывает, как ClickHouse выбирает и применяет проекции.
Результаты EXPLAIN
(показаны выше) раскрывают логический план запроса, сверху вниз:
Номер строки | Описание |
---|---|
3 | Планирует чтение из основной таблицы page_views |
5-13 | Использует region_proj для определения 3 частей, где region = 'us_west', отсеивая 2 из 5 частей |
14-22 | Использует user_id_proj для определения 1 части, где user_id = 107 , дополнительно отсеивая 2 из 3 оставшихся частей |
В итоге, всего 1 из 5 частей считывается из основной таблицы. Объединяя анализ индексирования нескольких проекций, ClickHouse значительно сокращает объем считываемых данных, улучшая производительность при низких накладных расходах на хранение.