Конфигурация: SET, PRAGMA, threads, memory_limit
DuckDB работает разумно «из коробки»: по умолчанию он сам определяет число доступных ядер и берёт адекватный лимит памяти. Но «по умолчанию» — это компромисс, а у конкретной задачи и конкретной машины бывают особые требования. Тогда нужно уметь настроить движок.
Двумя самыми важными параметрами — threads и memory_limit — инженер управляет постоянно: один задаёт степень параллелизма, второй — бюджет памяти и поведение при работе с большими данными. В этом уроке мы разберём механизм конфигурации DuckDB (SET и PRAGMA) и подробно — эти два ключевых параметра. Версия — DuckDB 1.5.2.
Важно сразу задать верное отношение к конфигурации. У DuckDB она устроена минималистично: нет огромного конфигурационного файла, который нужно изучать перед стартом, как у серверных СУБД. Это намеренно — следствие философии «работает из коробки». Поэтому урок не про то, чтобы заучить параметры, а про то, чтобы понимать механизм и знать два главных рычага. Остальное при необходимости находится в самодокументированном справочнике движка.
Два механизма: SET и PRAGMA
У DuckDB два способа управлять параметрами движка — оператор SET и PRAGMA. На практике для большинства параметров они взаимозаменяемы, и можно пользоваться любым.
Оператор SET задаёт значение параметра:
SET threads = 4;
SET memory_limit = '8GB';
PRAGMA исторически пришёл из мира SQLite и делает то же самое:
PRAGMA threads = 4;
PRAGMA memory_limit = '8GB';
Наличие двух синтаксисов — снова отголосок родства DuckDB и SQLite: PRAGMA DuckDB унаследовал ради совместимости привычек, а SET — это форма из мира «больших» SQL-СУБД. Практически выбирайте любой; в этом курсе для единообразия используется SET.
Привычка проверять текущее значение перед тем, как его менять, очень полезна: так вы видите, от чего отталкиваетесь, и понимаете, что именно изменилось. Слепое выставление параметра без взгляда на исходное значение — частый источник путаницы при диагностике.
Прочитать текущее значение параметра можно через current_setting:
SELECT current_setting('threads') AS threads;
┌─────────┐
│ threads │
│ int64 │
├─────────┤
│ 8 │
└─────────┘
А посмотреть все доступные параметры с их значениями и описаниями — через служебную функцию:
SELECT name, value, description
FROM duckdb_settings()
WHERE name IN ('threads', 'memory_limit', 'temp_directory');
Область действия настроек
У параметров есть область действия (scope) — на что распространяется значение. Это важно понимать, чтобы настройка применилась туда, куда вы рассчитываете.
Глобальная (GLOBAL) область — настройка действует на всю базу/соединение. Так настраивают, например, threads и memory_limit: это свойства движка в целом.
Локальная (LOCAL, она же session) область — настройка действует только в пределах текущей сессии.
Область можно указать явно:
SET GLOBAL threads = 4;
SET LOCAL ...; -- для параметров, поддерживающих сессионную область
Для большинства задач достаточно простого SET без явного указания области — движок применит настройку с разумной областью по умолчанию. Явный scope нужен, когда вы сознательно хотите ограничить настройку текущей сессией или, наоборот, сделать её глобальной.
Вернуть параметр к значению по умолчанию можно через RESET:
RESET threads;
Из Python есть ещё один способ задать настройки — при создании соединения, передав их словарём в connect():
import duckdb
con = duckdb.connect(
"analytics.duckdb",
config={"threads": "4", "memory_limit": "8GB"},
)
Разница с SET тонкая, но удобная на практике. SET применяется к уже открытому соединению — это команда внутри сессии. config= задаёт параметры в момент открытия — они действуют с самого первого запроса, и их не нужно выставлять отдельным шагом. Для пайплайна, где соединение создаётся в одном месте кода, config= делает настройку явной и привязанной к месту подключения: видно сразу, с какими параметрами работает эта база. Оба способа эквивалентны по результату — выбор между ними это вопрос стиля кода.
threads: степень параллелизма
Параметр threads задаёт, сколько потоков DuckDB использует для исполнения запросов. Это степень параллелизма движка.
По умолчанию DuckDB определяет число доступных аппаратных потоков машины и берёт его. На 8-ядерной машине это обычно 8. Чаще всего значение по умолчанию — то, что нужно: движок задействует все ядра.
Полезно понимать, что значит «задействует все ядра» в случае DuckDB. Это не означает, что каждый запрос механически делится на N равных кусков. DuckDB использует morsel-driven параллелизм: работа дробится на небольшие порции (morsel), и потоки разбирают эти порции по мере готовности. Параметр threads задаёт, сколько потоков участвует в этом разборе. Поэтому увеличение threads ускоряет запрос лишь до тех пор, пока есть, что параллелить, и пока хватает физических ядер: поставить threads больше числа ядер машины обычно не даёт выигрыша, потому что лишние потоки просто конкурируют за те же ядра. Подробно morsel-driven модель курс разбирает в модуле про параллелизм; здесь достаточно понимать, что threads — это рычаг степени параллелизма, а не число «кусков» запроса.
-- посмотреть текущее число потоков
SELECT current_setting('threads');
-- ограничить четырьмя потоками
SET threads = 4;
Зачем вообще менять threads? Несколько практических причин.
Оставить ресурсы другим процессам. Если на машине параллельно работает что-то ещё, и вы не хотите, чтобы DuckDB занял все ядра, — уменьшите threads.
Диагностика и воспроизводимость. SET threads = 1 отключает параллелизм. Это полезно, чтобы понять, что в поведении запроса связано именно с параллельным исполнением, и чтобы получить детерминированный, легко сравниваемый замер.
Контроль памяти. Число потоков косвенно влияет на память: больше потоков — больше параллельных операций, и каждая держит свои промежуточные данные. На задачах, тяжёлых по памяти, уменьшение threads снижает суммарное давление на память.
Если запрос неожиданно упирается в память на машине со многими ядрами, попробуйте уменьшить threads. Каждый поток ведёт собственные промежуточные структуры (например, локальные хеш-таблицы при join и агрегации), поэтому суммарный расход памяти растёт с числом потоков. Меньше потоков — меньше параллельный расход памяти, ценой скорости. Подробно связь параллелизма и памяти курс разбирает в модулях про параллелизм и larger-than-memory.
memory_limit: бюджет памяти
Параметр memory_limit задаёт, сколько оперативной памяти DuckDB разрешено использовать для исполнения запросов — для буферов, векторов, промежуточных структур.
-- посмотреть текущий лимит
SELECT current_setting('memory_limit');
-- задать лимит в 8 гигабайт
SET memory_limit = '8GB';
Значение задаётся строкой с единицей измерения: '8GB', '512MB' и так далее. По умолчанию DuckDB берёт лимит, исходя из объёма памяти машины (адекватную долю от неё).
Здесь критически важно понять, что memory_limit — это не «сколько данных можно обработать». DuckDB умеет out-of-core исполнение: когда промежуточные данные перестают помещаться в memory_limit, движок не падает, а спиллит их на диск и доводит запрос до конца. Поэтому memory_limit управляет не возможностью, а тем, в какой момент начнётся выгрузка на диск.
Стоит уточнить, что именно учитывается в этом бюджете. memory_limit — это лимит на память, которой управляет буфер-менеджер DuckDB: буферы, в которых лежат векторы и промежуточные структуры исполнения — хеш-таблицы соединений, состояния агрегаций, буферы сортировки. Это та память, расход которой движок контролирует и может при необходимости выгрузить на диск. Сам исполнитель спроектирован так, чтобы работать в пределах этого бюджета: операторы, способные спиллить (внешняя агрегация, внешнее соединение, внешняя сортировка), отслеживают своё потребление и переходят к выгрузке, когда упираются в лимит. Детально устройство буфер-менеджера и механику спилла каждого оператора курс разбирает в модуле про larger-than-memory; здесь важно понимать общую картину: memory_limit — это договорённость между вами и движком о том, сколько RAM он может занять, прежде чем начнёт пользоваться диском.
Со спиллом связан ещё один параметр — temp_directory, путь к каталогу, куда DuckDB выгружает временные данные:
SET temp_directory = '/fast/ssd/duckdb_tmp';
Если ожидается спилл больших объёмов, имеет смысл указать temp_directory на быстром диске (SSD) — скорость спилл-диска прямо влияет на скорость out-of-core запроса.
Зачем менять memory_limit:
- Уменьшить — чтобы DuckDB ужился с другими процессами на машине и не вытеснял их из памяти; либо чтобы намеренно протестировать поведение запроса при ограниченной памяти.
- Увеличить — если машина выделена под DuckDB и хочется, чтобы как можно больше работы шло в RAM без спилла.
Другие полезные параметры
threads и memory_limit — главные, но duckdb_settings() показывает десятки параметров. Несколько из них полезно знать уже сейчас.
default_order — порядок сортировки по умолчанию (ASC/DESC), применяемый, когда направление не указано явно. default_null_order — куда помещать NULL при сортировке (в начало или в конец). Эти параметры влияют на поведение ORDER BY и важны, когда нужна предсказуемость результата.
enable_progress_bar — включает индикатор прогресса для долгих запросов в интерактивной работе. На тяжёлых запросах удобно видеть, что движок не завис, а считает.
preserve_insertion_order — управляет тем, сохраняет ли DuckDB порядок строк при некоторых операциях. На больших данных отключение этого параметра иногда снижает расход памяти, потому что движку не нужно поддерживать порядок — но это меняет наблюдаемый порядок строк, и менять параметр стоит осознанно.
Главное — не запоминать список, а знать инструмент: duckdb_settings() — это полный, самодокументированный справочник параметров с описаниями. Когда возникает вопрос «а можно ли настроить вот это», первый шаг — заглянуть туда.
-- найти параметры, относящиеся к памяти
SELECT name, value, description
FROM duckdb_settings()
WHERE name LIKE '%memory%' OR name LIKE '%temp%';
Профили настроек под задачу
threads и memory_limit обычно настраивают вместе, под характер задачи и машины. Несколько типичных профилей.
| Профиль | Настройка | Когда |
|---|---|---|
| По умолчанию | Ничего не трогать | Машина под DuckDB, обычная аналитика — defaults уже хороши |
| Деликатный сосед | threads и memory_limit уменьшены | На машине работают и другие процессы, DuckDB не должен их вытеснять |
| Диагностика | SET threads = 1 | Нужен детерминированный замер без эффектов параллелизма |
| Тест памяти | memory_limit намеренно мал, задан temp_directory | Проверить поведение запроса с обязательным спиллом на диск |
| Выделенная машина | memory_limit поднят, threads = все ядра | Машина целиком под тяжёлую аналитику |
Главная мысль: в большинстве случаев настройки по умолчанию разумны, и трогать их не нужно. Конфигурация — инструмент для конкретных ситуаций: соседство с другими процессами, диагностика, контроль над спиллом. Меняйте параметры осознанно, понимая, что именно вы оптимизируете.
Стоит предостеречь от типичной ошибки — «преждевременной настройки». Соблазн велик: открыл DuckDB, сразу выставил threads и memory_limit «на всякий случай». Но defaults у DuckDB не случайны — они выведены из характеристик машины: число потоков по числу ядер, лимит памяти по объёму RAM. Произвольно заданное значение чаще всего хуже автоматического, потому что оно не учитывает конкретное железо. Правильный порядок такой: сначала запустить с настройками по умолчанию, посмотреть, как ведёт себя реальная нагрузка, и менять параметры только если есть конкретная наблюдаемая причина — запрос вытесняет другие процессы, нужен детерминированный замер, хочется протестировать спилл. Настройка должна быть ответом на замеченную проблему, а не ритуалом перед началом работы. Это та же логика, что и в оптимизации запросов: сначала измерить, потом менять.
Попробуй сам
Поэкспериментируйте с конфигурацией на реальной нагрузке.
- Запустите CLI. Посмотрите значения по умолчанию:
SELECT name, value FROM duckdb_settings() WHERE name IN ('threads', 'memory_limit', 'temp_directory');. - Сгенерируйте достаточно крупную таблицу:
CREATE TABLE big AS SELECT i AS id, i % 1000 AS grp, random() AS val FROM range(20_000_000) AS r(i);. - Включите
.timer on. Выполните тяжёлую агрегациюSELECT grp, count(*), avg(val) FROM big GROUP BY grp;и запишите время. - Выполните
SET threads = 1;и повторите тот же запрос. Сравните время с параллельным исполнением — разница покажет вклад параллелизма. - Верните
RESET threads;. Теперь намеренно ужмите память:SET memory_limit = '200MB';, при необходимости задайтеtemp_directory. Повторите агрегацию. Запрос должен завершиться (а не упасть) — пусть и медленнее: это и есть спилл на диск в действии. - Сформулируйте письменно, что доказал шаг 5 про смысл
memory_limit.
Шаг 5 — ключевой: он на практике показывает, что memory_limit задаёт точку спилла, а не предел обрабатываемых данных.