Learning Platform
Глоссарий Troubleshooting
Урок 05.04 · 20 мин
Средний
ProjectionsPROJECTIONQuery rewritingMergeTreePre-aggregationFINAL

Проекции: автоматическая оптимизация

Проекция — это встроенный вариант MV, живущий внутри таблицы. Вместо отдельной target-таблицы проекция хранит дополнительную копию данных в каждом part, отсортированную или агрегированную по-другому. ClickHouse автоматически переключает запрос на проекцию, если она читает меньше гранул.


PROJECTION синтаксис

Проекция определяется внутри CREATE TABLE как часть DDL:

CREATE TABLE events (
    event_date Date,
    user_id UInt64,
    event_type String,
    duration_ms UInt32,

    -- Проекция: альтернативный порядок сортировки
    PROJECTION by_user (
        SELECT event_date, user_id, event_type, duration_ms
        ORDER BY user_id
    ),

    -- Проекция: агрегация по дате
    PROJECTION daily_agg (
        SELECT
            event_date,
            count() AS total_events,
            sum(duration_ms) AS total_duration
        GROUP BY event_date
        ORDER BY event_date
    )
) ENGINE = MergeTree()
ORDER BY (event_date, event_type);

Каждая проекция создаёт дополнительную копию данных в каждом part:

  • by_user — те же столбцы, отсортированные по user_id (вместо event_date, event_type)
  • daily_agg — агрегированные данные по event_date (неявно AggregatingMergeTree внутри)

Автоматическое переписывание запросов

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

-- Запрос по user_id: таблица отсортирована по (event_date, event_type)
-- Без проекции: full scan. С проекцией by_user: binary search по user_id
SELECT * FROM events WHERE user_id = 12345;
-- ClickHouse автоматически использует проекцию by_user

-- Запрос COUNT по дате: проекция daily_agg содержит готовый результат
SELECT event_date, count() FROM events GROUP BY event_date;
-- ClickHouse автоматически использует проекцию daily_agg

Переписывание прозрачно — запрос пишется к основной таблице, оптимизатор решает использовать проекцию самостоятельно. Результат идентичен.


Проверка использования проекции

EXPLAIN показывает, какую проекцию (или основную таблицу) выбрал оптимизатор:

EXPLAIN indexes = 1
SELECT event_date, count() FROM events GROUP BY event_date;

-- В выводе: ReadFromMergeTree (daily_agg)
-- Если проекция не используется: ReadFromMergeTree (без имени проекции)

Настройка force_optimize_projection = 1 заставляет ClickHouse выдать ошибку, если подходящая проекция существует, но не используется — полезно для отладки.


GROUP BY в проекции

GROUP BY в проекции неявно создаёт AggregatingMergeTree-подобное хранение внутри part:

PROJECTION daily_agg (
    SELECT
        event_date,
        count() AS total_events,
        uniq(user_id) AS unique_users
    GROUP BY event_date
    ORDER BY event_date
)

Каждый part хранит агрегированную версию своих данных по event_date. При merge — агрегаты пересчитываются. При чтении — ClickHouse объединяет агрегаты из всех parts (аналогично -Merge, но автоматически).


ORDER BY в проекции

ORDER BY в проекции становится первичным ключом проекции. Это определяет физический порядок данных в проекционном хранении и структуру sparse index:

-- Основная таблица: ORDER BY (event_date, event_type)
-- Проекция: ORDER BY user_id
PROJECTION by_user (
    SELECT event_date, user_id, event_type, duration_ms
    ORDER BY user_id
)

Запросы с фильтром WHERE user_id = X используют sparse index проекции — binary search за микросекунды вместо full scan.


Ограничение FINAL

Проекции не работают с модификатором FINAL:

-- FINAL отключает использование проекций
SELECT * FROM events FINAL WHERE user_id = 12345;
-- Проекция by_user НЕ будет использована
-- ClickHouse читает основную таблицу и применяет merge-on-read

FINAL заставляет ClickHouse применить merge-on-read (ReplacingMergeTree, CollapsingMergeTree). Этот процесс несовместим с проекциями, поскольку проекция может хранить данные в другом порядке, несовместимом с логикой merge.

WARNING

Если ваши запросы регулярно используют FINAL (например, с ReplacingMergeTree), проекции не дадут ускорения. Рассмотрите Materialized View с отдельной target-таблицей.


Storage tradeoff

Каждая проекция хранит дополнительную копию данных в каждом part:

-- Таблица с 2 проекциями:
-- Основные данные: 100 ГБ
-- Проекция by_user: ~100 ГБ (те же данные, другой порядок)
-- Проекция daily_agg: ~1 ГБ (агрегаты, значительно меньше)
-- Итого на диске: ~201 ГБ

Проекция без GROUP BY (альтернативный ORDER BY) хранит полную копию всех столбцов. Проекция с GROUP BY хранит только агрегаты — значительно компактнее.


Добавление проекции к существующей таблице

-- Добавить проекцию к существующей таблице
ALTER TABLE events ADD PROJECTION by_user (
    SELECT event_date, user_id, event_type, duration_ms
    ORDER BY user_id
);

-- Материализовать проекцию для существующих parts
ALTER TABLE events MATERIALIZE PROJECTION by_user;

ADD PROJECTION определяет проекцию для новых parts. MATERIALIZE PROJECTION пересчитывает проекцию для уже существующих parts — это тяжёлая операция.


Сравнение: проекция vs Materialized View

Проекция vs Materialized View
ProjectionВстроенная в partПроекция живёт внутри part-файлов основной таблицы. Автоматическое переписывание запросов. Нет отдельной target-таблицы. Не работает с FINAL. Backfill автоматический при MATERIALIZE PROJECTION.
Materialized ViewОтдельная таблицаMV записывает результат в отдельную target-таблицу. Ручной backfill. Работает с любым движком. Гибче: поддерживает JOIN, UNION, сложные трансформации. Работает с FINAL в target.
КритерийProjectionMaterialized View
ХранениеВнутри каждого partОтдельная таблица
BackfillMATERIALIZE PROJECTIONРучной INSERT INTO … SELECT
Query rewritingАвтоматическоеРучной SELECT FROM target
FINALНе работаетРаботает
JOIN в определенииНетДа
Storage overheadКопия данных per partОтдельная таблица
Лучше дляАльтернативный ORDER BYСложные агрегации, ETL
B-tree: страницы и внутренняя структура Bucketing в Spark: устранение shuffle при join

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Таблица events имеет PROJECTION daily_agg (SELECT event_date, count() GROUP BY event_date ORDER BY event_date). Запрос: SELECT event_date, count() FROM events WHERE event_type = 'click' GROUP BY event_date. Будет ли использована проекция?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7