Learning Platform
Глоссарий Troubleshooting
Урок 06.07 · 24 мин
Средний
distributed-executionpageblockvectorization

Page и Block: колоночный формат и векторизация

Весь модуль мы говорили: операторы обрабатывают Page, данные текут по конвейеру Page за Page. Пора вскрыть Page — это самый «железный» уровень курса. Здесь мы доходим до того, как байты данных физически разложены в памяти воркера, почему эта раскладка колоночная и почему именно она делает Trino быстрым.

Этот урок — про Page и Block: структуру колоночного in-memory формата, виды блоков, dictionary-блоки и связь с векторизацией и кэшами процессора.


Page: единица передачи данных

Page — это единица передачи данных между операторами в Trino. Когда оператор отдаёт результат следующему, он отдаёт Page. Driver прогоняет данные через цепочку операторов именно Page-ами.

Но что внутри Page. Логически Page — это набор строк, скажем, до нескольких тысяч. Однако физически Page устроен не построчно. Page — это набор Block-ов, и каждый Block хранит данные одного столбца.

Page — набор Block-ов по столбцам
PageЕдиница передачи между операторами. Логически — несколько тысяч строк.
физически состоит из
Block: столбец AВсе значения столбца A для строк этого Page, лежат в памяти подряд.
Block: столбец BВсе значения столбца B для тех же строк, отдельный непрерывный блок.
Block: столбец CВсе значения столбца C, ещё один отдельный блок.

Вот это — суть. Если бы Page был построчным, в памяти лежало бы: строка 1 (orderkey, total, status), строка 2 (orderkey, total, status)… Значения разных столбцов чередовались бы. Trino делает иначе: сначала все orderkey подряд (Block A), потом все total подряд (Block B), потом все status подряд (Block C). Это колоночная раскладка: значения одного столбца лежат в памяти непрерывно. Trino — колоночный движок не только на диске (форматы Parquet, ORC), но и в памяти, во время исполнения.

Apache Arrow: Memory Layout — колоночный формат в памяти Arrow Memory Layout: буферы, bitmap и RecordBatch ClickHouse: векторизованное выполнение

Block: данные одного столбца

Block — это структура, хранящая значения одного столбца для строк одного Page. Block — кирпичик, из которого сложен Page.

Block хранит не только значения, но и информацию о null. У столбца значение может отсутствовать. Block держит отдельно «полезную нагрузку» (сами значения) и отметки, какие позиции — null. Так оператору не нужно для каждого значения проверять особый «маркер null» внутри данных — есть отдельная компактная структура null-флагов.

Физическая раскладка Block зависит от типа столбца. Для столбца фиксированной ширины (bigint — 8 байт на значение) Block — это, по сути, один непрерывный массив: значение i-й строки лежит по смещению i * 8. Для столбца переменной ширины (varchar) сложнее: один массив хранит все байты строк подряд, второй — смещения, где какая строка начинается. Но идея общая: значения столбца лежат компактно и непрерывно.

NOTE

Block в Trino неизменяемый (immutable). Оператор не модифицирует входной Block — он создаёт новый Block для выхода. Это упрощает многое: блок можно безопасно передавать между операторами и драйверами, не опасаясь, что кто-то его поменяет. Фильтрация, например, не вычёркивает значения из блока, а создаёт новый блок или выборку позиций.


Почему колоночность быстра: кэши процессора

Зачем колоночная раскладка в памяти. Ответ — на уровне «до железа», в устройстве процессора.

Процессор читает память не по одному байту. Он читает cache line — блок фиксированного размера, типично 64 байта, — и кладёт его в кэш (L1, L2). Когда программа обращается к адресу в памяти, процессор подтягивает в кэш всю 64-байтную линию вокруг этого адреса. Если следующее нужное значение оказалось в той же линии — оно уже в кэше, доступ мгновенный. Если нет — процессор ждёт новую линию из памяти, а это медленно.

Теперь сравним раскладки на операции «просуммировать столбец total по всем строкам Page».

Построчная раскладка. Значения total разбросаны: между total строки 1 и total строки 2 лежат orderkey и status. Процессор подтягивает cache line ради одного total — а вместе с ним в кэш попадают ненужные сейчас orderkey и status. Кэш забивается мусором, на каждое значение total — почти отдельная линия.

Колоночная раскладка. Все total лежат подряд в Block B. Процессор подтягивает cache line — и в ней сразу 8 значений total (если bigint, 8 байт). Восемь полезных значений за одно чтение памяти. Кэш используется полностью, обращений к памяти в разы меньше.

Cache line: построчно против колоночно
ПострочноЗначения столбца разбросаны. Cache line несёт одно нужное значение и мусор соседних столбцов.
Кэш забит лишнимВ кэш попадают столбцы, не нужные этой операции — кэш используется впустую.
колоночность лучше
КолоночноЗначения столбца подряд. Cache line несёт 8 нужных значений bigint сразу.
Кэш заполнен полезнымВ кэше только нужный столбец — кэш работает на полную.

Векторизация: батчи вместо строк

Колоночная раскладка раскрывает второй механизм скорости — векторизацию.

Trino — векторизованный движок: операторы обрабатывают данные батчами значений, а не по одной строке. Оператор filter не вызывается «для строки 1, потом для строки 2». Он получает Block столбца и прогоняет предикат по массиву значений в тесном цикле.

Почему это быстро. Во-первых, накладные расходы: вызов кода, проверки, ветвления — всё это происходит раз на батч, а не раз на строку. Во-вторых — тесный цикл по непрерывному массиву идеален для процессора: предсказуемый доступ к памяти, и компилятор (а в случае JVM — JIT) может применить SIMD — Single Instruction Multiple Data, инструкции, обрабатывающие несколько значений одной командой процессора.

Колоночность и векторизация работают в паре: вектор для обработки — это и есть Block, непрерывный массив значений столбца. Построчная модель такой батч-обработки не позволяет в принципе — там значения столбца не лежат подряд.

Trino написан на Java, и это накладывает рамки: JVM-векторизация ограничена тем, что умеет JIT-компилятор. В свежих релизах Trino добавляет и более явную векторизацию: например, движок детектит поддержку набора инструкций AVX-512 у процессора и использует его для ускорения сериализации блоков и подавления null. Но базовый принцип — обработка Block-ами в тесных циклах — работает независимо от этого.


Dictionary blocks: экономия памяти и CPU

Не все Block-и — простые массивы. Один из важных видов — dictionary block (словарный блок).

Идея — для столбца с малым числом уникальных значений при большом числе строк. Пример: столбец country на миллион строк, а уникальных стран — 50. Хранить миллион строковых значений 'Germany', 'France'… — расточительно: одни и те же строки повторяются тысячи раз.

Dictionary block хранит данные иначе, в двух частях:

  1. Словарь (dictionary) — компактный список уникальных значений: 50 названий стран, каждое один раз.
  2. Массив индексов (ids) — для каждой из миллиона строк маленькое целое число: индекс её значения в словаре.

Вместо миллиона строк — словарь из 50 строк плюс миллион маленьких целых. Экономия памяти кратная. Но выигрыш не только в памяти — он и в CPU. Оператор может работать прямо по словарю: чтобы применить функцию к столбцу, достаточно применить её к 50 значениям словаря, а не к миллиону строк. Один и тот же 'Germany' не пересчитывается тысячи раз.

Dictionary block: словарь плюс индексы
Обычный блокМиллион строк — миллион полных значений, многие повторяются.
словарное кодирование
Словарь: 50 значенийКаждое уникальное значение хранится один раз.
Индексы: миллион целыхДля каждой строки — маленькое целое, индекс в словаре.

Родственный механизм — RLE, run-length encoding: если в блоке подряд идёт много одинаковых значений, хранится значение и число повторов. Частный, но яркий случай — блок, где все значения одинаковы: хранится одно значение и счётчик. Dictionary-блоки и RLE особенно полезны в операциях вроде UNNEST и в join-ах, где данные естественно содержат повторы. Trino создаёт такие блоки автоматически, когда это выгодно, — отдельно их включать не нужно.

TIP

Dictionary-блоки — это пример того, как Trino экономит не только память, но и работу процессора: операция применяется к словарю (немного значений), а не к полному столбцу (много значений). Когда в EXPLAIN ANALYZE столбец с малой кардинальностью обрабатывается подозрительно быстро — вероятно, движок задействовал словарное кодирование.


Page и Block в общей картине

Соберём весь модуль воедино через Page. Distributed planning дал дерево стадий. Стадии разворачиваются в задачи на воркерах. Задачи — в драйверы, цепочки операторов. Операторы передают друг другу данные Page-ами. И вот теперь мы знаем, что такое Page изнутри: набор Block-ов, по одному на столбец, в колоночной раскладке. Колоночность даёт эффективное использование cache line процессора; векторизация — обработку Block-ов батчами с возможным SIMD; dictionary-блоки — экономию памяти и CPU на повторяющихся значениях. Между стадиями Page-и сериализуются и идут через exchange.

Page и Block — это фундамент, на котором стоит вся скорость Trino. MPP-распределённость раздаёт работу по кластеру; pipeline-исполнение гонит данные потоком; а колоночный формат Page с векторизованными операторами выжимает максимум из каждого ядра процессора. Вот теперь таксономия пройдена до самого железа.


Попробуй сам

Эффекты колоночного формата косвенно видны в EXPLAIN ANALYZE:

  1. Выполните EXPLAIN ANALYZE SELECT sum(totalprice) FROM tpch.sf10.orders. Запрос трогает один столбец из многих. Подумайте, как колоночность Block помогает: читается и обрабатывается только нужный столбец.
  2. Сравните SELECT sum(totalprice) FROM tpch.sf10.orders и SELECT * FROM tpch.sf10.orders LIMIT 1000000. Первый трогает один столбец, второй — все. Сопоставьте объёмы обработанных данных.
  3. Выполните агрегацию по столбцу малой кардинальности — SELECT orderstatus, count(*) FROM tpch.sf10.orders GROUP BY orderstatus (у orderstatus всего несколько значений). Это сценарий, где уместно словарное кодирование.
  4. Сформулируйте письменно: почему построчная раскладка хуже использует cache line процессора, чем колоночная, и как dictionary block экономит и память, и CPU.

Проверка знанийKnowledge check
Как устроен Page изнутри, почему колоночная раскладка в памяти быстрее построчной на уровне процессора, и как dictionary block экономит память и CPU?
ОтветAnswer
Page — единица передачи данных между операторами. Логически это батч строк, но физически Page устроен не построчно: Page — это набор Block-ов, и каждый Block хранит значения одного столбца для строк этого Page. Это колоночная раскладка: значения одного столбца лежат в памяти непрерывно, подряд. Trino — колоночный движок не только на диске, но и в памяти во время исполнения. Колоночная раскладка быстрее построчной из-за устройства процессора. Процессор читает память не по байту, а cache line — блоками типично по 64 байта — и кладёт их в кэш. При построчной раскладке значения нужного столбца разбросаны между значениями других столбцов: cache line несёт одно нужное значение и мусор соседних столбцов, кэш используется впустую. При колоночной раскладке значения столбца лежат подряд: одна cache line несёт сразу несколько нужных значений (например, 8 значений bigint), кэш заполнен полезными данными, обращений к памяти в разы меньше. Колоночность также раскрывает векторизацию: оператор обрабатывает Block батчами в тесном цикле по непрерывному массиву, накладные расходы идут раз на батч, и возможен SIMD. Dictionary block — особый вид Block для столбца с малым числом уникальных значений при большом числе строк: вместо повторения одних и тех же значений он хранит словарь уникальных значений (каждое один раз) и массив маленьких целых индексов (для каждой строки — индекс её значения в словаре). Это экономит память кратно, а ещё экономит CPU: оператор может применить функцию прямо к словарю — к немногим значениям, а не ко всему столбцу, и одно и то же значение не пересчитывается тысячи раз.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Как Page физически устроен внутри?

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

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

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

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