Перейти к содержанию

ClickHouse: введение

Зачем это DE?

ClickHouse — колоночная СУБД для аналитики, которая обрабатывает миллиарды строк за секунды. Если PostgreSQL — надёжный седан для OLTP, то ClickHouse — болид для OLAP. Data Engineer использует его для логов, метрик, аналитических витрин и real-time дашбордов.


ClickHouse vs PostgreSQL vs Greenplum

Параметр PostgreSQL Greenplum ClickHouse
Тип OLTP (строковая) OLAP (MPP) OLAP (колоночная)
1 млрд строк SELECT Минуты Секунды Миллисекунды
INSERT одной строки Быстро Медленно Медленно (батчами!)
Транзакции ACID ACID Eventual consistency
UPDATE/DELETE Быстро Средне Медленно (мутации)
Лучше для CRUD, API, транзакции DWH, SQL-аналитика Логи, метрики, real-time

Архитектура

Колоночное хранение

Text Only
Строковое (PostgreSQL):          Колоночное (ClickHouse):
┌──────┬─────┬────────┐          ┌──────────────────┐
│ id   │ name│ amount │          │ id: 1, 2, 3, ... │ ← столбец
├──────┼─────┼────────┤          ├──────────────────┤
│ 1    │ Ann │ 100    │          │ name: Ann, Bob...│ ← столбец
│ 2    │ Bob │ 200    │          ├──────────────────┤
│ 3    │ Cat │ 300    │          │ amount: 100, 200 │ ← столбец
└──────┴─────┴────────┘          └──────────────────┘

SELECT SUM(amount):              SELECT SUM(amount):
→ читает ВСЕ столбцы             → читает ТОЛЬКО amount
→ 3 столбца × N строк            → 1 столбец × N строк
                                  → сжатие 10-50x

Почему быстрее для аналитики:

  1. Читает только нужные столбцы (не все 100)
  2. Данные одного типа сжимаются в 10-50 раз (LZ4, ZSTD)
  3. Векторное выполнение: обработка пачками по 8192 строк

MergeTree — основной движок

MergeTree — основное семейство движков ClickHouse. ORDER BY определяет порядок хранения данных на диске и заменяет индекс.

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

SQL
CREATE TABLE events (
    event_date Date,
    event_time DateTime,
    user_id UInt64,
    event_type LowCardinality(String),  -- оптимизация для enum-like
    page_url String,
    duration_ms UInt32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)     -- партиции по месяцам
ORDER BY (event_date, user_id)         -- первичный ключ (сортировка на диске)
TTL event_date + INTERVAL 1 YEAR      -- автоудаление через год
SETTINGS index_granularity = 8192;     -- гранулярность индекса

ORDER BY — это индекс

В отличие от PostgreSQL, ClickHouse не имеет вторичных индексов в привычном смысле. ORDER BY определяет порядок хранения данных → это и есть primary index.

SQL
-- Быстро: фильтр по первым столбцам ORDER BY
SELECT COUNT(*) FROM events
WHERE event_date = '2025-01-15' AND user_id = 42;

-- Медленно: фильтр по столбцу НЕ из ORDER BY
SELECT COUNT(*) FROM events
WHERE page_url = '/checkout';  -- full scan

Порядок столбцов в ORDER BY критичен

ORDER BY (date, user_id) → фильтр по date быстрый, по user_id — только после date. Как составной индекс в PostgreSQL.


Вставка данных

Батчами, не по одной строке!

Python
# ❌ Медленно: INSERT по одной строке
for row in data:
    client.execute("INSERT INTO events VALUES", [row])

# ✅ Быстро: батч 10,000+ строк
client.execute(
    "INSERT INTO events VALUES",
    data,  # список кортежей или dict'ов
    types_check=True
)

Не делай INSERT по одной строке

ClickHouse создаёт парт (часть данных) на каждый INSERT. 1000 INSERT'ов → 1000 партов → деградация. Батчи от 10K строк.

Из файлов

SQL
-- Загрузка CSV
INSERT INTO events FORMAT CSVWithNames
-- (через clickhouse-client)
clickhouse-client --query="INSERT INTO events FORMAT CSVWithNames" < events.csv

-- Из S3
INSERT INTO events
SELECT * FROM s3(
    'https://s3.amazonaws.com/bucket/events/*.parquet',
    'key', 'secret',
    'Parquet'
);

Материализованные представления

В ClickHouse materialized view — это trigger на INSERT: каждая вставка в исходную таблицу автоматически агрегируется и записывается в целевую.

SQL
-- Исходная таблица (детальные события)
CREATE TABLE events_raw (
    event_date Date,
    user_id UInt64,
    event_type String,
    amount Decimal(12,2)
) ENGINE = MergeTree()
ORDER BY (event_date, user_id);

-- Целевая таблица (агрегат)
CREATE TABLE daily_revenue (
    event_date Date,
    total_amount AggregateFunction(sum, Decimal(12,2)),
    user_count AggregateFunction(uniq, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY event_date;

-- Materialized View — автозаполнение
CREATE MATERIALIZED VIEW mv_daily_revenue
TO daily_revenue
AS SELECT
    event_date,
    sumState(amount) AS total_amount,
    uniqState(user_id) AS user_count
FROM events_raw
GROUP BY event_date;

-- Чтение с финализацией агрегатов
SELECT
    event_date,
    sumMerge(total_amount) AS revenue,
    uniqMerge(user_count) AS users
FROM daily_revenue
GROUP BY event_date;

ReplacingMergeTree — дедупликация

SQL
CREATE TABLE users (
    user_id UInt64,
    name String,
    email String,
    updated_at DateTime
) ENGINE = ReplacingMergeTree(updated_at)  -- оставит последнюю версию
ORDER BY user_id;

-- INSERT дубли — ClickHouse в фоне оставит строку с max(updated_at)
-- Для гарантии при чтении:
SELECT * FROM users FINAL WHERE user_id = 42;
-- FINAL форсирует дедупликацию при чтении

Типы данных

Тип Описание Пример
UInt8/16/32/64 Целые без знака user_id UInt64
Int8/16/32/64 Целые со знаком amount Int32
Float32/64 Числа с плавающей точкой price Float64
Decimal(P,S) Точные числа amount Decimal(12,2)
String Строка (любая длина) name String
FixedString(N) Строка фиксированной длины country_code FixedString(2)
Date Дата (без времени) event_date Date
DateTime Дата и время (секунды) created_at DateTime
LowCardinality(T) Словарное кодирование status LowCardinality(String)
Nullable(T) Допускает NULL email Nullable(String)
Array(T) Массив tags Array(String)

LowCardinality вместо Enum

Для столбцов с малым числом уникальных значений (status, country) используй LowCardinality(String) — сжатие + скорость, но гибче Enum.


Python-клиент

Bash
pip install clickhouse-connect
Python
import clickhouse_connect

client = clickhouse_connect.get_client(
    host="localhost",
    port=8123,
    username="default",
    password=""
)

# Чтение
result = client.query("SELECT event_date, COUNT(*) FROM events GROUP BY event_date")
for row in result.result_rows:
    print(row)

# DataFrame
df = client.query_df("SELECT * FROM events WHERE event_date = '2025-01-15'")

# Вставка
client.insert("events", data, column_names=["event_date", "user_id", "event_type"])

Когда ClickHouse, когда PostgreSQL

Сценарий Выбор Почему
CRUD-API для приложения PostgreSQL Транзакции, UPDATE, индексы
Аналитика по 1 млрд строк ClickHouse Скорость, сжатие
Логи и метрики ClickHouse TTL, партиции, сжатие
DWH с Star Schema Greenplum / ClickHouse Зависит от нагрузки
Real-time дашборды ClickHouse Миллисекундные ответы
CDC + медленно меняющиеся данные PostgreSQL ACID, UPDATE

Что запомнить

Концепция Ключевое
Колоночное хранение Читает только нужные столбцы, сжатие 10-50x
ORDER BY = индекс Порядок столбцов определяет скорость фильтрации
Батчевая вставка Минимум 10K строк за INSERT
MergeTree Основной движок: партиции, TTL, ORDER BY
Materialized Views Автоагрегация при INSERT
ReplacingMergeTree Дедупликация по версии

Проверь себя


Источники