Learning Platform
Глоссарий Troubleshooting
Урок 14.02 · 21 мин
Средний
memory-limittemp-directoryspillconfiguration

memory_limit и temp_directory: куда и когда DuckDB спиллит

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

Этот урок разбирает обе настройки: что они означают, какие значения по умолчанию, когда происходит спилл и как его наблюдать. Это связующее звено между общим устройством буферного менеджера и конкретными out-of-core операторами в следующих уроках.


memory_limit: бюджет памяти

memory_limit — это потолок памяти, который DuckDB-движок разрешает себе занять под все свои структуры: блоки данных, hash-таблицы, буферы сортировки, кэш. Buffer manager следит, чтобы суммарное потребление не превышало этот лимит; при приближении к нему он начинает вытеснять данные.

Значение по умолчанию — примерно 80% физической оперативной памяти машины. На 16 ГБ RAM это около 12.8 ГБ. Оставшиеся 20% — буфер для самой ОС, других процессов и накладных расходов, которые DuckDB не учитывает в своём лимите.

-- посмотреть текущий лимит
SELECT current_setting('memory_limit');
-- 12.8 GiB

-- задать явно
SET memory_limit = '4GB';
SELECT current_setting('memory_limit');
-- 4.0 GiB

-- принимаются единицы: KB, MB, GB, TB и бинарные KiB/MiB/GiB
SET memory_limit = '512MB';

Важно понимать, что memory_limit — это лимит для движка DuckDB, а не для всего процесса. Память, которую держит Python-интерпретатор, ваши pandas-DataFrame, другие библиотеки — в этот лимит не входит. Если в одном процессе живёт DuckDB с лимитом 12 ГБ и pandas с DataFrame на 8 ГБ, суммарно процесс хочет 20 ГБ — и на 16 ГБ машине это приведёт к проблемам, хотя сам лимит DuckDB «корректен».

WARNING

memory_limit ограничивает только движок DuckDB. Если DuckDB живёт внутри Python-процесса вместе с pandas, polars, numpy — память этих библиотек в лимит не входит. На машине, где процесс делит RAM с другими потребителями, лимит DuckDB надо ставить с запасом: не 80% RAM, а столько, чтобы хватило и всему остальному.


temp_directory: куда уходит спилл

Когда buffer manager вынужден вытеснить грязные данные (промежуточные результаты, которых нет на диске), он пишет их в файлы во временной директории, заданной настройкой temp_directory. Это и есть «спилл» — сброс данных из RAM на диск.

Значения по умолчанию зависят от режима:

  • Persistent-база (открыт файл data.db): temp-директория по умолчанию — data.db.tmp рядом с файлом базы.
  • In-memory база (база в памяти, без файла): по умолчанию .tmp в текущем рабочем каталоге.
-- посмотреть и задать
SELECT current_setting('temp_directory');

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

В temp-директории DuckDB создаёт временные файлы на время выполнения запросов и удаляет их по завершении. Размер этих файлов может быть значительным — если запрос спиллит десятки гигабайт промежуточных данных, столько же временно займёт и temp-директория.

Куда уходят данные при спилле
Оператор запросаHash-таблица или буфер сортировки растёт по ходу запроса
растут сверх memory_limit
Buffer manager вытесняетПамяти не хватает, грязные данные надо куда-то записать
запись spill-файлов
temp_directory на дискеВременные файлы; создаются на время запроса, удаляются по завершении

Когда происходит спилл

Спилл — не постоянное состояние, а реакция на нехватку памяти. Он происходит, когда суммарное потребление DuckDB подходит к memory_limit, а запросу нужно ещё. Конкретно спилл случается, когда:

  • Hash-таблица агрегации не помещается в память — внешняя агрегация партиционирует группы и спиллит партиции.
  • Hash-таблица join не помещается — внешний join спиллит обе стороны.
  • Буфер сортировки не помещается — внешняя сортировка спиллит отсортированные куски.

Эти три механизма — темы следующих уроков. Здесь важен общий принцип: пока запрос укладывается в memory_limit, всё работает в RAM на максимальной скорости; как только не укладывается — buffer manager начинает спиллить, и запрос замедляется, но завершается, а не падает.

Это ключевое отличие DuckDB от движков, которые при нехватке памяти просто падают с OOM. DuckDB деградирует по скорости, но не по результату. Цена out-of-core — время; выгода — запрос вообще отрабатывает.

Память достаточна против память недостаточна
Данные меньше memory_limitВсё помещается в RAM; операторы работают на полной скорости
без спилла
Работа целиком в RAMМаксимальная скорость, temp-директория не используется
Данные больше memory_limitПромежуточные результаты не влезают; buffer manager вынужден спиллить
спилл на диск
Out-of-core исполнениеЧасть данных на диске; медленнее, но запрос завершается, а не падает

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

Чтобы увидеть спилл своими глазами, поставим заведомо маленький лимит и запустим тяжёлый запрос.

-- создаём датасет, который не влезет в маленький лимит
CREATE TABLE events AS
SELECT range AS id, range % 5000000 AS user_id, random() AS val
FROM range(50000000);

-- ограничиваем память до 300 МБ
SET memory_limit = '300MB';
SET temp_directory = '/tmp/duck-spill';

-- тяжёлая группировка по 5 млн уникальных user_id
SELECT user_id, count(*), avg(val)
FROM events
GROUP BY user_id;
-- запрос ОТРАБОТАЕТ, несмотря на лимит 300 МБ

Пока этот запрос выполняется, в /tmp/duck-spill появятся временные файлы — это спилл партиций hash-агрегации. По завершении запроса файлы исчезнут. Поведение видно и в плане:

SET memory_limit = '300MB';
EXPLAIN ANALYZE
SELECT user_id, count(*) FROM events GROUP BY user_id;

В выводе EXPLAIN ANALYZE оператор HASH_GROUP_BY будет помечен как работающий в out-of-core / external режиме — это и есть индикатор спилла. Если тот же запрос выполнить с большим лимитом, метка external исчезнет: данные поместились в RAM.

TIP

Главный практический навык — связывать наблюдаемое поведение с настройками. Запрос внезапно стал медленным? Проверь, не начал ли он спиллить: посмотри EXPLAIN ANALYZE на метку external у операторов и наличие файлов в temp_directory во время запроса. Спилл — это не ошибка, но это сигнал, что данные переросли memory_limit.


Практические рекомендации

Несколько правил, которые экономят часы отладки.

Ставь temp_directory на быстрый диск. Спилл — это I/O. Если temp-директория на медленном HDD или сетевом диске, out-of-core запросы будут мучительно медленными. SSD/NVMe для temp-директории кардинально ускоряет спилл. Если на машине есть быстрый локальный диск — temp-директория должна быть на нём.

Следи за местом на диске под temp_directory. Если запрос спиллит 80 ГБ, а на диске под temp-директорией свободно 20 ГБ — запрос упадёт из-за нехватки места на диске. Out-of-core переносит проблему с RAM на диск, но не отменяет её: место нужно где-то.

memory_limit ниже RAM, если процесс не один. Если DuckDB делит процесс с Python-библиотеками или машину с другими сервисами — занижай лимит, чтобы не спровоцировать системный OOM, который убьёт весь процесс жёстко, в обход аккуратного спилла DuckDB.

Занижение memory_limit — рабочий приём. Иногда лимит занижают намеренно: например, чтобы DuckDB на shared-машине не съел всю память и не помешал соседям. Это нормально — запрос просто будет больше спиллить. Лучше предсказуемо медленный запрос, чем убитый OOM-киллером процесс.

СимптомВероятная причинаЧто проверить / сделать
Запрос упал с OOMсистемная память кончилась раньше спиллазанизить memory_limit, убрать конкурентов за RAM
Запрос упал: нет места на дискеtemp_directory переполнилась спилломосвободить диск или указать раздел побольше
Запрос внезапно очень медленныйначался спилл (данные переросли лимит)EXPLAIN ANALYZE: метка external; temp на быстрый диск
Запрос медленный и temp на HDD/сетиспилл идёт на медленный носительперенести temp_directory на SSD/NVMe

Где задавать настройки

memory_limit и temp_directory можно задать в трёх местах, и место влияет на область действия.

Через SET в сессии. SET memory_limit = '4GB' действует на текущее соединение. Это удобно для разовой настройки под конкретный тяжёлый запрос: подняли лимит, выполнили, при желании вернули.

При подключении базы. В API-клиентах настройки можно передать в момент connect — через объект конфигурации. Тогда соединение сразу открывается с нужным лимитом и temp-директорией, без отдельного SET.

Глобально для in-memory части. Часть настроек применима как глобальные, действующие на весь экземпляр DuckDB.

import duckdb

# настройки прямо при подключении
con = duckdb.connect(
    "warehouse.db",
    config={"memory_limit": "8GB", "temp_directory": "/fast-nvme/spill"}
)

Практический смысл различия: если у вас сервис, который держит соединение к DuckDB долго и гоняет много запросов, настройки логично задать один раз при подключении. Если же это интерактивная сессия и под отдельный запрос нужен особый лимит — удобнее SET прямо перед запросом. И то и другое — рабочие подходы; выбор зависит от того, разовая это настройка или постоянная для всего соединения.


Попробуй сам

  1. Посмотри значения по умолчанию: SELECT current_setting('memory_limit'); и SELECT current_setting('temp_directory'); Запусти DuckDB сначала как in-memory (duckdb), потом на файл (duckdb test.db) — отличается ли temp_directory по умолчанию?
  2. Создай большую таблицу: CREATE TABLE t AS SELECT range AS id, range % 3000000 AS g FROM range(30000000);
  3. Задай SET temp_directory = '/tmp/spill-demo'; и SET memory_limit = '250MB'; Запусти SELECT g, count(*) FROM t GROUP BY g; — пока он идёт, посмотри в другом терминале содержимое /tmp/spill-demo. Появились файлы? Что с ними после завершения запроса?
  4. Выполни EXPLAIN ANALYZE того же запроса с лимитом 250MB, затем подними лимит до значения побольше (например, 4GB) и повтори. Сравни метку режима у оператора HASH_GROUP_BY — есть ли пометка external в первом случае и пропадает ли она во втором?
  5. Подумай: если temp_directory указать на сетевой диск, как это скажется на времени запроса из шага 3 и почему.
Trino: spill-to-disk и управление памятью на воркерах
Проверка знанийKnowledge check
Чем отличается поведение DuckDB при нехватке памяти от движка, который при нехватке памяти падает с OOM, и какую роль в этом играют memory_limit и temp_directory?
ОтветAnswer
Многие движки при нехватке памяти просто падают с ошибкой OOM — запрос не завершается. DuckDB вместо этого деградирует по скорости, но не по результату: запрос завершается, просто медленнее. Это обеспечивает out-of-core исполнение, управляемое двумя настройками. memory_limit задаёт бюджет памяти движка (по умолчанию около 80% физической RAM): пока промежуточные данные запроса укладываются в этот лимит, всё работает в RAM на полной скорости. Когда данные перерастают лимит, buffer manager начинает спиллить — сбрасывать грязные промежуточные данные (содержимое hash-таблиц, буферов сортировки) на диск. temp_directory задаёт место для этих spill-файлов: по умолчанию это файл рядом с базой для persistent-режима или каталог .tmp для in-memory. Spill-файлы создаются на время запроса и удаляются по завершении. Важные оговорки: memory_limit ограничивает только движок DuckDB, а не весь процесс — память pandas и других библиотек в него не входит, поэтому при делении процесса лимит занижают; temp_directory должна быть на быстром диске с достаточным свободным местом, иначе спилл будет либо мучительно медленным, либо упрётся в нехватку места на диске. Цена out-of-core — время, выгода — запрос вообще отрабатывает.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чему примерно равно значение memory_limit по умолчанию?

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

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

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

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