ClickHouseSQLdata engineeringanalytics databaseаналитик данных

ClickHouse 2026: практический гайд по ENGINE, ARRAY JOIN, materialized views

2026-05-30 14 мин

ClickHouse — колоночная OLAP-СУБД из Яндекса (Open Source с 2016). В 2026 стала стандартом для real-time analytics в РФ: Avito, Wildberries, Ozon, Тинькофф, ВКонтакте, Yandex.Cloud — все используют CH в продакшне для дашбордов с миллиардами строк и sub-second latency.

Этот гайд — практические паттерны: какие ENGINE когда выбирать, как избежать ловушек FINAL, зачем нужны materialized views и ARRAY JOIN. С примерами SQL и подводными камнями.


Что узнаешь


Шаг 1: Чем ClickHouse отличается от Postgres

Postgres (row-store): оптимизирован для OLTP — частые UPDATE/DELETE, транзакции, JOIN сложных моделей. Аналитический SUM по 1B строкам — медленный.

ClickHouse (column-store): оптимизирован для OLAP — append-mostly, агрегации, фильтрация. Аналитический SUM по 1B строкам — секунды. Цена: нет UPDATE (только ALTER TABLE ... DELETE через mutations), нет полноценных JOIN, eventual consistency.

Postgres: запрос -> читает все колонки строки -> фильтрует
CH: запрос -> читает ТОЛЬКО колонки в SELECT -> компрессия x10

Когда CH:

Когда НЕ CH:

См. подборку 16 ClickHouse-вопросов с собесов — все паттерны ниже спрашивают на собесах в Яндекс/Авито.

Шаг 2: ENGINE = MergeTree — основа

Каждая таблица в CH должна иметь ENGINE. MergeTree — базовый.

create table events (
    event_time DateTime,
    event_date Date materialized toDate(event_time),
    user_id UInt64,
    event_type LowCardinality(String),
    amount Float64
)
engine = MergeTree
partition by toYYYYMM(event_date)
order by (event_date, user_id, event_time)
ttl event_date + interval 90 day;

Что здесь важно:

Шаг 3: ReplacingMergeTree — дедупликация

Если события могут дублироваться (at-least-once delivery):

create table user_state (
    user_id UInt64,
    subscription_tier String,
    updated_at DateTime,
    version UInt64
)
engine = ReplacingMergeTree(version)
order by user_id;
ReplacingMergeTree(version) — при merge оставляет запись с максимальным version для одинакового ORDER BY ключа.

Подвох: дедупликация происходит в фоне. Между merge'ами в таблице могут быть дубли.

-- ❌ Дубли возможны:
select * from user_state where user_id = 123;
-- 123 | basic | 2024-01-01 | 1
-- 123 | pro   | 2024-01-15 | 2

-- ✅ Правильный read (без FINAL):
select user_id, argMax(subscription_tier, version) as tier
from user_state
where user_id = 123
group by user_id;

Шаг 4: Почему FINAL это анти-паттерн (и что вместо)

SELECT ... FINAL форсит дедупликацию на лету. Удобно, но очень дорого — игнорирует индексы, scan всех parts:
-- ❌ Медленно (FINAL):
select user_id, balance from accounts final where user_id = 123;
-- Reads all parts, sort, merge

-- ✅ Быстро (argMax):
select user_id, argMax(balance, version) as balance
from accounts
where user_id = 123
group by user_id;

Правило: FINAL — только в ad-hoc запросах для проверки. В production-дашбордах и пайплайнах — argMax/anyLast + GROUP BY.

Шаг 5: Materialized views — INSERT-triggers

CH materialized views работают как INSERT-триггеры: при вставке в source таблицу CH автоматически выполняет SELECT и пишет в destination.

-- Source: сырые events
create table events (
    event_time DateTime,
    user_id UInt64,
    amount Float64
) engine = MergeTree order by event_time;

-- Destination: дневные агрегаты
create table daily_revenue (
    day Date,
    revenue AggregateFunction(sum, Float64)
)
engine = AggregatingMergeTree
order by day;

-- MV — триггер при INSERT в events
create materialized view daily_revenue_mv to daily_revenue as
select
    toDate(event_time) as day,
    sumState(amount) as revenue
from events
group by day;

-- INSERT'ы в events автоматически апдейтят daily_revenue
insert into events values ('2024-01-15 10:00:00', 1, 100.0);

-- Read с финализацией:
select day, sumMerge(revenue) as total_revenue
from daily_revenue
group by day;

Подводные:

Шаг 6: ARRAY JOIN — разворачивание массивов

CH нативно поддерживает массивы как колонки. Полезно для tags, properties, mtu paths.

create table user_events (
    user_id UInt64,
    event_time DateTime,
    tags Array(String),
    properties Map(String, String)
) engine = MergeTree order by event_time;

insert into user_events values
(1, '2024-01-15 10:00:00', ['organic', 'mobile', 'ru'], {'plan': 'pro', 'utm': 'fb'}),
(2, '2024-01-15 10:01:00', ['paid', 'desktop', 'us'], {'plan': 'basic'});

-- ARRAY JOIN — каждый элемент массива становится строкой:
select user_id, tag
from user_events
array join tags as tag;
-- 1 | organic
-- 1 | mobile
-- 1 | ru
-- 2 | paid
-- ...

-- Через колонку map:
select user_id, properties['plan'] as plan
from user_events;

ARRAY функции (hasAny, arrayMap, arrayFilter, arrayReduce, arraySort) — стандарт для event tracking.

Шаг 7: Skip indexes — точечная оптимизация

Когда primary key (ORDER BY) не покрывает все фильтры, добавь skip index:

alter table events add index idx_user_id user_id type minmax granularity 4;
alter table events add index idx_country country type set(100) granularity 4;
alter table events add index idx_tag_bf tags type bloom_filter(0.01) granularity 4;

Типы:

Skip indexes не альтернатива ORDER BY — они только помогают пропускать data parts, которые гарантированно не содержат данные.

Шаг 8: Distributed для шардинга

В кластере используется Distributed ENGINE для прозрачной агрегации:

-- На каждом шарде:
create table events_local on cluster '{cluster}' (...)
engine = ReplicatedMergeTree(...)
partition by ... order by ...;

-- На координаторе:
create table events on cluster '{cluster}' as events_local
engine = Distributed('{cluster}', currentDatabase(), events_local, sipHash64(user_id));

Запросы к events идут на все шарды параллельно, агрегируются на координаторе.

FAQ

CH vs PostgreSQL?

CH для analytics (OLAP), Postgres для transactions (OLTP). Это разные tools.

Можно UPDATE в ClickHouse?

Через ALTER TABLE ... UPDATE ... — это mutation, тяжёлая операция. В production используют CollapsingMergeTree или ReplacingMergeTree для логических updates.

Что быстрее: ClickHouse или BigQuery?

Зависит от паттерна. CH быстрее на per-query latency (sub-second). BQ удобнее на ad-hoc больших аналитических задачах через serverless. CH дешевле при предсказуемой нагрузке.

Стоит ли учить ClickHouse в 2026?

Да. В РФ — must-have для аналитика Senior+. Каждый второй стартап и middle компания используют. На собесах спрашивают в 80% случаев для DA/PA.

Где практиковать ClickHouse?

Есть ClickHouse Play — публичный playground. Или подними локально через Docker: docker run -d --name ch -p 8123:8123 clickhouse/clickhouse-server.

dbt работает с CH?

Да, есть dbt-clickhouse — full support. См. наш dbt-гайд.

Что дальше

Источники

ClickHouse окупается на втором дашборде. Open SQL-тренажёр — решай CH-задачи и через месяц будешь писать MV и аналитические DAU/Retention queries без поиска по докам.

Прокачай ClickHouse-SQL
480+ SQL-задач (включая 18 CH-specific), AI-разбор, бесплатный старт.
Открыть тренажёр →