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

Отчёт на 5 000 строк можно собрать прямо в HTTP-запросе, а выгрузку на 20 млн строк — уже нельзя безопасно считать обычным ответом API. На практике выбор между синхронным ответом, фоновой задачей и заранее подготовленной витриной определяет не только скорость скачивания, но и нагрузку на PostgreSQL, повторяемость данных, поведение при сбое и стоимость хранения.
Сначала определите, что именно обещает выгрузка
Ошибка начинается с требования «нужен Excel из всех заказов». Для проектирования важнее четыре параметра: сколько строк попадёт в результат, насколько свежими должны быть данные, как часто запускается отчёт и что произойдёт, если пользователь нажмёт кнопку повторно.
- Объём. Считайте не только строки, но и размер результата. Текстовые поля, JSON и повторяющиеся данные быстро увеличивают файл.
- Свежесть. Одному отчёту нужны данные на текущую минуту, другому достаточно состояния на начало дня.
- Время ожидания. Пользовательский интерфейс и фоновый процесс имеют разные допустимые задержки. Таймаут браузера не должен определять архитектуру отчёта.
- Повторяемость. Два запуска за один период должны либо давать одинаковый срез, либо явно показывать, почему результаты различаются.
- Доступ. Файл может содержать персональные и коммерческие данные, поэтому ссылка на него не должна жить дольше необходимого.
Главный вопрос не «как быстрее отдать CSV», а «какую цену система может заплатить за построение этого CSV».
Срез данных1 — это зафиксированное состояние набора данных на определённый момент или период. Без него пользователь получает файл, в котором разные части результата могут относиться к разным моментам времени.
Три архитектуры выгрузки
1. Синхронный отчёт в HTTP-запросе
Клиент отправляет фильтры, сервер выполняет запрос и возвращает CSV, JSON или другой формат в том же соединении. Это самый короткий путь от кнопки до файла: не нужны очередь, отдельный статус задачи и хранилище результатов.
Подход разумен для небольшого результата и предсказуемого запроса. Например, пользователь выгружает операции за один день по одному магазину, а запрос использует индекс и возвращает ограниченный набор колонок.
Цена простоты — привязка вычисления к запросу пользователя. Долгий SQL занимает соединение приложения с базой, может попасть под таймаут прокси и повториться несколько раз, если браузер или клиент не дождался ответа. Ограничение по количеству строк защищает интерфейс, но не защищает от тяжёлого плана выполнения: фильтр может вернуть мало строк после просмотра большого объёма таблицы.
2. Фоновая задача с готовым файлом
Система принимает параметры отчёта, создаёт задачу и сразу возвращает её идентификатор. Воркер строит результат отдельно, сохраняет файл в объектное хранилище или файловое хранилище, а интерфейс показывает статус и ссылку после завершения.
Пользователь не держит открытым HTTP-соединение, а временный сбой можно обработать повтором. В ответ вы получаете более сложную систему: нужны очередь, хранение статусов, срок жизни файлов, контроль параллельных задач и правила повторного запуска.
Важно сохранить в задаче не только идентификатор пользователя, но и полный набор фильтров, сортировку, формат, часовой пояс и момент создания. Если воркер возьмёт «текущие настройки» из профиля, результат может измениться между постановкой задачи и её выполнением.
3. Предварительно рассчитанная витрина
Система заранее собирает агрегаты: например, оборот по дням, региону и товарной группе. Пользовательский отчёт читает уже подготовленную структуру, а не пересчитывает миллионы исходных строк при каждом открытии.
В PostgreSQL 16 для части таких сценариев подходят материализованные представления. Они хранят результат запроса и обновляются отдельной операцией, поэтому чтение становится проще для базы. Это не бесплатный кеш: обновление требует ресурсов, а пользователь должен видеть время последнего успешного обновления.
Витрина хороша для дашбордов, регулярных отчётов и одинаковых срезов. Она хуже подходит для произвольного конструктора фильтров: каждое новое измерение может потребовать изменения схемы, процесса обновления и контроля качества данных.
Сравнение вариантов
| Подход | Когда выбирать | Что выигрываете | Чем платите |
|---|---|---|---|
| Синхронный запрос | Небольшой результат, простой фильтр, короткое выполнение | Минимум компонентов и почти мгновенная обратная связь | Риск таймаутов, пиковая нагрузка, сложные повторы |
| Фоновая задача | Большой файл, непредсказуемое время, редкий запуск | Изоляция долгой операции и управляемые повторы | Очередь, воркеры, статусы, хранение и очистка файлов |
| Витрина | Частые одинаковые отчёты и допустимая задержка данных | Быстрое чтение и стабильная нагрузка на исходные таблицы | Сложность обновления, место для данных, устаревший срез |
| Комбинация | Дашборд на витрине и большой экспорт по тем же фильтрам | Быстрый интерфейс и отдельный путь для тяжёлой выгрузки | Два контура, которые нужно синхронизировать и тестировать |
Как выбрать подход по задаче
Для оперативного просмотра
Начинайте с синхронного запроса, если результат ограничен по размеру, а время выполнения можно подтвердить измерением на данных, близких к боевым. Ограничьте период, число колонок и максимальный объём ответа. Не рассчитывайте, что пагинация автоматически делает весь экспорт дешёвым: получение каждой страницы всё равно может повторять фильтрацию и сортировку.
Для разовой тяжёлой выгрузки
Выбирайте фоновую задачу, если файл нельзя гарантированно построить за время обычного запроса или его скачивают несколько раз. Задача должна иметь состояния вроде «создана», «в работе», «готова» и «ошибка», а пользователь — понятное сообщение о причине сбоя.
Сделайте запуск идемпотентным: одинаковый запрос одного пользователя за один период не должен случайно создавать десятки одинаковых файлов. Идемпотентность2 означает, что повтор одной операции не меняет результат сверх первого успешного выполнения.
Для регулярного отчёта и дашборда
Если один и тот же агрегат открывают сотни раз, переносите вычисление из пользовательского запроса в витрину. Записывайте время начала и завершения обновления, число обработанных строк и статус последнего запуска. Если обновление прервалось, интерфейс должен показать старый результат и его дату, а не молча выдать неполный новый.
Что обязательно решить до реализации
- Зафиксируйте контракт результата. Опишите названия и типы колонок, порядок сортировки, формат дат, часовой пояс, кодировку и разделитель CSV. Изменение названия колонки может сломать импорт у клиента.
- Определите границы выборки. Используйте явный период и стабильное правило включения границ. Для событий с одинаковым временем добавьте вторичный порядок, иначе строки могут перемещаться между страницами.
- Выберите момент среза. Показывайте его в интерфейсе или в метаданных файла. Если отчёт строится долго, заранее решите, должен ли он отражать состояние на старт задачи или на момент чтения каждой таблицы.
- Проверьте план на реальных объёмах. Тестовая база с тысячу строк не показывает, что произойдёт после роста таблицы в сто раз. Снимайте время выполнения, объём прочитанных данных, нагрузку на CPU и память, а не только время скачивания.
- Спроектируйте отказ. Ошибка базы, заполненное хранилище, отменённая задача и истёкшая ссылка должны иметь разные состояния и понятные действия пользователя.
Что ломается чаще всего
Тяжёлый запрос маскируется маленьким файлом
Файл на несколько мегабайт может потребовать чтения большой таблицы, соединения нескольких источников и сортировки в памяти или на диске. Проверяйте стоимость построения, а не размер результата. Если отчёт нужен регулярно, это аргумент в пользу индексов, витрины или фонового запуска, но не повод без измерений добавлять индексы на все поля.
Экспорт запускается дважды
Пользователь обновил страницу, клиент повторил запрос после сетевой ошибки, а очередь получила две одинаковые задачи. Храните отпечаток параметров и период его действия либо разрешайте параллельные запуски только при явной необходимости. Повтор должен быть безопасен и для базы, и для хранилища.
Выгрузка собирается из меняющихся данных
За время длинного запроса часть строк уже обновилась, а часть ещё нет. Для финансовых и сверочных отчётов это опаснее, чем медленная выдача. Нужен согласованный срез, транзакционная стратегия или предварительно подготовленный набор данных; конкретный выбор зависит от модели изменений в вашей базе.
Файл остаётся доступным после увольнения сотрудника
Проверяйте права в момент создания задачи и в момент скачивания. Не помещайте персональные данные в имя файла или URL. Временная ссылка должна истекать, а готовые файлы — удаляться по понятному сроку, который согласован с требованиями продукта и безопасности.
Когда комбинировать решения
Частая рабочая схема — витрина для графика и фоновой экспорт для детализации. Дашборд читает агрегаты, обновлённые, например, по расписанию, а пользователь, которому нужны исходные строки, запускает отдельную задачу. Так вы не заставляете каждый просмотр пересчитывать историю и не пытаетесь хранить все возможные фильтры в одной витрине.
Синхронный путь можно оставить для коротких запросов, но с жёстким лимитом. Если лимит превышен, интерфейс не должен обрываться: он переводит запрос в фоновую задачу или просит сузить период. Это лучше, чем единый таймаут, который одинаково плохо обслуживает и маленький, и большой отчёт.
Что делать дальше
Соберите три тестовых сценария: маленький свежий отчёт, большой разовый экспорт и часто открываемый агрегат. Для каждого запишите объём данных, время построения, нагрузку на PostgreSQL 16, размер результата и поведение при повторе. После этого выберите архитектуру по измеренному риску, а в проде поставьте наблюдение за временем SQL, длиной очереди, количеством ошибок, объёмом хранилища и возрастом последнего успешного среза.
Отдельно документируйте формат файла и семантику дат. Инвалидация витрин и миграция схемы отчёта — отдельные темы; здесь важно не смешивать их с выбором способа доставки результата.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: PostgreSQL 16 Documentation: Materialized Views, RFC 9110: HTTP Semantics
Частые вопросы
Когда синхронная выгрузка уже слишком большая?
Нужно ли всегда использовать очередь для отчётов?
Что делать, если данные изменились во время построения файла?
Подойдёт ли эта схема для другой версии PostgreSQL?
Что проверять, если фоновая выгрузка не сработала?
Какие подводные камни возникают в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.