Learning Platform
Глоссарий Troubleshooting
Урок 03.05 · 22 мин
Средний
configurationsetpragmatuning

Конфигурация: 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');
Механизм конфигурации DuckDB
SET / PRAGMAДва взаимозаменяемых способа задать значение параметра движка; PRAGMA пришёл из SQLite
Параметр движкаНапример threads или memory_limit — управляет поведением исполнителя и буфер-менеджера
current_setting() / duckdb_settings()
Прочитать значениеcurrent_setting('name') возвращает одно значение; duckdb_settings() — таблицу всех параметров

Область действия настроек

У параметров есть область действия (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 снижает суммарное давление на память.

TIP

Если запрос неожиданно упирается в память на машине со многими ядрами, попробуйте уменьшить 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 он может занять, прежде чем начнёт пользоваться диском.

Роль memory_limit при работе с данными
Данные в пределах memory_limitПромежуточные структуры помещаются в заданный бюджет памяти — запрос идёт целиком в RAM, быстро
лимит достигнут
Спилл на дискDuckDB выгружает промежуточные данные в temp_directory и продолжает — out-of-core исполнение, медленнее, но запрос завершится
memory_limit определяет ТОЧКУ спилла, а не предел обработкиМеньше 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. Произвольно заданное значение чаще всего хуже автоматического, потому что оно не учитывает конкретное железо. Правильный порядок такой: сначала запустить с настройками по умолчанию, посмотреть, как ведёт себя реальная нагрузка, и менять параметры только если есть конкретная наблюдаемая причина — запрос вытесняет другие процессы, нужен детерминированный замер, хочется протестировать спилл. Настройка должна быть ответом на замеченную проблему, а не ритуалом перед началом работы. Это та же логика, что и в оптимизации запросов: сначала измерить, потом менять.


Попробуй сам

Поэкспериментируйте с конфигурацией на реальной нагрузке.

  1. Запустите CLI. Посмотрите значения по умолчанию: SELECT name, value FROM duckdb_settings() WHERE name IN ('threads', 'memory_limit', 'temp_directory');.
  2. Сгенерируйте достаточно крупную таблицу: CREATE TABLE big AS SELECT i AS id, i % 1000 AS grp, random() AS val FROM range(20_000_000) AS r(i);.
  3. Включите .timer on. Выполните тяжёлую агрегацию SELECT grp, count(*), avg(val) FROM big GROUP BY grp; и запишите время.
  4. Выполните SET threads = 1; и повторите тот же запрос. Сравните время с параллельным исполнением — разница покажет вклад параллелизма.
  5. Верните RESET threads;. Теперь намеренно ужмите память: SET memory_limit = '200MB';, при необходимости задайте temp_directory. Повторите агрегацию. Запрос должен завершиться (а не упасть) — пусть и медленнее: это и есть спилл на диск в действии.
  6. Сформулируйте письменно, что доказал шаг 5 про смысл memory_limit.

Шаг 5 — ключевой: он на практике показывает, что memory_limit задаёт точку спилла, а не предел обрабатываемых данных.

Trino: модель памяти в распределённом движке
Проверка знанийKnowledge check
Почему параметр memory_limit задаёт не предельный объём данных, который DuckDB способен обработать, а момент начала спилла на диск, и как с этим связан параметр threads?
ОтветAnswer
memory_limit задаёт бюджет оперативной памяти, который DuckDB разрешено использовать под буферы, векторы и промежуточные структуры исполнения. Но DuckDB умеет out-of-core исполнение: когда промежуточные данные перестают помещаться в memory_limit, движок не падает с ошибкой, а спиллит эти данные на диск (в каталог temp_directory) и доводит запрос до конца. Поэтому memory_limit управляет не тем, можно ли обработать датасет, а тем, в какой момент исполнение перейдёт от полностью in-memory к спиллу на диск. Если данные помещаются в лимит — запрос идёт целиком в RAM и быстро; если превышают — начинается спилл, запрос замедляется, но завершается. Меньший memory_limit означает более ранний спилл и более медленный запрос, больший — дольше всё держится в памяти; но обработать DuckDB способен и объёмы заметно больше лимита. Параметр threads связан с этим через память косвенно: больше потоков — больше параллельных операций, и каждая ведёт собственные промежуточные структуры (например, локальные хеш-таблицы при join и агрегации), поэтому суммарный расход памяти растёт с числом потоков. Если запрос упирается в память, уменьшение threads снижает параллельное давление на память — ценой скорости. То есть threads и memory_limit обычно настраивают совместно, балансируя скорость, параллелизм и расход памяти под конкретную задачу и машину.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что задаёт параметр `threads` в DuckDB?

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

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

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

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