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

DuckDB подходит именно для такой роли, но с важной оговоркой: это встраиваемая аналитическая база, которая работает внутри вашего процесса, а не сервер, к которому одновременно подключаются десятки людей. Она не заменяет многопользовательскую СУБД с ролями и сетевым доступом. Если вам нужна постоянная аналитическая СУБД для многих читателей, смотрите в сторону соседней темы, например VPS для ClickHouse. Здесь речь о другом: о пакетном задании, которое можно описать одним скриптом и передать коллеге, не передавая вместе с ним целую платформу.

С чего начинается отчёт: папка файлов и один вопрос

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

Первое, что стоит сделать, это записать вопрос отчёта прежде, чем писать запрос. Не «посчитать всё по заказам», а конкретно: какие строки входят (период, статусы), какие столбцы нужны, по каким ключам группируем, что считаем. Такая формулировка потом станет и описанием для следующего человека, и мерилом того, сколько данных реально нужно прочитать.

Сам запрос в DuckDB выглядит как обычный SQL, только вместо имени таблицы стоит путь к файлам:

SELECT channel,
       count(*) AS orders,
       sum(amount) AS revenue,
       avg((status = 'cancelled')::INT) AS cancel_share
FROM read_parquet('/data/orders/2025-*/*.parquet')
WHERE order_date >= current_date - INTERVAL 30 DAY
GROUP BY channel
ORDER BY revenue DESC;

Шаблон пути с маской позволяет не перечислять файлы вручную. Но у этого удобства есть цена, о которой лучше знать заранее: маска, которая подхватывает слишком много, подхватит и лишнее. Если в каталоге рядом с настоящими выгрузками лежит пробный файл с другой схемой, запрос либо упадёт, либо молча посчитает не то. Поэтому ограничивайте путь так, чтобы он описывал именно нужный набор, а не «всё, что там есть».

Почему Parquet удобен для отчёта и когда эта выгода пропадает

Parquet хранит данные по столбцам. Для отчёта это хорошая новость: если запрос обращается к пяти столбцам из сорока, читать остальные тридцать пять в принципе не нужно. Строковый формат вроде CSV такой возможности не даёт: чтобы добраться до суммы, приходится разбирать всю строку. Кроме того, Parquet разбит на группы строк и хранит по ним статистику, поэтому условие по дате может позволить пропустить группы, которые заведомо не подходят. Это общее свойство формата, и насколько оно сработает в вашем случае, зависит от того, как выгрузка записана.

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

  • Звёздочка в запросе. SELECT * возвращает все столбцы, и преимущество колоночного формата сходит на нет. Для отчёта перечисляйте только то, что нужно.
  • Фильтр по неупорядоченным данным. Если строки за разные даты перемешаны внутри файлов, статистика групп не позволит ничего пропустить: в каждой группе найдётся подходящая строка.
  • Много мелких файлов. Тысяча крошечных файлов вместо сотни крупных означает лишние открытия и метаданные. Это наше практическое наблюдение, а не цифра из документации, поэтому проверяйте на своих данных.
  • Широкая группировка. Группировка по почти уникальному ключу, например по идентификатору заказа, создаёт столько групп, сколько строк. Промежуточное состояние перестаёт помещаться в оперативную память.
  • Сортировка и соединения больших наборов. Полная сортировка или соединение двух крупных выгрузок требуют держать значительную часть данных одновременно.

Полезная привычка: перед тем как ставить отчёт в расписание, запустите его вручную и посмотрите, что он делает. В DuckDB есть EXPLAIN и EXPLAIN ANALYZE, которые показывают план запроса. Они помогут увидеть, какие столбцы читаются и где тратится время. Конкретный вид вывода зависит от версии, поэтому сверяйтесь с документацией той версии, что стоит у вас.

Память и диск: два разных ограничения

Вот место, где пакетный отчёт на VPS чаще всего удивляет. Новички нередко считают, что «ограничить память» значит решить вопрос ресурсов. На деле у задачи есть как минимум два отдельных предела: оперативная память и место под временные файлы. Они управляются разными настройками и ломаются по-разному.

Согласно материалу о управлении памятью в DuckDB, при обработке данных, превышающих память, система может выгружать промежуточные данные на диск. Параметр memory_limit регулирует использование буферного менеджера, а temp_directory задаёт, куда складывать временные файлы. То есть память и временный диск работают в паре: чем меньше вы даёте первой, тем больше нагрузки ложится на второй.

Размер временного каталога ограничивается отдельной настройкой. По разделу документации о лимитах, это max_temp_directory_size, и документация прямо относит действие memory_limit к буферному менеджеру. Отсюда практический вывод: memory_limit нельзя считать потолком потребления памяти всем процессом. Кроме буферного менеджера процесс занимает память под сам исполняемый код, служебные структуры и то, что вы делаете вокруг запроса, например забираете результат в скрипт. Поэтому запас на VPS оставляйте с головой.

Настройки задаются командами SQL перед отчётом:

SET memory_limit = '3GB';
SET temp_directory = '/var/tmp/duckdb-report';
SET max_temp_directory_size = '30GB';

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

Теперь о том, чем эти два ограничения различаются на практике.

Два предела пакетного отчёта
РесурсЧем управляетсяКак проявляется нехватка
Оперативная памятьmemory_limit (буферный менеджер)Данные уходят на диск, запрос замедляется; если выгрузка невозможна, запрос может завершиться ошибкой нехватки памяти
Временное место на дискеtemp_directory, max_temp_directory_sizeКогда временный диск исчерпан, запрос тоже может завершиться ошибкой нехватки памяти

Обратите внимание на формулировку из источника об управлении памятью: если выгрузка на диск невозможна или временный диск закончился, запрос может завершиться ошибкой out-of-memory. Сообщение об ошибке при этом говорит про память, а причиной может быть именно заполненный диск. Человеку, который получит ваш скрипт, стоит знать об этом заранее: увидев «out of memory», он должен проверить не только free, но и df на каталоге временных файлов.

Важна и обратная сторона. Возможность выгрузки на диск не обещает, что любой запрос выполнится. Некоторые операции могут оказаться такими, что выгрузить их состояние нельзя, и тогда запрос упадёт, даже если свободного диска много. Поэтому нельзя подходить к задаче так: «поставлю маленькую память, остальное уйдёт на диск». Нужно проверить на реальном объёме, что ваш отчёт укладывается.

Где держать временный каталог

Временный каталог лучше направить на быстрый диск. Выгрузка на медленный носитель превращает ночной отчёт в многочасовое ожидание. На VPS с NVMe такой каталог разумно разместить на том же быстром томе. Хорошо также, чтобы он лежал отдельно от каталога с исходными выгрузками и от места, куда пишется результат: тогда переполнение временных файлов не заденет всё остальное. Ограничение max_temp_directory_size служит предохранителем: лучше пусть запрос упадёт по понятному лимиту, чем отчёт забьёт диск и утащит за собой другие процессы машины.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Как выбрать размер машины под такой отчёт

Здесь легко соблазниться таблицей и сказать «для такого-то объёма данных нужен такой-то тариф». Честнее признать: заранее это не вычислить. У MAATRIX есть линейка VPS от 1 vCPU и 2 ГБ памяти (Ba) до 16 vCPU и 32 ГБ (Djed), на всех NVMe, AMD EPYC и root-доступ. Но ресурсы тарифа не равны обещанию ёмкости: сколько данных отчёт переварит, определяется запросом и формой выгрузки, а не названием плана.

Практичнее идти от измерения. Возьмите реальный набор файлов, запустите отчёт на небольшом плане, посмотрите пиковое потребление памяти и размер временного каталога во время работы, а затем решайте, нужно ли больше памяти, больше диска или достаточно переписать запрос. Часто переписывание запроса даёт больше, чем апгрейд: убрать лишние столбцы, ограничить период, перенести тяжёлую группировку на меньшее число ключей. Для пакетной ночной задачи есть и приятное свойство: машину можно взять побольше на время подбора параметров, а потом остановиться на подходящем размере. Корневой доступ позволяет поставить нужные инструменты и настроить каталоги, не договариваясь ни с кем.

С числом ядер связь простая, но тоже требует замера. DuckDB умеет использовать несколько потоков, поэтому на нескольких vCPU агрегация по большим файлам может идти быстрее. Насколько именно, зависит от данных, и обещать здесь ничего нельзя. Лишь учтите, что больше потоков означает и больше одновременно занятой памяти под промежуточные результаты, так что ядра и память нужно подбирать вместе.

Завершение отчёта: чтобы утром не открыть половину файла

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

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

Схема получается такой:

  1. Запрос пишет результат в файл вида report-2025-06-15.parquet.tmp в каталоге публикации.
  2. Скрипт убеждается, что запрос завершился без ошибки, и делает простую проверку: файл не пустой, число строк разумное, ключевые суммы не равны нулю.
  3. Только после этого файл переименовывается в report-2025-06-15.parquet.
  4. Указатель на «последний отчёт» (например, символическая ссылка или небольшой файл с именем) обновляется тем же приёмом: сначала новое имя рядом, потом переименование поверх старого.

Для записи результата в DuckDB есть команда COPY ... TO, которая умеет писать, в частности, в Parquet и CSV. Точный синтаксис и параметры сверяйте с документацией установленной у вас версии.

COPY (
  SELECT channel, count(*) AS orders, sum(amount) AS revenue
  FROM read_parquet('/data/orders/2025-06/*.parquet')
  GROUP BY channel
) TO '/srv/reports/report-2025-06-15.parquet.tmp' (FORMAT parquet);

Следом оболочка делает mv на итоговое имя, и только если команда выше вернула успешный код. В скрипте это одна строка с условием, но именно она отделяет «отчёт готов» от «отчёт пишется». Пользователь, который смотрит каталог публикации, никогда не видит промежуточных файлов с суффиксом .tmp, если заранее договориться, что такие имена не читаются.

Повтор ночного задания: сегодняшний запуск и вчерашний

Расписание рано или поздно сбоит: задание упало, машину перезагрузили, кто-то запустил скрипт вручную в середине дня. Поэтому вопрос не в том, повторится ли запуск, а в том, что произойдёт, когда он повторится.

Хороший ночной отчёт устроен так, что повторный запуск за тот же день даёт тот же результат и не множит файлы. Достичь этого помогает простое правило: имя результата определяется отчётной датой, а не моментом запуска. Тогда второй запуск за ту же дату просто заменит файл через то же переименование. Если же назвать результат по времени запуска, каждый ручной перезапуск оставит рядом ещё одну копию, и через месяц никто не вспомнит, какая из них настоящая.

Стоит договориться и о том, что происходит с неудачным запуском. Если задание упало посреди работы, на диске могут остаться временные файлы отчёта и содержимое каталога temp_directory. Скрипт должен в начале проверять, нет ли следов предыдущей неудачи, и убирать собственные временные имена. Вызывать этот механизм «идемпотентностью» не обязательно, достаточно записать в описании: повторный запуск безопасен, потому что результат определяется датой и публикуется переименованием.

Отдельно подумайте о защите от двух одновременных запусков. Если ночной планировщик и сотрудник запустят скрипт одновременно, они начнут бороться за один временный каталог и один выходной файл. Простая блокировка, например через утилиту flock вокруг запуска, решает вопрос без лишних зависимостей. Она не часть DuckDB, это приём оболочки, и вы вправе заменить его любым другим способом, который у вас принят.

Когда исходная выгрузка меняется во время чтения

Самая коварная ситуация: ночной отчёт читает файлы, а в это же время выгрузка из учётной системы ещё дописывается или заменяется. Например, процесс выгрузки перезаписывает файл за вчерашний день, пока ваш запрос его читает. Результат в лучшем случае получится смешанным (часть старых данных, часть новых), в худшем запрос упадёт на повреждённом файле.

Защита здесь организационная, а не техническая, и это нужно сказать прямо: никакая настройка отчёта не заменит договорённости с тем, кто пишет выгрузку. Минимум, который стоит согласовать:

  • Выгрузка пишется во временное имя и переименовывается после завершения, по тому же принципу, что и ваш отчёт. Тогда читатель видит только готовые файлы.
  • Есть признак завершённости набора: маленький файл-маркер вроде _READY в каталоге дня, который появляется последним. Отчёт стартует, только когда маркер на месте.
  • Уже опубликованные файлы за прошлые даты не изменяются на месте. Если данные пришлось исправить, исправление идёт под новым именем или в новую версию каталога, и отчёт явно выбирает, какую брать.

Полезно также фиксировать список файлов, по которым считался отчёт. Если скрипт перед запуском сохраняет рядом с результатом перечень прочитанных файлов с их размерами и временем изменения, то при расхождении цифр на следующий день можно быстро проверить, менялся ли вход. Это наша практика, не требование инструмента, но стоит она копейки и экономит часы разбирательств.

Если же договориться о порядке выгрузки невозможно, остаётся защитный вариант: сначала скопировать нужные файлы в рабочий каталог на VPS и считать уже по копии. Тогда отчёт видит стабильный снимок, а не движущуюся цель. Цена такого решения: дополнительное место на диске и время копирования, поэтому его нужно закладывать в расчёт диска наравне с временными файлами DuckDB.

Что передать следующему человеку

Читатель, который унаследует отчёт, должен суметь поддерживать его без вас. Для такого передачи хватает небольшого набора вещей, и платформа аналитики для этого не нужна.

  • Один скрипт запуска с понятными путями в начале: откуда читаем, куда пишем, где временный каталог.
  • Файл SQL с запросом и комментарием о том, какой вопрос он отвечает и какие столбцы считаются.
  • Значения memory_limit, temp_directory и max_temp_directory_size с короткой пометкой, почему выбраны именно они и на какой машине проверялись.
  • Описание того, что делать, если отчёт упал с ошибкой нехватки памяти: смотреть и память, и свободное место в каталоге временных файлов.
  • Строка расписания и способ запустить задание вручную, с замечанием, что повторный запуск за ту же дату безопасен.

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

Пустой день

Если всё сделано аккуратно, утром ничего не происходит, и это лучший знак. Файл отчёта лежит на месте с понятным именем, указатель на последнюю версию свежий, временный каталог пуст, процесс завершился и не держит ни память, ни порты. Никакого постоянно работающего сервера базы нет, поэтому нечего обновлять, сторожить и открывать в сеть.

Днём та же машина свободна. Её можно использовать под другое: небольшой сервис, автоматизацию, файловое хранилище. Ночное окно занято одним коротким заданием, а остальные часы суток принадлежат другим задачам. В этом и состоит довод в пользу такого подхода для редкого, раз в сутки, расчёта: отчёт сделал своё дело и ушёл, а сервер остался просто сервером.