Learning Platform
Глоссарий Troubleshooting
Урок 01.04 · 20 мин
Начальный
MergeTreeColumnar StorageSparse IndexMental ModelАрхитектура

Смена ментальных моделей

Самая частая ошибка при переходе на ClickHouse — применять модели мышления из PostgreSQL, MySQL или Snowflake. Эти системы спроектированы под разные паттерны доступа, и интуиция, выработанная при работе с ними, в ClickHouse часто вводит в заблуждение.

В этом уроке разберём пять фундаментальных сдвигов, которые нужно произвести в голове. Каждый из них объясняет не просто “как работает” та или иная функция, но “почему именно так” — и какие практические последствия это имеет для разработки и производительности.


Сдвиг 1: От строчного к колоночному хранению

Как работает PostgreSQL. Строки в PostgreSQL хранятся в блоках по 8 КБ (heap pages). Каждый блок содержит несколько строк целиком. Запись (id=1, name="Alice", age=30, city="Moscow", revenue=5000, status="active", ...) хранится как единый непрерывный блок байт. Чтение любого поля строки = чтение всей строки из heap page.

Как работает ClickHouse. Столбец name хранится отдельно от столбца age, который хранится отдельно от revenue. Каждый столбец — это свой файл на диске (или группа файлов для сжатых данных). Чтение значения revenue для всех строк = чтение только файла revenue.bin. Файл name.bin при этом не читается совсем.

Строчное vs Колоночное хранение
Таблица orders (3 строки, 5 столбцов)
Строчное хранение (PostgreSQL)5 столбцов на строкуСтрочное хранение: Row 1 = [id=1, user_id=101, amount=500, date=2026-01-01, status=paid]. Для SELECT amount FROM orders нужно прочитать все 5 полей каждой строки, даже если нужен только amount.
Колоночное хранение (ClickHouse)1 файл на столбецКолоночное хранение: файл amount.bin = [500, 800, 200]. Для SELECT avg(amount) читается только amount.bin — 3 значения. Файлы id.bin, user_id.bin, date.bin, status.bin не открываются вообще.

Практическое следствие. Запрос SELECT avg(amount) FROM orders на 100-колоночной таблице:

  • PostgreSQL: читает все 100 столбцов каждой строки (100% I/O)
  • ClickHouse: читает только файл amount.bin (1% I/O)

Это объясняет, почему ClickHouse в 10–100 раз быстрее PostgreSQL для аналитических запросов с агрегацией по нескольким столбцам. При этом для запросов SELECT * (все столбцы) или точечных SELECT ... WHERE id = 123 — преимущество колоночного хранения исчезает.

Row vs Columnar: физические механизмы выигрыша Vectorized Execution в SQL Internals: батчами, а не по одному

Сдвиг 2: От мутабельных строк к иммутабельным частям

Как работает PostgreSQL. UPDATE меняет строку на месте: MVCC создаёт новую версию строки, старая помечается “мёртвой” и удаляется VACUUM. INSERT добавляет строку в heap page. DELETE помечает строку как удалённую. Всё происходит по-строчно, немедленно, с transactional гарантиями.

Как работает ClickHouse. INSERT создаёт новую иммутабельную часть (part) на диске. Часть — это директория с набором файлов для каждого столбца, минимальными/максимальными значениями первичного ключа и метаданными. После создания часть никогда не изменяется. UPDATE/DELETE не модифицируют существующие части — они создают специальные мутационные инструкции, которые выполняются асинхронно.

Жизненный цикл частей в MergeTree

INSERT 1000 строк → создаётся часть 20260101_1

INSERT 1000 строк: ClickHouse создаёт новую часть на диске. Имя части формируется из диапазона первичного ключа: 20260101_20260101_1_1_0. Часть содержит файлы: id.bin, amount.bin, date.bin, primary.idx.
асинхронный merge

Часть 20260101_1 (1000 строк)

Фоновый merge: ClickHouse периодически объединяет несколько маленьких частей в одну большую. Слияние происходит в фоновом потоке. Данные сортируются по первичному ключу, дубликаты (если используется ReplacingMergeTree) — дедуплицируются.

Часть 20260101_2 (500 строк)

Вторая часть создана следующим INSERT. Merge объединит обе части в одну, отсортированную по первичному ключу. После слияния старые части удаляются.
merge завершён

Merged часть 20260101_1_2 (1500 строк)

Merged часть 20260101_1_2: содержит 1500 строк, отсортированных по первичному ключу. Имя части меняется: последний сегмент (уровень слияния) увеличивается. Старые части удаляются после merge.

Практическое следствие. При INSERT 1 миллиона строк в ClickHouse реально происходит следующее: создаётся одна часть с 1 миллионом строк. Данные уже доступны для чтения (часть видима читателям). Фоновые потоки merge периодически объединяют накопившиеся маленькие части в большие. Это eventual consistency относительно оптимальной структуры хранения — не относительно видимости данных.

Главное заблуждение. “ClickHouse медленно INSERT-ит, потому что MergeTree”. Нет. MergeTree INSERT очень быстрый, потому что просто создаёт директорию с файлами. Медленная часть — merge в фоне. Правило: не делать слишком много маленьких INSERT (много частей → много merge). Рекомендуемый размер batch: не менее 1000 строк, оптимально — 100 000 строк и больше.


Сдвиг 3: От B-tree индекса к sparse-индексу

Как работает PostgreSQL. B-tree индекс создаётся для отдельного столбца (или нескольких). Каждая строка таблицы имеет запись в B-tree. Точечный поиск WHERE id = 12345 — O(log n) операций по дереву. Индекс может занимать столько же места, сколько сами данные.

Как работает ClickHouse. PRIMARY KEY в MergeTree не создаёт запись для каждой строки. Вместо этого создаётся sparse index (разреженный индекс) — файл primary.idx, который хранит одну запись на каждые 8192 строки (один granule). Granule — это минимальная единица чтения данных.

primary.idx (содержимое для таблицы с сортировкой по date):
2026-01-01  → offset granule 0   (строки 0–8191)
2026-01-08  → offset granule 1   (строки 8192–16383)
2026-01-15  → offset granule 2   (строки 16384–24575)
...

При запросе WHERE date BETWEEN '2026-01-08' AND '2026-01-10' ClickHouse:

  1. Бинарным поиском по primary.idx находит диапазон granule (granule 1–2)
  2. Читает только соответствующие granule из колоночных файлов
  3. Применяет фильтр для удаления строк за пределами диапазона

Практическое следствие. Sparse index занимает в 8192 раз меньше места, чем B-tree. Файл primary.idx для таблицы с миллиардом строк весит примерно 8 МБ и полностью помещается в память. Но: точечный поиск WHERE id = 12345 на несортированной по id таблице — плохая идея в ClickHouse. Придётся сканировать весь диапазон granule.

TIP

Правило выбора первичного ключа в ClickHouse: первичный ключ должен отражать паттерн фильтрации запросов, а не уникальность строк. Если 90% запросов фильтруют по event_date — это кандидат на первый столбец первичного ключа. Если 80% запросов добавляют фильтр по user_id — он идёт вторым.


Сдвиг 4: От транзакционной изоляции к eventual merge consistency

Как работает PostgreSQL. BEGIN → операции → COMMIT. Транзакция видит консистентный снапшот данных на момент её начала (MVCC snapshot isolation). Конкурирующие транзакции не видят незакоммиченных изменений. ROLLBACK полностью откатывает все операции транзакции.

Как работает ClickHouse. Нет BEGIN/COMMIT. Каждый INSERT атомарен на уровне одного batch — либо весь batch записан (часть создана), либо нет (часть удалена при ошибке). Между INSERT нет явной транзакционной границы.

Читатели всегда видят все завершённые части. Если два INSERT выполняются одновременно, создаются две части. Читатель видит данные из обеих частей немедленно (до фонового merge). Порядок чтения из разных частей не гарантирован — ClickHouse объединяет потоки из нескольких частей для выполнения запроса.

Eventual merge consistency означает: данные логически верны сразу после INSERT (все строки видимы читателям). Физическая оптимизация (слияние частей) происходит позже, асинхронно. Это не eventual consistency в смысле “данные могут быть не актуальны” — это eventual consistency в смысле “физическая структура хранения оптимизируется со временем”.

Практическое следствие. Не полагайтесь на ClickHouse для сценариев, требующих строгой транзакционности: “сначала обновить баланс, потом записать транзакцию”. Для таких сценариев PostgreSQL остаётся правильным выбором. ClickHouse — это аналитический слой поверх оперативных данных, не замена OLTP-системы.

Уровни изоляции в PostgreSQL: что реально обеспечивает СУБД

Сдвиг 5: От нормализации к денормализации

Как работает PostgreSQL. Нормализация — стандартная практика: orders → order_items → products → categories. JOIN выполняются через B-tree индексы, что позволяет эффективно соединять таблицы по первичному/внешнему ключу.

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

Вместо:

orders (order_id, user_id, date)
order_items (item_id, order_id, product_id, quantity, price)
products (product_id, name, category_id)
categories (category_id, name)

В ClickHouse эффективнее:

fact_order_events (
    order_id, user_id, date,
    product_id, product_name, product_category,
    quantity, price, revenue
)

Практическое следствие. При проектировании ClickHouse-схемы спросите себя: “Какие запросы мы будем выполнять чаще всего?” — и разработайте денормализованную схему, которая позволяет выполнить 80% запросов без JOIN или с минимальным количеством JOIN.

ClickHouse имеет механизмы для JOIN (HASH JOIN, MERGE JOIN), и они работают. Но если вы обнаруживаете, что каждый запрос соединяет 4–5 таблиц — это сигнал пересмотреть схему.

Row-store vs Column-store: физика чтения и сжатия Денормализация: когда и зачем нарушать нормальные формы
NOTE

Материализованные представления (Materialized Views) — альтернатива денормализации. Исходные данные хранятся нормализованно, преаггрегированные результаты — в отдельных MV-таблицах. Это компромисс: сложнее поддерживать, зато не нужно денормализовать при вставке. Подробно рассматривается в Phase 55 курса.


Сводная таблица ментальных сдвигов

КонцепцияPostgreSQLClickHouseПрактическое следствие
ХранениеСтроки в heap pagesСтолбцы в отдельных файлахSELECT по 3 из 100 столбцов — в 33 раза меньше I/O
Изменение данныхUPDATE/DELETE в местеИммутабельные части + mergeИзбегайте мелких INSERT, используйте batch
ИндексB-tree на каждую строкуSparse index 1 запись на 8192 строкPrimary key = паттерн фильтрации, не уникальность
ТранзакцииACID BEGIN/COMMITАтомарный INSERT + eventual mergeНе используйте для OLTP-сценариев
СхемаНормализованная (JOIN)Денормализованная (плоские таблицы)Проектируйте схему под запросы, а не под сущности

Ключевые выводы

  1. Колоночное хранение — читается только нужный столбец; преимущество растёт с количеством столбцов в таблице
  2. Иммутабельные части — INSERT быстрый (создаёт директорию), merge медленный (фоновый); используйте batch INSERT от 100 000 строк
  3. Sparse index — 1 запись на 8192 строк; первичный ключ должен отражать паттерн фильтрации запросов
  4. Нет транзакций — eventual merge consistency; ClickHouse не замена OLTP-системе
  5. Денормализация — JOIN дорог в ClickHouse; проектируйте плоские таблицы под конкретные запросы
Прогресс модуля
0 из 4