Vector и DataChunk: колоночный батч
В двух предыдущих уроках мы говорили о «векторах» как о порциях данных, движущихся по конвейеру, и установили, что порция — это около 2048 значений. Пора назвать структуры данных точно. У векторизованного движка DuckDB есть две фундаментальные структуры: Vector и DataChunk. Всё, что течёт между операторами, что обрабатывает каждый оператор, что возвращает запрос, — это DataChunk, составленный из Vector-ов.
Понять их устройство необходимо для всего остального в модуле. Физические типы векторов, selection vector, validity mask, push-based исполнение — всё это надстройки над Vector и DataChunk. Этот урок определяет обе структуры точно и показывает, как они соотносятся с колоночной природой DuckDB.
Vector: массив значений одной колонки
Vector — это массив значений одного типа, относящихся к одной колонке. Если у вас есть колонка amount типа INTEGER, то её вектор — это массив целых чисел: примерно 2048 подряд идущих значений amount.
Ключевые свойства Vector. Первое — однотипность: все значения вектора одного типа данных. Вектор amount — только целые, вектор name — только строки. Один вектор не смешивает типы. Второе — принадлежность одной колонке: вектор представляет фрагмент одной колонки, не нескольких. Третье — фиксированный максимальный размер: вектор содержит не более STANDARD_VECTOR_SIZE (2048) значений, обычно ровно столько, и меньше — только в последней, неполной порции.
Vector — это колоночная структура. В нём значения одной колонки лежат подряд, плотным массивом. Это прямое отражение того, что DuckDB — колоночная СУБД: и на диске колонка хранится по-колоночно, и в памяти при обработке она представлена вектором — массивом значений этой колонки.
Важно: Vector — это интерфейс, а не один-единственный способ хранения. Снаружи вектор всегда выглядит как «массив из N значений колонки», но физически внутри он может быть устроен по-разному: плоским массивом, одним повторяющимся значением, ссылкой на словарь. Эти физические формы — тема следующего урока. Пока достаточно понимать Vector как логическую единицу: фрагмент одной колонки, до 2048 значений одного типа.
DataChunk: набор векторов одной кардинальности
Отдельный Vector представляет одну колонку. Но запрос работает со строками, у которых много колонок. Структура, объединяющая колонки, — DataChunk.
DataChunk — это коллекция Vector-ов, по одному на каждую колонку, плюс одно общее число — кардинальность (cardinality), количество логических строк в этом чанке.
Главное свойство DataChunk — все его векторы имеют одну и ту же кардинальность. Если DataChunk содержит 1500 строк, то и вектор region, и вектор amount, и вектор qty — все содержат ровно 1500 значений. Это и делает DataChunk представлением набора строк: i-е значение каждого вектора в совокупности образует i-ю строку.
DataChunk — это и есть «порция», которую мы в прошлых уроках называли единицей движения по конвейеру. Точнее так: между операторами движутся DataChunk-и. Оператор получает на вход DataChunk (набор векторов до 2048 строк), обрабатывает каждый вектор плотным циклом, формирует выходной DataChunk, передаёт дальше. DataChunk — это «колоночный батч»: маленькая горизонтальная нарезка таблицы (до 2048 строк), представленная по-колоночно.
| Структура | Что представляет | Размер | Аналогия |
|---|---|---|---|
Vector | Фрагмент одной колонки | До 2048 значений одного типа | Столбец маленькой таблички |
DataChunk | Фрагмент таблицы, набор строк | Набор векторов, все одной кардинальности | Маленькая табличка целиком |
Почему колоночный батч, а не строчный
DataChunk хранит данные по-колоночно (набор векторов-колонок), а не по-строчно (массив записей). Это сознательный выбор, и он критичен для скорости.
Если бы батч хранился по-строчно — массивом записей, где каждая запись это {region, amount, qty} единым куском, — оператор «прибавить 1 к amount» был бы вынужден прыгать по памяти: значения amount лежали бы не подряд, а через region и qty. Прыжки по памяти убивают кэш процессора и делают невозможным SIMD.
Колоночный DataChunk хранит все amount подряд в одном векторе. Оператор «прибавить 1 к amount» проходит вектор amount плотным линейным циклом — идеально для кэша, идеально для SIMD. Колонки, которые оператору не нужны (region, qty), он просто не трогает — их векторы лежат отдельно.
Колоночность DataChunk — это продолжение колоночности всего DuckDB. На диске таблица хранится по колонкам. При чтении колонка превращается в вектор. DataChunk собирает векторы нужных колонок. Оператор обрабатывает каждый вектор плотным циклом. Колоночное представление сквозное — от storage-формата через буферы до структур исполнения, нигде не происходит дорогого «переворота» в строчный вид.
DataChunk — это также то, что DuckDB возвращает как результат запроса. Когда вы выполняете SELECT через Python API и получаете данные, под капотом результат сформирован как последовательность DataChunk-ов. Интеграция с Arrow, Pandas, Polars во многом эффективна именно потому, что колоночный DataChunk напрямую отображается на колоночные форматы этих библиотек без построчного переписывания (это разбирается в модуле про Python-экосистему).
Чанки в потоке: запрос — это последовательность DataChunk-ов
Таблица обычно гораздо больше 2048 строк. Поэтому запрос обрабатывает не один DataChunk, а поток чанков. Сканирование таблицы выдаёт DataChunk за DataChunk-ом: первые 2048 строк, следующие 2048, и так далее. Каждый чанк прогоняется через конвейер операторов, затем берётся следующий.
Внутри движка существует и понятие более крупной группировки чанков. DuckDB оперирует не только отдельными DataChunk-ами, но и наборами чанков — например, при буферизации данных между стадиями параллельного исполнения. В коде движка для размера таких наборов есть внутренняя константа PARTIAL_CHUNK_COUNT — порог порядка примерно 50 векторов. Важно правильно к ней относиться: это внутренняя деталь реализации, а не публичный API и не гарантированное число. Не стоит думать о ней как о жёстком «50 умножить на 2048» — это ориентировочный внутренний порог, который касается организации параллельного исполнения и буферизации; конкретное значение и сама роль константы — внутреннее дело движка и могут меняться между версиями.
STANDARD_VECTOR_SIZE (2048) — фундаментальная, стабильная характеристика движка: на ней держится размер вектора, и от неё считается, например, размер row group. А PARTIAL_CHUNK_COUNT — внутренняя константа реализации, описывающая порог порядка примерно 50 векторов для буферизации наборов чанков. Не путайте их статус: первое — то, что нужно знать и на что можно опираться; второе — деталь внутренней механики, упоминаемая для полноты картины, но не число, на которое стоит закладываться в рассуждениях.
Итог урока: Vector — массив до 2048 значений одной колонки одного типа; DataChunk — набор таких векторов, по одному на колонку, все с одной кардинальностью, то есть колоночный батч строк. Между операторами движутся DataChunk-и, запрос целиком — это поток DataChunk-ов от сканирования к результату. Колоночность этих структур сквозная и обеспечивает плотные циклы, дружелюбные к кэшу и SIMD. Всё дальнейшее в модуле — физические типы векторов, selection vector, validity mask, push-based модель — это детали устройства и движения именно Vector и DataChunk.
Попробуй сам
Задания на наблюдение и размышление.
- Выполните
SELECT current_setting('threads')и любой запрос сEXPLAIN ANALYZE. В выводе плана обратите внимание на счётчики обработанных строк у операторов — мысленно прикиньте, на сколькоDataChunk-ов по 2048 строк делится это число. - Объясните своими словами разницу между
VectorиDataChunk. Что из них представляет колонку, а что — набор строк? - Почему все векторы внутри одного
DataChunkобязаны иметь одинаковую кардинальность? Что сломалось бы, если бы векторregionсодержал 1500 значений, а векторamount— 1490? - Объясните, почему оператор «прибавить 1 к колонке amount» работает быстрее с колоночным
DataChunk, чем работал бы со строчным массивом записей. - Почему
PARTIAL_CHUNK_COUNTне стоит воспринимать как гарантированное «50 x 2048»? Чем его статус отличается от статусаSTANDARD_VECTOR_SIZE?