Перейти к содержимому
Поиск
Разработка

DuckDB: SQL для CSV, JSON и Parquet — мастерская и 9 задач

Анализируем выгрузки с DuckDB: девять живых SQL-задач, качество данных, JOIN, рейтинги, JSON, Parquet и экспорт. Исходники и практический запуск на Windows, Linux и VPS.

Александр Некрасов
DuckDB — SQL-анализ CSV, JSON и Parquet

DuckDB на практике: SQL-анализ CSV, JSON и Parquet, проверка качества данных и отчёты. Девять живых задач, редактируемый SQL и исходники для Windows, Linux и VPS.

DuckDB: что это такое и что вы получите на практике

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

DuckDB — встраиваемая аналитическая база данных. Она работает в процессе программы, умеет читать CSV, JSON и Parquet, соединять наборы, группировать строки и выполнять оконные вычисления. Вы получаете настоящий SQL-движок, которому не нужен отдельный сетевой сервис для каждого локального отчёта. Его можно использовать в консоли, Node.js, Python и браузере через WebAssembly.

В этой статье мы превратим учебную выгрузку сервисного бизнеса в несколько полезных отчётов. На странице работает настоящая мини-мастерская: меняйте SQL, загружайте маленький CSV или JSON, смотрите таблицу и диаграмму, скачивайте результат. В исходниках находятся те же данные, нативные Node.js-примеры, тесты и инструкции для Windows, Linux и VPS. Можно сначала разобраться на готовом примере, затем перенести запрос в свой рабочий процесс.

Где применять: выгрузки, аналитика и проверка данных

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

Второй сценарий — подготовка данных для интерфейса. Админке часто нужны десять готовых показателей, а не весь подробный журнал операций. DuckDB может посчитать агрегаты и сохранить компактный JSON, который затем использует график. Так слой отображения получает понятные поля day, orders, revenue_minor. При этом сбор данных и авторизация остаются обязанностями вашего приложения.

Третий сценарий — контроль интеграций. До загрузки в основную базу можно проверить обязательные поля, повторные идентификаторы, диапазон дат и соответствие справочникам. Важно не ограничиваться красивой диаграммой: отчёт полезен только при понятных правилах отбора и подтверждённой полноте входных данных. Для преобразования вложенного JSON без SQL посмотрите JSONata, а для небольших консольных конвейеров — jq.

DuckDB, SQLite и серверная база: разные рабочие задачи

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

SQLite часто выбирают для компактного хранилища приложения с отдельными записями, индексами и транзакциями. PostgreSQL и MySQL предоставляют серверную модель для множества клиентов и постоянной работы бизнес-сервиса. У этих систем есть пересечения возможностей, поэтому выбор зависит от нагрузки и архитектуры. Превращать аналитическую мастерскую в общую многопользовательскую CRM-базу без анализа требований не следует.

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

Версии и устройство этой мастерской

Браузерная часть закреплена на официальном пакете @duckdb/duckdb-wasm 1.32.0 с ядром DuckDB 1.4.3. Используется однопоточный MVP bundle и настоящий Worker. Нативные примеры закреплены на официальном @duckdb/node-api 2.0.0-r.1, который запускает DuckDB 2.0.0. Это две разные поставки: номер npm-пакета браузерной оболочки не равен версии SQL-ядра.

JSON и Parquet в выбранной WASM-сборке подключаются как отдельные подписанные расширения. Они лежат рядом с мастерской; сборка сверяет их SHA256. Исходники содержат происхождение файлов и лицензии. Данные и исполняемые компоненты загружаются с того же сайта, сторонний CDN во время работы не требуется. Для выбранного MVP-варианта не нужны специальные заголовки COOP/COEP.

Само WASM-ядро занимает около 39,4 МБ до HTTP-сжатия, расширения — ещё около 3,6 МБ. Поэтому запуск вынесен в явную кнопку, а первый старт сопровождается пояснением. Следующие SQL-запросы используют уже созданную базу в памяти. Возможности документации новых выпусков могут отличаться: при обновлении пакетов проверяйте версии, совместимость расширений и реальные результаты примеров, а не только успешность установки.

-- В редакторе мастерской выполняйте один SELECT:
SELECT version();

-- Отдельно в нативной консоли DuckDB, вне редактора:
PRAGMA platform;

Мини-мастерская: девять задач и собственный SQL

Начните со сценария «Выручка по городам» и нажмите «Выполнить SQL». Когда появится результат, замените city на manager в SELECT, GROUP BY и ORDER BY там, где это требуется. Вы увидите другой разрез тех же данных. Затем откройте проверку качества и сравните исходную таблицу с очищенной. Такой порядок помогает понять, почему суммирование должно идти после проверки выгрузки.

Редактор принимает один SELECT или WITH. Кнопки позволяют вернуть исходный пример, вернуться к предыдущему запросу, остановить выполнение и скачать SQL, JSON или CSV. При изменении входа старый результат очищается, а его скачивание отключается до нового успешного выполнения. Пустой результат показывается как нормальное состояние, отдельно от ошибки SQL.

Мастерская ограничивает файл двумя МиБ, текст SQL — шестнадцатью КиБ, результат — пятьюстами строками и одним МиБ. Если строк больше, экспорт отклоняется целиком, чтобы усечённый файл не выглядел полным отчётом. Запрос выполняется до десяти секунд; первый запуск ядра может занять до двух минут. Отмена завершает Worker, а следующий запуск создаёт таблицы заново.

Учебные данные: CRM с намеренными ошибками

В orders.csv находятся 24 строки: двадцать корректных уникальных заказов, повтор O004, неверная дата O021, нечисловая сумма O022 и строка без идентификатора. В справочнике clients восемь клиентов. Заказ O020 ссылается на отсутствующего клиента 999 — это отдельная ошибка связи, которую нельзя обнаружить одним преобразованием типа.

В очищенной таблице шестнадцать оплаченных заказов. Их общая сумма составляет 2 575 000 копеек, то есть 25 750 рублей. Эти числа служат простой контрольной точкой: если после изменения запроса вы получили другой итог, должно быть понятно, какой фильтр или правило изменились. Отменённые и ожидающие оплаты работы остаются в данных, но не входят в выручку.

Файл events.json содержит шесть событий с вложенным объектом context. Файл orders.parquet хранит те же двадцать очищенных заказов с типами. Имена, суммы и события вымышлены. Это компактная модель для объяснения приёмов, а не доказательство производительности на многогигабайтных данных. Для собственного тестирования используйте репрезентативную выгрузку и заранее определённые ожидания.

SELECT count(*) AS total_orders,
       count(*) FILTER (WHERE status = 'paid') AS paid_orders,
       sum(amount_minor) FILTER (WHERE status = 'paid') AS revenue_minor
FROM orders_clean;

Windows: установить зависимости и получить первый отчёт

Скачайте архив целиком, распакуйте его в отдельную папку и откройте в ней PowerShell. Нужен поддерживаемый Node.js 22 или новее. После установки Node откройте новый терминал и проверьте node –version: старый процесс мог ещё не получить обновлённый PATH. npm ci использует сохранённый lock-файл и выбирает нативный binding для вашей платформы.

Команда report запускает настоящий нативный движок, поэтому для неё не нужен браузер или веб-сервер. Сценарий summary печатает сводку, export сохраняет отчёт по дням. Выходной файл создаётся только если такого пути ещё нет: случайный повтор не затрёт предыдущий результат. Для новой проверки используйте другое имя либо осознанно подготовьте пустой каталог.

Локальный сервер нужен только для графической мастерской. Откройте адрес 127.0.0.1:8187, а после проверки остановите процесс через Ctrl+C в его терминале. Не открывайте index.html двойным щелчком: загрузка Worker, модулей и WebAssembly должна идти через HTTP. Имеющиеся в исходниках SQL-файлы удобно редактировать независимо от оболочки, не борясь с разными правилами кавычек.

node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
node bin/report.mjs summary
node bin/report.mjs export report.json
npm.cmd run serve

Linux и VPS: тот же проект в рабочем окружении

На Linux используйте актуальный поддерживаемый Node.js для архитектуры сервера. Способ установки выбирайте в официальной инструкции Node.js: версия штатного пакета старого дистрибутива может не удовлетворять требованиям проекта. После распаковки запускайте npm ci заново. Папку node_modules, созданную в Windows, переносить на Linux нельзя: нативные библиотеки зависят от ОС и архитектуры.

Для просмотра мастерской на удалённом сервере достаточно SSH-туннеля. Учебный HTTP-сервер слушает только loopback; туннель переносит доступ на ваш локальный порт. Для постоянного размещения публикуйте готовый public через существующий HTTPS-сервер. Сам нативный отчёт можно запускать по SSH или из планировщика, без открытого веб-порта.

Рабочие выгрузки храните отдельно от кода и демоданных. Выделите пользователю задания права чтения входного каталога и записи отчётов, а не полномочия root. Определите срок хранения файлов и журналов, проверяйте свободное место и код завершения. Если переносите решение в контейнер, тома и пути следует описать явно; подготовиться поможет материал о Docker Compose.

node --version
npm ci --ignore-scripts
npm run build
npm test
node bin/report.mjs export report.json
npm run serve
# Run this SSH command on your own computer:
ssh -L 8187:127.0.0.1:8187 user@your-vps

Пример 1. CSV: сначала проверьте схему

CSV не хранит полноценную схему. В столбце с суммой может оказаться слово, идентификатор с ведущими нулями или пустое значение. Автоматическое определение типов удобно для знакомства, но регулярный отчёт должен иметь понятный контракт. В мастерской orders_raw специально загружается с all_varchar=true: мы сохраняем исходный текст до проверки.

Посмотрите information_schema.columns и сравните orders_raw с orders_clean. Вторая таблица содержит DATE, INTEGER и BIGINT там, где это необходимо. Такая разница объясняет важную границу: файл успешно прочитан, но это ещё не означает, что все его значения корректны для бизнес-расчёта. Идентификаторы, по которым арифметика не нужна, часто следует оставлять строками.

В серверном коде можно задавать columns явно, выбирать разделитель и формат даты. Для выгрузки с точкой с запятой нельзя без проверки копировать параметры CSV с запятой. Сначала исследуйте несколько строк и заголовок, затем закрепите договорённость в конфигурации загрузчика. Подробные параметры приведены в официальном описании импорта CSV.

SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_name IN ('orders_raw','orders_clean')
ORDER BY table_name, ordinal_position
SELECT * FROM read_csv('orders.csv', header=true, all_varchar=true);

Пример 2. Пропуски, дубликаты и невозможные даты

TRY_CAST возвращает NULL, когда преобразование не удалось. Это удобно для диагностики: одна плохая дата не прерывает анализ всей выгрузки, а попадает в отчёт об ошибках. Однако NULL после TRY_CAST нельзя автоматически считать нормальным пустым значением. Исходное поле и причина отклонения должны оставаться доступными.

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

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

SELECT order_id, order_date, amount_minor,
 CASE WHEN order_id IS NULL OR trim(order_id) = '' THEN 'Нет ID'
      WHEN TRY_CAST(order_date AS DATE) IS NULL THEN 'Неверная дата'
      WHEN TRY_CAST(amount_minor AS BIGINT) IS NULL OR TRY_CAST(amount_minor AS BIGINT) < 0 THEN 'Неверная сумма'
      WHEN count(*) OVER (PARTITION BY order_id) > 1 THEN 'Повторный ID'
      ELSE 'OK' END AS issue
FROM orders_raw
QUALIFY issue <> 'OK'
ORDER BY issue, order_id

Пример 3. GROUP BY: выручка по городам

В повседневном отчёте часто нужны три числа: сколько оплаченных работ выполнено, сколько денег они принесли и каков средний чек. WHERE задаёт состав выборки, GROUP BY собирает строки по городу, агрегатные функции вычисляют показатели. ORDER BY нужен для устойчивого и понятного порядка отображения.

Москва в учебных данных получает семь оплаченных заказов на 1 500 000 копеек, Казань — шесть на 885 000, Тула — три на 190 000. Общая сумма совпадает с контрольной точкой 2 575 000. Если вы замените paid на другой статус, изменится смысл показателя: ожидаемая выручка и фактически оплаченные работы должны называться по-разному.

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

SELECT city, count(*)::INTEGER AS orders,
 sum(amount_minor)::BIGINT AS revenue_minor,
 round(avg(amount_minor) / 100, 2)::DOUBLE AS average_rub
FROM orders_clean WHERE status = 'paid'
GROUP BY city ORDER BY revenue_minor DESC, city

Пример 4. LEFT JOIN: соединить заказы с клиентами

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

Заказ O020 с клиентом 999 получает подпись «Клиент не найден». С INNER JOIN эта строка исчезла бы. Иногда такое исключение действительно нужно, но оно должно быть осознанным. Для отдельного диагностического списка добавьте условие WHERE c.client_id IS NULL; затем уточните, неполон ли справочник или неверен ключ в заказе.

Есть и обратная ловушка: неуникальный ключ в справочнике размножает строки заказов и увеличивает суммы. Перед JOIN проверьте уникальность clients.client_id. Разные названия столбцов и типы тоже требуют внимания: строка с ведущими нулями может быть кодом, а не числом. Явное решение о типах и кардинальности связи полезнее, чем попытка исправить итог случайным DISTINCT.

SELECT o.order_id, o.client_id,
 coalesce(c.name, 'Клиент не найден') AS client,
 coalesce(c.segment, 'Неизвестно') AS segment,
 o.amount_minor, o.status
FROM orders_clean o
LEFT JOIN clients c ON o.client_id = c.client_id
ORDER BY o.order_id
SELECT client_id, count(*) AS duplicates
FROM clients GROUP BY client_id HAVING count(*) > 1;

Пример 5. Оконные функции: рейтинг и накопленная сумма

GROUP BY сжимает набор до одной строки на группу. Оконные функции добавляют вычисление к строкам результата: место в рейтинге, предыдущую величину, накопленную сумму или долю от общего итога. Поэтому они подходят для KPI, сравнения периодов и списков с дополнительными показателями.

В примере сначала строится выручка менеджеров, затем DENSE_RANK присваивает место по убыванию суммы. При равной выручке место будет одинаковым. Накопительная сумма использует явную рамку ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW и дополнительную сортировку по имени, чтобы порядок при равенстве оставался определённым.

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

WITH totals AS (
 SELECT manager, sum(amount_minor)::BIGINT AS revenue_minor
 FROM orders_clean WHERE status='paid' GROUP BY manager
)
SELECT manager, revenue_minor,
 dense_rank() OVER (ORDER BY revenue_minor DESC)::INTEGER AS place,
 sum(revenue_minor) OVER (ORDER BY revenue_minor DESC, manager
 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)::BIGINT AS running_minor
FROM totals ORDER BY revenue_minor DESC, manager

Пример 6. Вложенный JSON из API

JSON удобен для передачи событий и ответов API, но для отчёта часто нужен табличный срез. DuckDB может прочитать массив объектов и представить вложенные объекты структурированными полями. В учебных событиях канал и ожидаемая сумма находятся внутри context; обращение context.channel выбирает вложенное значение без ручного цикла JavaScript.

Запрос группирует события по каналу и считает сумму pipeline_minor. Это сумма потенциальных работ из событий, а не выручка orders_clean. Разные источники метрик нельзя складывать только потому, что у них одинаковые единицы. Уточняйте, отражает событие новый лид, изменение существующего лида или повторную запись.

Для неоднородного production JSON задавайте схему и проверяйте отсутствующие поля. Массив JSON, построчный NDJSON и объект с вложенным массивом — разные входные структуры. Автоматическое чтение удобно для знакомства, но регулярная интеграция требует контракта. В мастерской можно загрузить небольшой собственный файл и начать с SELECT * FROM uploaded LIMIT 50, чтобы увидеть получившиеся поля.

SELECT context.channel AS channel,
 count(*)::INTEGER AS events,
 sum(context.value_minor)::BIGINT AS pipeline_minor
FROM events GROUP BY context.channel
ORDER BY pipeline_minor DESC, channel
SELECT * FROM read_json('events.json', format='array');

Пример 7. Parquet: схема, столбцы и проверка результата

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

В мастерской orders.parquet действительно читается расширением Parquet и превращается в отдельную таблицу parquet_orders. Запрос сравнивает число строк и общую сумму с очищенным CSV. Совпадающие показатели полезны как быстрая проверка, но два разных набора могут иметь одинаковую сумму. В тестах дополнительно используется EXCEPT ALL в обе стороны.

Для повторных аналитических задач можно один раз нормализовать CSV, сохранить Parquet и дальше работать с типизированной выгрузкой. Исходный файл и правила преобразования при этом стоит сохранить. В нашем наборе Parquet создан нативным DuckDB с совместимым форматом V1 без сжатия для прозрачности примера; в рабочем проекте отдельно выбирайте кодек и измеряйте результат.

SELECT 'CSV → очищенная таблица' AS source,
 count(*)::INTEGER AS rows, sum(amount_minor)::BIGINT AS total_minor
FROM orders_clean
UNION ALL
SELECT 'Parquet → таблица', count(*)::INTEGER, sum(amount_minor)::BIGINT
FROM parquet_orders
COPY orders_clean TO 'orders.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);

Форматы, сжатие и особенности чтения описаны в документации Parquet. Команды чтения файлов и COPY выполняются нативным примером или на этапе импорта; редактор мастерской после загрузки работает только с уже созданными таблицами.

Пример 8. Параметры периода и минимальной суммы

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

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

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

SELECT order_id, order_date::VARCHAR AS day, city, amount_minor
FROM orders_clean
WHERE status='paid' AND order_date >= CAST($1 AS DATE)
 AND order_date < CAST($2 AS DATE) AND amount_minor >= CAST($3 AS BIGINT)
ORDER BY order_date, order_id
const statement = await connection.prepare(sql);
try {
  const table = await statement.query('2026-10-01', '2026-10-08', '100000');
  // Render Arrow rows without converting BIGINT to Number.
} finally {
  await statement.close();
}

Пример 9. Ежедневный отчёт и экспорт

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

В мастерской SQL скачивается отдельно от данных. JSON сохраняет большие целые строками, чтобы JavaScript не округлил их при выходе за безопасный диапазон Number. Потребитель результата должен знать этот контракт и преобразовывать значение осознанно. CSV добавляет BOM для удобного открытия UTF-8 в табличных программах и экранирует кавычки, разделители и переносы строк.

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

SELECT order_date::VARCHAR AS day,
 count(*)::INTEGER AS orders,
 sum(amount_minor)::BIGINT AS revenue_minor
FROM orders_clean WHERE status='paid'
GROUP BY order_date ORDER BY order_date
node bin/report.mjs export report.json
node bin/report.mjs export report.csv

Собственный CSV или JSON: что происходит при загрузке

Кнопка выбора файла использует браузерный File API. Файл читается в память текущей страницы и не отправляется на сервер. Для импорта принимаются CSV и JSON в UTF-8 размером до двух МиБ. Когда вы запускаете запрос после выбора файла, мастерская создаёт новый экземпляр базы и таблицу uploaded вместе с учебными таблицами.

CSV загружается строками: вы сами решаете, какие столбцы преобразовать через TRY_CAST. JSON использует определение структуры. Ошибка кодировки, неверный JSON или неподходящая форма данных должна быть исправлена во входном файле; автоматический импорт не заменяет договорённость о формате. Начните с небольшого обезличенного фрагмента, где легко проверить каждую строку.

После успешного импорта внешнее чтение файлов отключается, конфигурация блокируется. Поэтому SELECT FROM read_csv с новым путём из редактора не работает: редактор исследует созданные таблицы. При перезагрузке страницы собственный файл исчезает из памяти. Для постоянного хранения или большого объёма используйте нативный проект и контролируемые серверные пути, а не попытку расширить браузерное демо до полноценного хранилища.

SELECT * FROM uploaded LIMIT 50;
SELECT TRY_CAST(amount AS DECIMAL(18,2)) AS amount FROM uploaded;

Node.js: встроить расчёт в своё приложение

В исходниках официальный Node API создаёт базу в памяти, читает фиксированные демонстрационные файлы и выполняет выбранный сценарий. Это удобная отправная точка для локального отчёта, команды администрирования или фонового задания. Результат можно сохранить на диск, передать существующему API или использовать для генерации документа.

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

Не превращайте этот пример в открытый HTTP-метод «выполнить любой SQL». Нативный движок имеет доступ к файловой системе и ресурсам процесса. Для пользовательского интерфейса оставьте на сервере утверждённые запросы, проверяйте параметры и права, ограничивайте объём результата. Если нужен произвольный аналитический редактор для сотрудников, его изоляция и модель доступа требуют отдельного проектирования.

import duckdb from '@duckdb/node-api';
const instance = await duckdb.DuckDBInstance.create(':memory:');
const connection = await instance.connect();
try {
  const reader = await connection.runAndReadAll(
    'SELECT $1::INTEGER AS answer', [42]
  );
  console.log(reader.getRowObjectsJson());
} finally {
  connection.closeSync();
  instance.closeSync();
}

Способы чтения, типы и подготовленные выражения описаны в официальном руководстве Node Neo.

Деньги, большие идентификаторы и точность

В аналитическом отчёте ошибка точности бывает незаметнее синтаксической ошибки. JavaScript Number не представляет все целые числа после 2⁵³ − 1. Если длинный идентификатор сначала превратить в Number, а потом в строку, исходное значение уже не восстановится. Поэтому мастерская сохраняет BIGINT строкой при экспорте JSON. DECIMAL в браузере также становится точной строкой с масштабом из схемы Arrow; специальные значения NaN и Infinity обозначаются строками, поскольку обычный JSON их не поддерживает.

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

Тип данных должен соответствовать смыслу поля. Телефон, номер договора и код товара часто лучше хранить текстом, даже если они состоят из цифр. У них бывают ведущие нули, разделители и буквы. Добавьте в свой проверочный набор такие значения заранее: маленькие удобные числа не обнаружат проблему, которая возникнет на реальной выгрузке. Деление DECIMAL в DuckDB даёт результат с плавающей точкой; для точного примера ниже используется умножение на десятичный коэффициент, а тип можно проверить через typeof().

SELECT 9007199254740993::BIGINT AS exact_id,
       12345::DECIMAL(18,0) * 0.01::DECIMAL(3,2) AS amount_rub;

Поведение десятичных операций описано в документации числовых типов DuckDB.

Ошибки, ограничения и настоящая отмена запроса

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

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

Ограничение memory_limit задаёт лимит управляемой памяти движка, но не гарантирует такой же предел для всей вкладки. Есть память JavaScript, загруженные файлы, Arrow-результат и интерфейс. Для больших JOIN и оконных вычислений сначала уменьшите набор данных, затем перенесите задачу в нативное окружение. Не увеличивайте все лимиты одновременно, не измерив причину замедления.

SET memory_limit = '256MB';
SET threads = 1;
SET enable_external_access = false;
SET lock_configuration = true;

Эти параметры дополняют архитектуру ограничений, но не заменяют изоляцию серверного процесса. Подробности — в руководстве по безопасности DuckDB.

Большие файлы и EXPLAIN: как искать узкие места

Первый полезный шаг — понять, что именно вы читаете и возвращаете. Выбирайте нужные столбцы вместо SELECT *, ограничивайте период и проверяйте размер соединения таблиц. Небольшой итог может строиться по огромному промежуточному набору, особенно если ключ JOIN оказался неуникальным. LIMIT в самом конце не всегда спасает от дорогой агрегации перед ним.

EXPLAIN показывает план, а EXPLAIN ANALYZE действительно выполняет запрос и добавляет измерения. Сравнивайте одинаковые входные файлы и запросы, учитывайте прогретый файловый кеш и время чтения. Не делайте вывод «Parquet всегда быстрее» по одному крошечному файлу: важны набор столбцов, фильтрация, кодек и расположение данных.

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

EXPLAIN ANALYZE
SELECT city, sum(amount_minor)
FROM read_parquet('orders.parquet')
WHERE status = 'paid' GROUP BY city;

-- Native server process only; choose existing writable paths.
SET temp_directory = '/srv/duckdb-temp';
SET max_temp_directory_size = '2GB';

Практический отчёт на VPS: вход, расчёт и публикация

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

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

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

# Check the actual Node path first:
command -v node
# Example crontab, when Node is /usr/bin/node:
15 3 * * * cd /srv/duckdb-job && /usr/bin/node bin/report.mjs export /srv/reports/orders-$(date +\%F).json >> /srv/reports/job.log 2>&1

Как разместить мастерскую на своём сайте

После npm run build папка public содержит готовый интерфейс, Worker, WASM, локальные расширения, данные и лицензии. Для просмотра не нужен PHP-обработчик: SQL выполняется в браузере. PHP или Node на сервере понадобятся уже для ваших задач доступа к реальным данным, авторизации и фоновых отчётов.

Раздавайте каталог через HTTPS и проверьте MIME-типы. JavaScript должен приходить как исполняемый модуль, WASM — как application/wasm. Если вместо расширения сервер возвращает страницу ошибки или авторизации, кнопки интерфейса могут выглядеть нормально, но движок не запустится. Проверяйте именно выполнение SQL на опубликованном адресе и скачивание архива, а не только открытие HTML.

Для готового MVP bundle не требуется включать изоляцию COOP/COEP для всего WordPress-сайта. Архив должен находиться на уровень выше public под именем duckdb-project.zip, либо измените ссылку в исходнике. Примеры настройки серверов есть в материалах про Caddy и Nginx. Новую конфигурацию сначала проверяют, затем применяют reload без замены посторонних настроек.

data.example.org {
    root * /srv/duckdb/public
    encode zstd gzip
    file_server
}

Исходники, тесты и правила изменения проекта

В src находятся интерфейс, общие ограничения и девять сценариев. В fixtures — CSV, JSON, Parquet и ожидаемые результаты. tools содержит сборку, создание учебных данных и нативный адаптер; bin/report.mjs запускает готовый отчёт. В public/source сохранены удобные для просмотра копии исходников, а полный редактируемый проект находится в архиве.

Нативные тесты выполняют все девять запросов, проверяют контрольные суммы, потерянную связь клиента, идентичность Parquet и CSV в обе стороны, границы периода, передачу параметров, BIGINT, DECIMAL и ошибки. Отдельно проверяются ограничения ввода, CSV-экспорт, запрет внешнего чтения и изменения заблокированных настроек. Проверки обработчиков интерфейса воспроизводят остановку во время загрузки, позднее чтение файла, восстановление параметров и запрет скачивания устаревшего результата. Они дополняют реальную браузерную приёмку. Обычный запуск тестов не переписывает ожидаемые результаты: изменение эталонов требует явного действия и проверки смысла.

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

npm run build
npm test
node bin/report.mjs summary
# Regenerate demonstration files only in your own copy:
npm run fixtures
# Intentional replacement of expected results after review:
node tools/fixtures.mjs --accept

Частые вопросы и полезные направления для продолжения

Почему после выбора файла нет таблицы uploaded? Файл импортируется при следующем запуске базы: нажмите «Выполнить SQL» и прочитайте сообщение, если импорт не удался. Почему read_csv не работает в редакторе? После контролируемой загрузки внешний доступ отключён; исследуйте созданные таблицы либо переносите операцию в локальный нативный проект.

Почему сумма в JSON заключена в кавычки? Это договорённость экспорта BIGINT, сохраняющая точность. Почему столбец CSV стал VARCHAR? Сырой импорт намеренно сохраняет текст, а преобразование выполняется отдельно. Почему диаграмма пропала после изменения SQL? Она строится только для подходящего числового показателя выбранного сценария; таблица остаётся главным точным результатом запроса.

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