Learning Platform
Глоссарий Troubleshooting
Урок 17.03 · 24 мин
Продвинутый
capstonelarger-than-memoryexternal-aggregationmemory-control

Out-of-core агрегация датасета больше RAM

В капстоун-датасете 41 миллион поездок и несколько десятков гигабайт — заведомо больше оперативной памяти типичного ноутбука. При этом в прошлом уроке витрина mart_daily_zone посчиталась без ошибок. Это не случайность: DuckDB умеет out-of-core (larger-than-memory) исполнение — обрабатывать данные, которые не помещаются в RAM, спиллингом промежуточных результатов на диск.

Этот урок — про то, как именно это работает на нашей агрегации. Разберём механику external hash aggregation, научимся управлять memory_limit и temp_directory и увидим, как контролировать спилл осознанно. Модуль про larger-than-memory дал теорию; здесь мы применяем её к конкретному расчёту витрины.


Почему агрегация рискует не поместиться в память

Витрина mart_daily_zone группирует поездки по (trip_date, borough, zone_name). Чтобы посчитать sum и avg по группам, движку нужна хеш-таблица: ключ — комбинация группировки, значение — накапливаемые агрегаты. Эта хеш-таблица растёт с числом уникальных групп.

Прикинем масштаб. Поездки за два с лишним года, около 260 зон города: число групп (дата, зона) — это сотни тысяч записей. Сама хеш-таблица групп для нашей витрины невелика. Но опасность в другом — в промежуточных данных пайплайна. Скан 41 миллиона строк, hash join поездок со справочником зон, сортировки — всё это держит в памяти векторы и буферы. На запросе с тяжёлым join или агрегацией по миллионам групп пиковая память легко превышает RAM.

Что занимает память при агрегации витрины
Скан raw ParquetЧтение 41 млн строк батчами по 2048 значений. Сами по себе батчи невелики, но это вход для всего остального.
Hash join со справочникомBuild-сторона join держит хеш-таблицу зон в памяти. Для маленького справочника это немного, для больших правых таблиц — может быть много.
Hash aggregate по группамХеш-таблица групп: ключ — комбинация группировки, значение — накапливаемые sum/avg/count. Растёт с числом уникальных групп.

Вопрос не «поместится ли датасет в память» — целиком 41 миллион строк туда и не загружается, DuckDB обрабатывает поток. Вопрос в том, поместятся ли структуры состояния — хеш-таблицы join и агрегации. Когда не помещаются, включается external-механизм.


External hash aggregation: как это работает

Когда хеш-таблица агрегации перестаёт помещаться в memory_limit, DuckDB не падает с ошибкой памяти. Он переходит на external hash aggregation — алгоритм, который досчитывает группы с использованием диска.

Идея — партиционирование по хешу ключа группировки. Движок делит пространство групп на партиции по хеш-значению ключа. Партиции, которые не помещаются в память, целиком сбрасываются (спиллятся) на диск во временные файлы. Затем партиции обрабатываются по одной: партиция загружается с диска обратно, по ней досчитываются агрегаты, результат отдаётся дальше. Так как одна партиция много меньше всего набора, она помещается в память, даже когда весь набор групп — нет.

External hash aggregation: партиционирование и спилл
Поток строк агрегацииСтроки приходят в оператор агрегации. Каждая попадает в партицию по хешу ключа группировки.
партиционирование по хешу ключа
Партиции в памятиПартиции, которые помещаются в memory_limit, остаются в RAM и агрегируются сразу.
Партиции на дискеПартиции сверх memory_limit спиллятся во временные файлы в temp_directory. Потом загружаются по одной и досчитываются.
досчёт по одной партиции
Результат агрегацииВсе группы посчитаны. Запрос завершился корректно, несмотря на то что весь набор групп в память не помещался.

Тот же принцип работает для join (external hash join спиллит обе стороны, обрабатывает по партициям) и для сортировки (external merge sort). Все три механизма курс разбирал в модуле про larger-than-memory; в капстоуне они срабатывают сами, когда расчёт витрины упирается в лимит памяти.

NOTE

Спилл не бесплатен: запись и чтение временных файлов медленнее работы в RAM. External-режим — это про корректность («запрос завершится»), а не про скорость. Цель тюнинга — не запретить спилл любой ценой, а не дать ему случаться там, где данные могли бы поместиться в память.


Управление памятью: memory_limit и temp_directory

Поведением out-of-core управляют две настройки. Зададим их для капстоуна осознанно.

memory_limit — потолок памяти движка. По умолчанию DuckDB берёт около 80% физической RAM. Это и есть граница, после которой начинается спилл. temp_directory — папка для временных файлов спилла.

-- Капстоун-расчёт на ноутбуке с 16 ГБ RAM
SET memory_limit = '10GB';                    -- потолок памяти движка
SET temp_directory = '/fast-ssd/duck_tmp';    -- спилл-файлы на быстрый SSD
SET threads = 8;                              -- степень параллелизма

-- Тяжёлый расчёт: материализуем витрину из 41 млн поездок
CREATE OR REPLACE TABLE mart_daily_zone_mat AS
FROM mart_daily_zone;

-- Результат:
-- 287_044 строк витрины посчитано.
-- Если памяти не хватило — в /fast-ssd/duck_tmp появлялись
-- временные файлы спилла, но запрос завершился корректно.

Два правила для temp_directory. Первое — это должен быть быстрый диск (SSD/NVMe): под спилл-нагрузкой скорость временного диска становится скоростью запроса. Спилл на медленный HDD превращает out-of-core расчёт в очень долгий. Второе — на нём должно хватать места: спилл-файлы могут занять объём, сравнимый с промежуточными данными запроса.

Существенно и распределение памяти по потокам. Бюджет memory_limit делится между threads рабочими потоками. Грубая прикидка: агрегационно-тяжёлой нагрузке нужно порядка 1-2 ГБ на поток, join-тяжёлой — порядка 3-4 ГБ. Если memory_limit поделить на слишком большое число потоков, каждому достанется мало, и спилл начнётся рано. Иногда уменьшение threads при фиксированном memory_limit ускоряет тяжёлый запрос — каждому потоку достаётся больше памяти, и спилла меньше.


Наблюдаем за спиллом

Тюнинг вслепую бесполезен — нужно видеть, спиллит запрос или нет. Главный инструмент — EXPLAIN ANALYZE: он исполняет запрос и показывает фактическое дерево операторов с временем и числом строк.

EXPLAIN ANALYZE
CREATE OR REPLACE TABLE mart_daily_zone_mat AS
FROM mart_daily_zone;

Признаки и диагностика:

  • Появление временных файлов в temp_directory во время запроса — прямой признак, что спилл идёт.
  • Самый дорогой по времени оператор в выводе EXPLAIN ANALYZE — обычно hash aggregate или hash join. Если он неожиданно долгий, а данные больше memory_limit, он почти наверняка работает в external-режиме.
  • Эффект изменения memory_limit. Если поднять лимит и запрос резко ускорился — он спиллил, и теперь данные поместились. Если время не изменилось — спилла не было, и узкое место в другом.
Цикл тюнинга out-of-core расчёта
ЗапуститьПрогнать расчёт под EXPLAIN ANALYZE с текущими memory_limit и threads.
Найти спиллПроверить temp_directory на временные файлы и найти самый дорогой оператор в плане.
ПодстроитьПоднять memory_limit, если есть запас RAM; уменьшить threads, чтобы дать потоку больше памяти; убедиться, что temp_directory на SSD.
повторить
Сравнить времяПерезапустить и сравнить. Если ускорилось — спилл уменьшился. Если нет — узкое место не в памяти.

Практические выводы для капстоуна

Сведём в правила, которые применим в остальных уроках модуля.

  • Не бояться датасета больше RAM. DuckDB досчитает агрегацию и join через external-механизмы; запрос завершится корректно. Это штатный режим, а не аварийный.
  • Задавать memory_limit и temp_directory явно. Раз данные больше памяти, полагаться на умолчания нельзя. temp_directory — обязательно на быстрый SSD с запасом места.
  • memory_limit — это про «где начнётся спилл». Слишком низкий лимит провоцирует ранний спилл и замедляет расчёт. Разумный лимит — настолько большой, насколько позволяет свободная RAM.
  • threads влияет на память на поток. Бюджет делится между потоками; при тяжёлом расчёте иногда меньше потоков означает быстрее, потому что каждому достаётся больше памяти и меньше спилла.
  • Диагностировать через EXPLAIN ANALYZE. Любой тюнинг памяти начинается с того, чтобы увидеть, спиллит ли запрос и какой оператор самый дорогой.

В следующем уроке мы возьмём этот материализованный расчёт и сделаем витрину партиционированной и инкрементально обновляемой через MERGE — чтобы новый месячный файл не требовал пересчёта всех 41 миллиона строк.


Попробуй сам

Понадобится DuckDB 1.5.x и датасет, заметно больший, чем выставленный memory_limit (можно сгенерировать синтетику через range() или взять большой Parquet).

Задания:

  1. Поставьте намеренно маленький memory_limit (например, '500MB') и temp_directory на быстрый диск. Запустите тяжёлую агрегацию по большому числу групп. Во время выполнения проверьте папку temp_directory — появились ли временные файлы спилла.
  2. Прогоните тот же расчёт под EXPLAIN ANALYZE. Найдите оператор hash aggregate и оцените, сколько строк через него прошло.
  3. Поднимите memory_limit так, чтобы расчёт целиком поместился в память, и сравните время с шагом 1. Зафиксируйте, насколько спилл замедлял запрос.
  4. При фиксированном memory_limit прогоните расчёт с threads = 2 и с threads, равным числу ядер. Объясните результат через распределение бюджета памяти по потокам.

pandas: что происходит, когда DataFrame не помещается в RAM
Проверка знанийKnowledge check
Как DuckDB обрабатывает агрегацию датасета больше оперативной памяти и как memory_limit, temp_directory и threads влияют на этот процесс?
ОтветAnswer
Когда хеш-таблица агрегации перестаёт помещаться в memory_limit, DuckDB не падает с ошибкой памяти, а переходит на external hash aggregation. Механизм основан на партиционировании по хешу ключа группировки: пространство групп делится на партиции, партиции сверх лимита памяти целиком спиллятся (сбрасываются) на диск во временные файлы, а потом обрабатываются по одной — партиция загружается с диска, по ней досчитываются агрегаты. Поскольку одна партиция много меньше всего набора групп, она помещается в память, даже когда весь набор — нет. Тот же принцип работает для external hash join и external merge sort. Важно понимать: целиком 41 миллион строк в память и не загружается, DuckDB обрабатывает поток; вопрос лишь в том, помещаются ли структуры состояния (хеш-таблицы join и агрегации). memory_limit — это потолок памяти движка и граница, после которой начинается спилл; слишком низкий лимит провоцирует ранний спилл и замедляет расчёт, разумный лимит должен быть настолько большим, насколько позволяет свободная RAM. temp_directory — папка для спилл-файлов; она обязана быть на быстром SSD/NVMe (под спилл-нагрузкой скорость этого диска становится скоростью запроса) и иметь запас места. threads влияет на память на поток: бюджет memory_limit делится между рабочими потоками, и при слишком большом числе потоков каждому достаётся мало памяти и спилл начинается рано — поэтому при тяжёлом расчёте уменьшение threads иногда ускоряет запрос. Спилл не бесплатен (диск медленнее RAM), поэтому external-режим — это про корректность завершения запроса, а не про скорость; цель тюнинга — не дать спиллу случаться там, где данные могли бы поместиться в память. Диагностируется всё через EXPLAIN ANALYZE и наблюдение за появлением временных файлов в temp_directory.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что делает DuckDB, когда хеш-таблица агрегации перестаёт помещаться в memory_limit?

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

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

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

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