Learning Platform
Глоссарий Troubleshooting
Урок 06.02 · 22 мин
Начальный
columnarrow-basedpredicate-pushdownprojection-pushdown

Один файл, два способа лежать

Представь таблицу с миллионом заказов и 30 колонками: order_id, customer_id, created_at, amount, currency, ещё 25 полей. Как эти данные положить на диск?

Вариант первый, естественный: пишем строку за строкой. Сначала весь первый заказ (все 30 значений подряд), потом второй, потом третий. Это row-based (построчное) хранение. Так работают CSV, JSON, Avro, OLTP-базы данных типа Postgres и MySQL.

Вариант второй, странный: пишем колонку за колонкой. Сначала все миллион значений order_id, потом все миллион значений customer_id, потом все created_at. Это columnar (колоночное) хранение. Так работают Parquet, ORC, ClickHouse, BigQuery, Snowflake (внутри).

Разница кажется косметической, но она определяет всю архитектуру современной аналитики.

Row vs Columnar — физическая раскладка

Одни и те же данные, два разных способа размещения на диске

Row-based: строки лежат целиком, одна за другой. Чтобы прочитать одну колонку — приходится пройти все строки. Зато добавить новую строку — это просто append в конец файла
Columnar: все значения колонки лежат вместе, одно за другим. Чтобы прочитать колонку — читаешь подряд непрерывный блок. Чтобы прочитать одну строку — нужно собрать значения из всех колонок

Почему это важно: типичные аналитические запросы

Посмотрим на реальный запрос:

SELECT customer_id, SUM(amount)
FROM orders
WHERE created_at >= '2026-05-01'
GROUP BY customer_id;

В row-based хранилище: чтобы посчитать сумму по клиентам, движок читает каждую строку целиком — все 30 колонок, хотя нам нужны только 3 (customer_id, amount, created_at). Если в таблице 100 GB данных, прочитать придётся все 100 GB.

В columnar: движок открывает только три файла-колонки и читает их. Если три колонки занимают 10 GB, читаются именно эти 10 GB. В 10 раз меньше I/O.

Это первое и главное преимущество колоночного хранения для аналитики: projection pushdown — читаем только нужные колонки.

Projection pushdown: читать только нужное

Термин происходит из проекции реляционной алгебры (SELECT col1, col2 FROM ...). “Pushdown” — потому что оптимизатор запроса “толкает” эту операцию вниз, ближе к storage layer, ещё до чтения данных.

В row-based проекция применяется только после чтения строк — мы прочитали всё, выбросили лишние колонки в памяти. В columnar — проекция применяется на уровне I/O: лишние колонки даже не открываются с диска.

import pyarrow.parquet as pq

# Читаем все колонки — медленно
df_full = pq.read_table("orders.parquet")

# Читаем только две — в 10-15 раз быстрее на широких таблицах
df_proj = pq.read_table("orders.parquet", columns=["customer_id", "amount"])

Predicate pushdown: пропускать ненужное

Второй фундаментальный механизм — predicate pushdown. Это про условие WHERE.

В Parquet/ORC данные хранятся блоками (row groups). Для каждого блока сохраняются statistics: min/max значение каждой колонки, count of nulls, иногда bloom filter. Когда движок видит WHERE created_at >= '2026-05-01', он проверяет stats каждого блока:

  • Блок A: min=2026-01-01, max=2026-02-28 -> весь блок не подходит -> пропускаем, не читаем
  • Блок B: min=2026-03-01, max=2026-04-30 -> не подходит -> пропускаем
  • Блок C: min=2026-05-01, max=2026-05-17 -> подходит -> читаем

На исторической таблице с данными за 5 лет запрос за последний месяц сканирует ~2% данных вместо 100%.

Predicate pushdown: skipping блоков по min/max

Запрос WHERE date >= '2026-05-01' пропускает блоки, не подходящие по statistics

Блок A: min=2026-01-01, max=2026-02-28. Не пересекается с условием WHERE date >= 2026-05-01. Движок пропускает чтение этого блока
Блок B: min=2026-03-01, max=2026-04-30. Не пересекается с условием. Пропускается
Блок C: min=2026-05-01, max=2026-05-17. Пересекается с условием WHERE — этот блок читается с диска

Компрессия: колонки сжимаются лучше

Это третий бесплатный бонус columnar. В одной колонке значения однородны: все currency — это USD, EUR, RUB и десяток других. Все country — короткий набор кодов. Все statuspending, paid, cancelled.

Однородные данные сжимаются великолепно:

  • Dictionary encoding: вместо строк хранится словарь {0:USD, 1:EUR, 2:RUB} + массив индексов [0, 0, 1, 0, 2, 0, ...]. Колонка из миллиона USD весит как байт + dictionary.
  • Run-length encoding (RLE): если значение повторяется, хранится пара (значение, количество). [USD, USD, USD, USD, EUR, EUR] -> [(USD,4), (EUR,2)].
  • Delta encoding для сортированных чисел: хранятся разности, не сами значения.
  • Поверх — стандартные алгоритмы (Snappy, ZSTD, GZIP).

В результате типичная аналитическая таблица в Parquet занимает в 5-20 раз меньше места, чем в CSV. На больших объёмах это означает реальные деньги: вместо 1 PB в S3 — 100 TB, разница в стоимости $20K/мес.

В row-based такая компрессия невозможна: рядом лежат 1001, 42, 150.00, USD — разные типы, разные распределения, словарь и RLE не работают.

Цена columnar: точечные операции

За всё надо платить. Колоночное хранение проигрывает в трёх сценариях:

1. Чтение одной строки. Запрос SELECT * FROM orders WHERE order_id = 1001 в row-based найдёт одну страницу с этой строкой и прочитает её. В columnar придётся открыть 30 файлов-колонок и из каждого достать одно значение. Дорого.

2. Запись отдельных строк (INSERT/UPDATE). Добавить одну строку в row-based — append в конец файла. В columnar — нужно дописать 30 разных колонок, перестроить statistics. На один INSERT — тяжёлая операция. Поэтому Parquet/ORC обычно пишутся батчами: накопили 100K строк, записали как row group.

3. Транзакционность. Изменить одну строку — практически невозможно без переписывания целого блока. Поэтому OLTP базы (Postgres, MySQL) — row-based, а OLAP (ClickHouse, BigQuery) — columnar.

Когда что выбирать

Простое правило: много колонок, мало читаемых за раз, агрегации, исторические данные -> columnar. Много мелких операций, точечные UPDATE/DELETE, транзакции -> row-based.

СценарийRowColumnar
Web app, INSERT/UPDATE per requestДаНет
Аналитический отчёт, агрегация миллиардов строкНетДа
Логи (append-only, фильтр по полям)ИногдаДа
Точечное чтение по PKДаНет
Сканирование 90% данныхНе разницаНе разница
NOTE

В реальности современные системы делают гибрид: HTAP-базы хранят данные в обоих представлениях, real-time идёт в row-store, потом мигрирует в columnar. Подробности в уроке 5.04 (HTAP) и в курсе clickhouse-course.

Row vs Columnar: физика на уровне байтов Векторизованное выполнение: как columnar engine обрабатывает миллиарды строк

Внутри columnar: Parquet и ORC

Когда говорят “колоночный формат” в data engineering — обычно имеют в виду Parquet (от Apache, чаще в Spark/Databricks/Snowflake) или ORC (от Apache Hive). Это два самых распространённых формата в data lake. Снаружи оба колоночные, внутри отличаются деталями row groups, encoding-ов, индексов. Разбираем оба отдельно в уроках 4.03 и 4.04.

ClickHouse, BigQuery и другие OLAP-движки используют свои собственные колоночные форматы внутри, недоступные снаружи. Но публикуют экспорт в Parquet — это de facto lingua franca data lake.

Реальный замер

Возьми любую публичную таблицу: например, NYC Taxi (1.6 млрд поездок). В CSV это ~250 GB. В Parquet — ~30 GB (8x компрессия). Запрос “сумма выручки за май 2026 в Манхэттене”:

  • CSV: full scan 250 GB, парсинг каждой строки -> 15-25 минут на одной машине
  • Parquet: predicate pushdown отбрасывает 99% блоков, projection отбрасывает 95% колонок -> читается ~50 MB -> 2-3 секунды

Это разница не в проценты, а в три порядка. Углубление в Parquet — следующий урок.

Попробуй сам

  1. Скачай любую открытую таблицу (NYC Taxi, например — https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page). Сохрани в CSV и Parquet. Сравни размеры на диске.
  2. В DuckDB выполни запрос SELECT passenger_count, AVG(fare_amount) FROM taxi GROUP BY passenger_count сначала на CSV, потом на Parquet. Замерь время. Объясни разницу.
  3. Открой Parquet-файл через pyarrow.parquet.ParquetFile(path).metadata. Посмотри, как разбит на row groups, какие statistics хранятся.
  4. Прикинь оценочную экономию места: у тебя таблица 10 TB в CSV. Перевели в Parquet с ZSTD. Сколько займёт? Сколько ты сэкономишь на S3 ($23/TB/мес)?
Проверка знанийKnowledge check
У вас есть таблица событий: 100 GB в CSV, 50 колонок, 2 млрд строк. Аналитик жалуется, что простой запрос SELECT user_id, COUNT(*) FROM events WHERE event_date = '2026-05-17' GROUP BY user_id выполняется 20 минут. Что произойдёт, если переложить данные в Parquet (с дефолтными настройками), и почему?
ОтветAnswer
Запрос ускорится в 50-200 раз. Причины: 1) projection pushdown — читаются только две колонки (user_id, event_date) вместо 50, это сразу 25-кратное сокращение I/O; 2) predicate pushdown — Parquet хранит min/max по event_date в каждом row group; если данные хоть как-то отсортированы по дате (или партиционированы по дню), скан сократится до нескольких процентов; 3) компрессия Parquet даёт ещё 5-10x по объёму на диске; 4) колоночные данные читаются и обрабатываются векторно — CPU работает быстрее. В итоге запрос превратится в 10-30 секунд.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Запрос SELECT customer_id, SUM(amount) FROM orders WHERE date >= '2026-05-01' GROUP BY customer_id. Таблица 100 GB, 30 колонок, в Parquet. Какие механизмы columnar storage позволяют выполнить его быстро?

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

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

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

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