Эта идея появилась у меня давно.
Когда мы внедряли BI в крупном банке, я заметил одну вещь: больше всего внедрению радовались руководители. У них появлялись дашборды, графики, показатели, визуальная картина происходящего.
А вот люди, которые каждый день работали с данными, не всегда были в таком же восторге.
BI хорошо показывает, что что-то изменилось: появилась аномалия, просел показатель, выросло значение, изменилась динамика. Но после этого почти всегда возникает следующий вопрос: почему так произошло?
Причин может быть много: ошибка, поздняя загрузка, изменение записей задним числом, какое-то событие, особенность расчёта, естественный ход событий. Чтобы это проверить, нужно смотреть строки, сравнивать выгрузки, пересчитывать показатели и иногда руками разбирать, что попало в отчёт.
У финансистов, которые занимались такими разборами, доступ к хранилищу был ограничен. Они не были техническими специалистами, и это нормально. Но при этом именно им нужно было проверять банковские показатели, сверять расчёты и понимать, что изменилось.
Для таких задач пользователи часто использовали Excel: добавляли формулы, сверяли расчёты, сравнивали текущую выгрузку с предыдущей.
Отсюда появилось желание дать им возможность получать нужные данные самостоятельно, не дожидаясь каждый раз разработчика или аналитика.
Что хотелось получить
Для того чтобы воплотить идею в жизнь, я сформулировал для себя следующие требования к будущему продукту:
отчёт задаётся SQL-запросом;
пользователь не пишет SQL, а только заполняет параметры;
отчёт может выполняться долго;
веб-сервер не должен падать по таймауту;
результат должен сохраняться на сервере;
старые выгрузки должны оставаться в истории;
файл должен быть пригоден для дальнейшей работы в Excel, Power Query или других инструментах;
само приложение не должно превращаться в ещё одну BI-систему.
Это не замена Power BI, Metabase, Superset или Redash. Такие инструменты решают более широкий круг задач.
Здесь сценарий уже: пользователь запускает SQL-отчёт с параметрами, приложение выполняет его в фоне, сохраняет CSV-файл и оставляет запуск в истории.
Предыдущая версия
Эта задача не новая.
Ещё в 2016 году на Хабре у меня выходила статья “Этого не будет в BI” с похожей постановкой вопроса. Тогда решение было завязано на Excel: приложение формировало отчёты непосредственно в Excel-файлах, использовало потоковую запись и могло ждать завершения долгих запросов к базе.
В новую версию я не стал переносить всё как есть. Остался только основной сценарий:
заранее подготовленный SQL-запрос;
параметры для запуска;
веб-интерфейс;
выполнение отчёта вне пользовательского HTTP-запроса;
файл результата;
история выгрузок.
Главное отличие — формат и масштаб. Вместо Excel теперь CSV, вместо более широкой системы отчётности — небольшое ядро, которое можно встроить в свою инфраструктуру.
Авторизация, расписание и сложная система прав не вошли в ядро намеренно. В реальной инфраструктуре такие вещи часто удобнее подключать отдельным слоем: через reverse proxy, Keycloak, VPN, корпоративный SSO или внешний планировщик.
Почему не просто экспорт из BI
Экспорт из BI действительно есть, поэтому формулировка “BI этого не умеет” была бы неправильной.
Проблема в другом. Экспорт из BI обычно связан с конкретной моделью, визуалом, правами, ограничениями платформы и настройками администратора. Для управляемой аналитики это нормально. Но для сценария “получить большой row-level снимок данных и дальше разбирать его как файл” это не всегда удобно.
Можно решить задачу и другими способами:
дать пользователю Power Query с прямым подключением к БД;
написать Python-скрипт и использовать Jupyter Notebook;
настроить выгрузку по cron;
использовать Metabase, Redash или Superset;
сделать отдельную ETL-задачу.
Но у каждого варианта есть своя цена.
Power Query с прямым доступом к БД требует раздавать подключения, драйверы и права.
Jupyter и скрипты удобны техническим специалистам, но не обычным бизнес-пользователям.
Metabase и Superset мощнее, но это уже отдельные системы со своей установкой, администрированием и моделью использования. Cron-выгрузка хороша для регулярных задач, но хуже подходит для запуска отчёта пользователем с произвольными параметрами.
Мне нужен был небольшой инструмент между “скриптом на сервере” и “полноценной BI-платформой”.
Почему CSV
Старая версия подобного инструмента у меня формировала Excel-файлы. Это работало, но у Excel есть практическое ограничение: 1 048 576 строк на лист. Если данных больше, приходится резать результат на несколько листов или файлов.
CSV проще.
Его можно писать потоком, строка за строкой. Не нужно держать весь результат в памяти. Нет листов, стилей, формул, ширины колонок и других деталей XLSX. Такой файл можно открыть не только в Excel: его можно подключить через Power Query, обработать скриптом, загрузить в другой инструмент или просто хранить как снимок данных.
Конечно, CSV не делает выгрузки “безлимитными”. Остаются ограничения диска, файловой системы, времени выполнения SQL, сети и инструмента, которым файл потом читают. Но само приложение не добавляет лимит уровня “столько-то строк в листе”.
Общая схема
Я сделал небольшое приложение, которое состоит из двух частей:
Python-сервер;
Фронтенд на HTML/JavaScript.
Сервер отдаёт статические файлы клиента и предоставляет HTTP API. Клиент показывает список отчётов, форму параметров, историю запусков и страницу настройки отчёта.
Схема работы такая:
Пользователь открывает отчёт.
Заполняет параметры.
Нажимает кнопку запуска.
Сервер создаёт запись о запуске и стартует отдельный worker-процесс.
Worker выполняет SQL-запрос и пишет результат в CSV.
Клиент периодически опрашивает сервер и обновляет статус.
После завершения файл появляется в истории.
Главный момент: долгий SQL-запрос не выполняется внутри HTTP-запроса пользователя.
HTTP-запрос только запускает задачу. Дальше отчёт живёт отдельно.

Worker вместо долгого HTTP-запроса
Если выполнять большой отчёт прямо в обработчике HTTP-запроса, появляется ряд вопросов:
что делать с таймаутом веб-сервера;
что увидит пользователь, если запрос выполняется час;
что будет, если браузер закроют;
как показать историю;
как понять, что отчёт ещё выполняется;
как не заблокировать сервер одним тяжёлым запросом.
Поэтому каждый запуск отчёта уходит в отдельный процесс.
Упрощённо:

server.py отвечает за HTTP API, список отчётов, историю, запуск worker’ов и отдачу готовых файлов.
worker.py занимается только выполнением отчёта: подключается к источнику данных, выполняет запрос, пишет CSV и завершает работу.
Сервер не выполняет отчёт сам, а запускает отдельный процесс. Этот процесс живёт независимо от пользовательского HTTP-запроса: выполняет SQL, пишет CSV и завершается.
В результате отчёт может выполняться долго, не удерживая открытое HTTP-соединение.
Чтобы не запустить слишком много тяжёлых отчётов одновременно, на стороне сервера есть ограничение количества параллельных worker’ов.
Как хранится история
Я не стал заводить отдельную служебную БД для истории запусков.
Результаты лежат в файловой системе. Для каждого отчёта есть своя папка, внутри — файлы запусков.
Во время генерации файл пишется как временный:
20260706_143012.pid18234.csv.part
После успешного завершения он переименовывается в готовый CSV:
20260706_143012.csv
Если при выполнении произошла ошибка, рядом создаётся файл с описанием ошибки:
20260706_143012.error.txt
История в интерфейсе строится по этим файлам.
У такого решения есть плюсы:
легко посмотреть результат руками;
легко забэкапить;
не нужна отдельная БД для служебных данных;
можно восстановить состояние после перезапуска сервера;
готовый файл появляется только после завершения записи.
Минусы тоже есть: если нужна сложная история, аудит, права на отдельные выгрузки и retention policy, файловой модели уже может не хватить. Для моей задачи это был приемлемый компромисс.
Конфигурация отчёта
Отчёт описывается JSON-файлом. Его можно создать или изменить вручную: это обычный файл в папке конфигов, без отдельной базы настроек и без скрытого формата.
Для удобства в приложении есть страница настроек отчёта. Через неё можно задать название, SQL-запрос, параметры, запрос для получения колонок и отображаемые заголовки CSV. По сути, это интерфейс для редактирования того же JSON-конфига.

Минимальный пример:
{ "id": "sales_data", "name": "Sales data", "connector": "postgresql", "sql": "SELECT * FROM sales_data WHERE (%(region)s IS NULL OR region = %(region)s)", "params": [ { "name": "region", "view_name": "Region", "type": "string" } ] }
Пользователь видит поле Region, но не видит SQL. Если параметр не заполнен, в запрос передаётся NULL.
Для необязательных фильтров удобно писать так:
WHERE (%(region)s IS NULL OR region = %(region)s) AND (%(date_from)s IS NULL OR sale_date >= %(date_from)s)
Для PostgreSQL используются named placeholders psycopg2. Значения параметров передаются отдельно от текста SQL, а не подставляются конкатенацией строк.
Параметры
Сейчас поддерживаются несколько типов параметров:
string
number
date
multilist
Для обычных строк и чисел пользователь вводит значение, оно уходит в параметры запроса.
Для multilist можно задать отдельный SQL-запрос, который возвращает список вариантов. Например, список регионов или продуктов.
Условный пример:
{ "name": "products", "view_name": "Products", "type": "multilist", "list_query": "SELECT product_id, product_name FROM products ORDER BY product_name" }
Для дат можно задать запросы, которые возвращают минимальное и максимальное значение. Они используются как подсказки в интерфейсе. Например:
“дата от, минимальная в данных — такая-то”.
Это не ограничение на ввод, а именно подсказка пользователю.
Колонки и заголовки CSV
В отчёте можно оставить имена колонок как в БД, а можно задать отображаемые названия для CSV.
Для этого используется column_mapping.
Например:
{ "column_mapping": { "sale_date": "Sale date", "product_name": "Product", "revenue": "Revenue" } }
Есть и отдельный columns_query. Он нужен, чтобы получить список колонок без выполнения полного тяжёлого отчёта. Например, можно указать тот же запрос, но с LIMIT 0, если это подходит для конкретной БД и запроса.
Коннекторы к источникам данных
Я не хотел жёстко привязывать приложение к одной базе.
Сейчас есть готовый коннектор к PostgreSQL, но работа с источником вынесена в отдельный слой. Коннектор — это отдельный файл в server/connectors/.
Идея такая: серверу всё равно, откуда пришли строки. Ему нужны колонки и итератор строк результата.
Условный интерфейс коннектора:
def stream_query(query, params): ... return columns, rows
query здесь — просто текст, который сервер передаёт коннектору как есть, не разбирая и не интерпретируя. Для PostgreSQL это SQL, потому что это язык запросов именно этой СУБД. Но сама архитектура не завязана на SQL: для другого источника query может быть чем угодно, что этот источник умеет выполнять — другой диалект SQL, KQL, вызов метода внутреннего API или вообще специфичный для конкретного хранилища синтаксис. Какой текст ожидать в query и как его выполнять — решает сам коннектор, а не сервер.
Поэтому можно добавить коннектор к другой СУБД или к другому источнику табличных данных. Например, к внутреннему API, корпоративной витрине, другой базе или специализированному хранилищу.
PostgreSQL в текущей версии — это первая реализация, а не принципиальное ограничение архитектуры.
Почему нет встроенной авторизации
Авторизации в приложении сейчас нет. Это сделано намеренно.
Если ставить такой инструмент в реальную корпоративную среду, авторизация обязательна. Но почти в каждой компании она уже решена по-своему: Keycloak, корпоративный SSO, VPN, reverse proxy с внешней аутентификацией, внутренняя система прав.
Если в маленькое приложение зашить собственную авторизацию, она легко может оказаться неподходящей. Кому-то нужна интеграция с Keycloak/OIDC, кому-то достаточно доступа только из VPN, кому-то нужно прокинуть пользователей через существующий gateway.
Поэтому текущая версия рассчитана на установку за внешним слоем безопасности.
Например:

Внутри приложения оставлено простое ядро: отчёты, параметры, запуск, CSV, история. Авторизацию можно добавить поверх или закрыть приложение внешними средствами.
Почему нет расписания
В старой версии подобного инструмента у меня был запуск отчётов по расписанию. В новой я его убрал.
Причина не в том, что расписание сложно запустить технически. Сама модель уже подходит для этого: отчёт стартует отдельным worker’ом, параметры передаются явно, результат сохраняется в историю.
Расписание тянет за собой дополнительные вопросы:
где хранить задания;
как редактировать расписание;
как показывать автоматические запуски в истории;
что делать с ошибками;
как ограничивать нагрузку;
как не дать случайно запускать тяжёлый отчёт каждый час;
как разграничивать права на создание расписаний.
При необходимости расписание можно сделать внешним cron’ом, отдельным scheduler’ом или добавить как модуль поверх текущего механизма запуска.
Интерфейс
Интерфейс состоит из нескольких страниц.
На главной странице — список отчётов.

На странице отчёта — форма параметров, запуск и история выгрузок.

В истории видно, какие файлы уже сформированы, какие ещё выполняются, какие завершились ошибкой. Готовые CSV можно скачать, старые результаты можно удалить.

Есть страница настройки отчёта. Через неё можно создать новый отчёт или изменить существующий: SQL, параметры, запросы для списков, заголовки колонок.
Конфиги всё равно остаются обычными JSON-файлами. Интерфейс просто упрощает редактирование.
Установка
Простейший запуск из исходников:
cd server pip install -r requirements.txt cp .env.example .env python server.py
После этого клиент открывается по адресу: http://127.0.0.1:8000
Клиент отдаётся тем же Python-сервером. Отдельно запускать фронтенд не нужно.
Есть установка через npm:
npm install no-timeout-csv python node_modules/no-timeout-csv/server/server.py
Node.js здесь используется только как способ доставки файлов на диск. Само приложение не работает на Node.js.
Если ставить через npm, лучше сразу вынести папки конфигов и результатов за пределы node_modules, например через переменные CONFIGS_DIR и RESULTS_DIR в .env.
Что получилось
В итоге получился небольшой инструмент для запуска SQL-выгрузок через веб-интерфейс.
Он работает по следующему сценарию: отчёт заранее описывается SQL-запросом, пользователь открывает форму, задаёт параметры и запускает выгрузку. Сам запрос выполняется отдельно от пользовательского HTTP-запроса, поэтому отчёт может строиться долго. После завершения результат сохраняется на сервере в CSV, а запуск остаётся в истории.
Основные возможности:
заранее подготовленные SQL-отчёты;
параметры запуска через веб-форму;
выполнение отчёта в отдельном процессе;
сохранение результата в CSV;
история выгрузок на сервере;
возможность скачать предыдущие результаты;
настройка отчётов через JSON-файл или через страницу настроек;
подключаемые коннекторы к источникам данных.
Для пользователя это выглядит просто: выбрать отчёт, заполнить параметры, запустить, дождаться готового файла и дальше работать с ним в Excel, Power Query или другом инструменте.
Материалы
Пакет на npm: https://www.npmjs.com/package/no-timeout-csv
Текущая версия проекта: https://github.com/AlexKorole/no-timeout-csv
Демо без реального подключения к базе: https://windowrepino.ru/notimeoutcsv/index.html
Старая статья по этой же теме: https://habr.com/ru/articles/281703
Видео со старой версией: https://youtu.be/_csGSw-xyzQ
