12. ClickHouse под капотом

Колоночные структуры, гранулы и векторные инструкции

ClickHouse под капотом: Почему он такой быстрый?

Если MySQL и PostgreSQL — это надежные «рабочие лошадки» для OLTP, то ClickHouse — это болид формулы-1 для OLAP (Online Analytical Processing). Когда нам нужно мгновенно агрегировать миллиарды строк, обычные реляционные базы «умирают» в попытках прочитать терабайты данных с диска.

Секрет ClickHouse не в магии, а в экстремальном следовании принципам Mechanical Sympathy. Он выжимает максимум из каждого такта CPU, каждой кэш-линии и пропускной способности SSD.


1. Колоночное хранение: Mechanical Sympathy в действии

В традиционных БД (MySQL/PostgreSQL) данные хранятся строками. Одна строка — один блок на диске.
В ClickHouse данные хранятся колонками. Все значения колонки UserAge лежат вместе, один за другим.

flowchart TD
    subgraph RowLayout["Строковый формат (OLTP, MySQL/PostgreSQL)"]
        R1["Row 1: [ID=1, Name='Alice', Age=30, Email='alice@ex.com']"]
        R2["Row 2: [ID=2, Name='Bob',   Age=25, Email='bob@ex.com']"]
    end

    subgraph ColumnLayout["Колоночный формат (ClickHouse)"]
        C_ID["Файл ID.bin: [1, 2, ...]"]
        C_Name["Файл Name.bin: ['Alice', 'Bob', ...]"]
        C_Age["Файл Age.bin: [30, 25, ...]"]
        C_Email["Файл Email.bin: ['alice@ex.com', 'bob@ex.com', ...]"]
    end

    classDef default fill:#1e293b,stroke:#334155,color:#f8fafc;
    classDef col fill:#312e81,stroke:#6366f1,color:#ffffff;
    class ColumnLayout col;
  

2. MergeTree: Сердце системы

Основной движок ClickHouse — MergeTree. Его архитектура напоминает LSM-деревья, используемые в NoSQL (см. 2. Key Value базы), но адаптирована под аналитику.

Гранулы и Разреженный индекс (Sparse Index)

ClickHouse не строит индекс на каждую строку (это бы заняло терабайты). Вместо этого данные разбиваются на Гранулы (Granules) — по умолчанию 8192 строки.

flowchart LR
    Idx0["Mark 0: ID=1"] --> Granule0["Гранула 0<br>(Строки 1..8192)"]
    Idx1["Mark 1: ID=8193"] --> Granule1["Гранула 1<br>(Строки 8193..16384)"]
    Idx2["Mark 2: ID=16385"] --> Granule2["Гранула 2<br>(Строки 16385..24576)"]

    classDef default fill:#1e293b,stroke:#334155,color:#f8fafc;
    classDef idx fill:#312e81,stroke:#6366f1,color:#ffffff;
    class Idx0,Idx1,Idx2 idx;
  

Индекс хранит значения первичного ключа только для начала каждой гранулы.

  • Как работает поиск: ClickHouse по индексу находит диапазон гранул, где могут быть данные, и читает их целиком.
  • Результат: Индекс занимает считанные мегабайты и полностью помещается в RAM даже для таблиц на триллионы строк.

Парты и Слияния (Merge Process)

Когда вы делаете INSERT, ClickHouse создает новый Парт (Part) — набор файлов на диске (по файлу на колонку).

flowchart TD
    Insert1["INSERT 1"] --> Part1["Парт 1<br>Неизменяемый"]
    Insert2["INSERT 2"] --> Part2["Парт 2<br>Неизменяемый"]

    Part1 --> Merger["Background Merge Process<br>Фоновое слияние"]
    Part2 --> Merger

    Merger --> BigPart["Укрупненный Парт<br>Отсортирован по ключу"]

    classDef default fill:#1e293b,stroke:#334155,color:#f8fafc;
    classDef merge fill:#312e81,stroke:#6366f1,color:#ffffff,stroke-width:2px;
    class Merger merge;
  

В фоне ClickHouse объединяет маленькие парты в большие, пересортировывая данные. Это позволяет поддерживать данные в порядке без блокировок на запись.


3. Сжатие данных

Поскольку данные в колонке однотипны (например, много дат или цен), они отлично сжимаются.

  • LZ4 / ZSTD: Стандартные алгоритмы сжатия общего назначения.
  • Delta / DoubleDelta: Хранение только разницы между соседними числами (идеально для временных меток).
  • Gorilla: Специальное сжатие для Float-чисел с плавающей точкой.

Сжатые данные не только экономят место, но и ускоряют работу: прочитать сжатые 100 МБ и распаковать их в CPU быстрее, чем прочитать 1 ГБ несжатых данных из-за ограничений пропускной способности шины PCIe и диска.


4. Векторное исполнение запросов

ClickHouse не интерпретирует запрос по одной строке (как это делает классический MySQL). Он оперирует блоками (векторами).
Если нужно выполнить price * 0.2, ClickHouse берет кусок колонки price (например, 65536 значений) и прогоняет их через цикл, оптимизированный под конвейер процессора. Это исключает накладные расходы на вызовы функций для каждой строки.


5. Практика в Go: ClickHouse-драйвер и Batching

Самая большая ошибка при работе с ClickHouse из Go — вставлять данные по одной строке.

Идиоматичный подход в Go — использование Batching.

Go
package main

import (
    "context"
    "log"
    "time"

    "github.com/ClickHouse/clickhouse-go/v2"
)

func main() {
    conn, err := clickhouse.Open(&clickhouse.Options{
        Addr: []string{"127.0.0.1:9000"},
        Auth: clickhouse.Auth{Database: "default"},
    })
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // Создаем Batch
    batch, err := conn.PrepareBatch(context.Background(), "INSERT INTO events (user_id, event_time, score)")
    if err != nil {
        log.Fatal(err)
    }

    // Набиваем батч данными в памяти (аллокация в куче Go)
    for i := 0; i < 100_000; i++ {
        err := batch.Append(uint32(i), time.Now(), float64(i)*0.5)
        if err != nil {
            log.Fatal(err)
        }
    }

    // Отправляем всё одним сетевым пакетом и одним "партом" на диск
    if err := batch.Send(); err != nil {
        log.Fatal(err)
    }
}

Итог

  1. Колоночность сводит I/O к минимуму.
  2. MergeTree обеспечивает эффективную запись и фоновую сортировку.
  3. Векторизация и SIMD делают вычисления невероятно быстрыми на уровне железа.
  4. В Go всегда используйте PrepareBatch, стремясь к размеру батча от 10к до 100к строк.

Мы разобрали, как ClickHouse доминирует в аналитике. Но что если нам нужна база, которая сочетает в себе реляционную мощь SQL и горизонтальное масштабирование NoSQL? Переходим к следующей главе: 13. NewSQL. CockroachDB.