Learning Platform
Глоссарий Troubleshooting
Урок 14.06 · 24 мин
Средний
out-of-corememory-budgettuninglimits

Обработка датасета больше RAM: бюджет памяти на поток и пределы

Предыдущие уроки разобрали механизмы out-of-core по отдельности: buffer manager, спилл, внешняя агрегация, join и сортировка. Этот урок — практический синтез: как реально прогнать запрос по датасету, который в несколько раз больше оперативной памяти, как настроить memory_limit и threads, как оценить бюджет памяти на поток, и — честно — где у out-of-core всё-таки есть пределы.

Это финальный урок модуля, и его задача — превратить понимание механики в рабочую интуицию: с какими ручками подходить к большому датасету и чего от них ожидать.


DuckDB и данные больше памяти: что реально работает

Начнём с факта, который стоит закрепить. DuckDB действительно обрабатывает датасеты больше RAM — это не маркетинг.

Trino: spilling на диск — aggregations, joins, sort Документированный пример: ранние версии с внешней агрегацией прогоняли все запросы из набора в 50 ГБ на ноутбуке с 16 ГБ RAM. Сканирование, фильтрация, агрегация, join, сортировка — все основные операции имеют out-of-core реализацию и спиллят на диск, когда нужно.

Это не значит, что «размер данных вообще не имеет значения». Это значит, что нехватка RAM переводит запрос в более медленный режим, а не в падение. Цена out-of-core — время и дисковый I/O; выгода — запрос выполним на скромном железе.

Датасет больше RAM: режимы исполнения
Датасет помещается в RAMВсе промежуточные структуры влезают в memory_limit
полная скорость
Максимальная производительностьНикакого дискового I/O под промежуточные данные
Датасет больше RAMПромежуточные структуры не влезают; включается спилл
спилл на диск
Медленнее, но запрос завершаетсяСкорость ограничена дисковым I/O; результат корректен

Бюджет памяти на поток

Ключевая модель для рассуждений об out-of-core — память делится между потоками. Buffer manager даёт один общий memory_limit на весь движок, а исполнение идёт на threads потоках. Грубо: на каждый поток приходится memory_limit / threads памяти под его долю работы.

Если поток-агрегатор строит свою частичную hash-таблицу, а на него приходится мало памяти — он раньше начнёт спиллить. Отсюда исследовательская оценка, которую важно подать именно как грубую эвристику, а не гарантию:

  • Для агрегационно-тяжёлых запросов комфортным считается порядка 1-2 ГБ памяти на поток.
  • Для join-тяжёлых запросов — порядка 3-4 ГБ на поток (build-стороны и hash-таблицы крупнее).
WARNING

Цифры 1-2 ГБ на поток для агрегации и 3-4 ГБ для join — это приблизительные ориентиры из исследований, а не жёсткие гарантии. Реальная потребность зависит от числа групп, размера build-стороны, тяжести агрегатного состояния, ширины строк. Используйте эти числа как стартовую точку для прикидки, а не как формулу, которой можно слепо доверять. Точную настройку всегда даёт замер на ваших данных через EXPLAIN ANALYZE.

Из этой модели следует неинтуитивный вывод: больше потоков — не всегда быстрее. Восемь потоков делят memory_limit на восемь долей; если запрос join-тяжёлый и каждой доле не хватает, все восемь потоков спиллят, и суммарно из-за дискового I/O может выйти медленнее, чем четыре потока с вдвое большей долей памяти каждый. Параллелизм ускоряет CPU-часть, но обостряет конкуренцию за память.

-- прикидка: 16 ГБ лимита на 8 потоков -> ~2 ГБ на поток
-- для join-тяжёлого запроса это на нижней границе

SET memory_limit = '16GB';
SET threads = 8;   -- ~2 ГБ/поток: join может спиллить

SET threads = 4;   -- ~4 ГБ/поток: join-запросу комфортнее
-- меньше параллелизма по CPU, но меньше спилла

Подход к настройке: для join-тяжёлой нагрузки на машине с ограниченной памятью имеет смысл попробовать уменьшить threads, чтобы каждому потоку досталось больше памяти, и сравнить время. Иногда четыре потока без спилла обгоняют восемь со спиллом.


Рецепт: прогон датасета больше RAM

Соберём практический порядок действий для запроса по датасету, заведомо большему памяти.

Шаг 1. Задать temp_directory на быстрый диск с запасом места. Спилл пойдёт сюда. Нужен SSD/NVMe и свободное место, сопоставимое с объёмом промежуточных данных (может быть десятки-сотни ГБ).

SET temp_directory = '/fast-nvme/duckdb-spill';

Шаг 2. Задать честный memory_limit. Столько, сколько движок реально может занять, не уронив систему. Если процесс делит RAM с другими — занизить.

SET memory_limit = '12GB';

Шаг 3. Подобрать threads под характер запроса. Join-тяжёлый и память впритык — попробовать меньше потоков. CPU-bound и памяти достаточно — оставить по числу ядер.

SET threads = 6;

Шаг 4. Запустить и наблюдать через EXPLAIN ANALYZE. Посмотреть, какие операторы ушли в external-режим, где узкое место по времени.

Шаг 5. Уменьшить объём данных там, где можно. Самый сильный рычаг — не настройки, а сам запрос: фильтровать раньше (filter pushdown), читать только нужные колонки (projection pushdown), не делать SELECT *. Чем меньше данных входит в дорогие операторы, тем меньше спилла.

Порядок настройки под датасет больше RAM
temp_directory на быстрый дискSSD/NVMe со свободным местом под объём спилла
honest memory_limitСтолько, сколько движок может занять, не уронив систему
threads под характер запросаДля join-тяжёлых при нехватке памяти — меньше потоков, больше памяти на поток
EXPLAIN ANALYZE: что спиллитНайти external-операторы и узкое место по времени
Уменьшить вход дорогих операторовФильтры раньше, только нужные колонки — самый сильный рычаг

Пределы out-of-core: где честная граница

Out-of-core мощный, но не всемогущий. Несколько реальных пределов, о которых нужно знать.

Место на диске. Out-of-core переносит проблему с RAM на диск, но не отменяет её. Если запрос спиллит 200 ГБ промежуточных данных, на диске под temp_directory должно быть 200 ГБ свободно. Иначе запрос упадёт — уже не по памяти, а по месту на диске.

Скорость диска. Спилл — это I/O, и его скорость ограничена диском. На медленном HDD огромный out-of-core запрос может идти часами. NVMe принципиально меняет картину, но даже он на порядок медленнее RAM. «Запрос завершится» не означает «завершится быстро».

Не всякое промежуточное состояние спиллится. Как отмечалось в уроке про агрегацию: некоторые сложные агрегатные состояния переменного размера на момент написания не все умеют спиллить. Запрос с экзотическим агрегатом по огромному числу групп теоретически может упереться в OOM даже при работающем out-of-core. Это известное ограничение из документации DuckDB.

Память на сам запрос и метаданные. Какой-то минимум памяти нужен всегда — под структуры самого запроса, метаданные, буферы. Поставить memory_limit в несколько мегабайт и ждать, что отработает любой запрос, не выйдет: для очень большой задачи нужен разумный минимум RAM, даже если основная масса данных спиллится.

ПределВ чём сутьКак смягчить
Место на дискеспилл требует свободного места под temp_directoryосвободить диск, выбрать раздел побольше
Скорость дискаспилл ограничен I/O, HDD очень медленныйtemp_directory на NVMe/SSD
Не всё состояние спиллитсяэкзотические агрегаты переменного размераупростить агрегат, уменьшить число групп
Минимум памяти на запросструктурам запроса нужен какой-то RAMне занижать memory_limit до абсурда

Когда датасет перерос локальную машину

Если даже при грамотной настройке out-of-core запрос идёт неприемлемо долго или упирается в место на диске — это сигнал, что задача переросла модель «один файл на одной машине». Варианты роста:

  • Уменьшить данные на входе. Часто самый дешёвый ход: партиционировать исходные данные и читать только нужные партиции (partition pruning), агрегировать предварительно, не тянуть лишние колонки.
  • Более мощная машина. DuckDB отлично масштабируется вертикально: больше RAM и быстрее NVMe радикально расширяют, что выполнимо in-memory или с дешёвым спиллом.
  • MotherDuck. Managed-облако на DuckDB — вынести тяжёлую часть в облако.
  • DuckLake. Лейкхаус-формат: данные партиционированы в объектном хранилище, запросы читают только релевантные файлы.

Принцип тот же, что в модуле про конкурентность: out-of-core — это про то, чтобы выжать максимум из одной машины. Когда и этого мало, растут не настройкой флага, а сменой архитектуры. Но планка «одной машины» у DuckDB высокая: терабайты данных на мощном узле — вполне её территория.


Попробуй сам

  1. Создай датасет, заметно больший твоего memory_limit по умолчанию: широкую таблицу на 80-100 млн строк через range с несколькими колонками.
  2. Поставь SET temp_directory на быстрый диск и SET memory_limit = '1GB'; Прогони тяжёлый запрос с GROUP BY по многим группам. Замерь время.
  3. Тот же запрос с SET threads = 8; и затем SET threads = 2; при том же лимите 1GB. Сравни время. Для твоего запроса больше потоков ускорило или замедлило? Свяжи ответ с бюджетом памяти на поток.
  4. Прикинь бюджет: memory_limit делить на threads — сколько памяти на поток в каждом из вариантов шага 3? Попадает ли это в ориентир 1-2 ГБ (агрегация) или 3-4 ГБ (join)?
  5. Через EXPLAIN ANALYZE найди операторы в external-режиме. Затем добавь к запросу селективный WHERE и проверь: уменьшился ли спилл, ушли ли операторы из external-режима?

Trino: модель памяти — pools, reserved memory, general pool
Проверка знанийKnowledge check
Почему при обработке join-тяжёлого датасета больше RAM увеличение числа потоков может не ускорить, а замедлить запрос? Сформулируйте через бюджет памяти на поток.
ОтветAnswer
Buffer manager выдаёт один общий memory_limit на весь движок, а исполнение идёт на threads потоках — грубо, на каждый поток приходится memory_limit / threads памяти под его долю работы. Для join-тяжёлых запросов комфортным ориентиром считается порядка 3-4 ГБ на поток (это грубая эвристика, а не гарантия: build-стороны и hash-таблицы крупные). Если увеличить число потоков, общий memory_limit делится на большее число долей, и каждому потоку достаётся меньше памяти. Когда доля каждого потока опускается ниже того, что нужно его части join, все потоки начинают спиллить промежуточные данные на диск. Спилл — это дисковый I/O, он на порядки медленнее работы в RAM. В итоге восемь потоков, каждый со спиллом, могут суммарно отработать медленнее, чем четыре потока с вдвое большей долей памяти, которым спилл не понадобился. Параллелизм ускоряет CPU-часть запроса, но обостряет конкуренцию за общий бюджет памяти. Поэтому для join-тяжёлой нагрузки на машине с ограниченной памятью имеет смысл попробовать уменьшить threads, чтобы каждому потоку досталось больше памяти, и сравнить время — иногда меньше потоков без спилла обгоняет больше потоков со спиллом.

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

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

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

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

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

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