COPY: экспорт в Parquet, CSV и JSON
Аналитика не заканчивается запросом — результат нужно куда-то отдать: в файл для коллеги, в data lake для следующего пайплайна, в формат, который поймёт другой инструмент. Команда COPY — основной механизм DuckDB для выгрузки данных в файлы. Этот урок разбирает COPY для экспорта в три главных формата — Parquet, CSV и JSON — и опции, которыми управляют выводом.
COPY симметрична: ею можно и читать файлы в таблицу, и писать таблицы в файлы.
Parquet: Pages — минимальные единицы кодирования Этот урок — про запись (COPY ... TO); чтение внешних файлов разбиралось в модуле про внешние данные.
Базовый синтаксис COPY … TO
Общая форма: COPY <источник> TO '<путь>' (опции). Источник — таблица или результат запроса. Путь — файл назначения. Опции в скобках управляют форматом.
-- экспорт таблицы целиком в Parquet
COPY sales TO 'sales.parquet' (FORMAT parquet);
-- экспорт результата запроса, а не всей таблицы
COPY (
SELECT region, sum(amount) AS total
FROM sales
GROUP BY region
) TO 'region_totals.parquet' (FORMAT parquet);
-- в CSV
COPY sales TO 'sales.csv' (FORMAT csv, HEADER true);
-- в JSON
COPY sales TO 'sales.json' (FORMAT json);
Возможность экспортировать результат произвольного запроса — ключевая. Не нужно создавать промежуточную таблицу: фильтрация, агрегация, join делаются прямо в COPY (...), и на диск ложится уже готовый результат.
DuckDB также понимает сокращённую запись без явного FORMAT — формат выводится из расширения файла:
-- формат определён по расширению .parquet
COPY sales TO 'sales.parquet';
Выбор формата: Parquet, CSV или JSON
Три формата решают три разные задачи. Выбор не вкусовой — у каждого своя ниша.
| Формат | Сильные стороны | Когда выбирать |
|---|---|---|
| Parquet | колоночный, сжатый, со схемой и статистикой; компактный и быстрый для повторного чтения | data lake, аналитические пайплайны, промежуточные данные, обмен между движками |
| CSV | текстовый, читается человеком и любым инструментом | выгрузка для не-технических пользователей, импорт в системы, понимающие только CSV |
| JSON | текстовый, представляет вложенные структуры | API-обмен, данные с вложенностью, интеграция с JSON-ориентированными системами |
Parquet — формат по умолчанию для аналитики. Он колоночный, как и хранилище DuckDB; данные сжаты; в файл встроены схема (типы колонок) и статистика (min/max по row group). При повторном чтении такого файла работают projection pushdown и filter pushdown — читаются только нужные колонки и пропускаются row group по статистике. Для промежуточных данных между шагами пайплайна и для data lake Parquet — правильный выбор почти всегда.
CSV — для совместимости, не для производительности. Текстовый формат: больше по объёму (нет сжатия по умолчанию, числа хранятся как текст), медленнее парсится, не несёт типов. Зато его открывает что угодно — Excel, любой язык, любая БД. Выбирайте CSV, когда на другом конце система или человек, понимающие только CSV.
JSON — для вложенных данных и интеграции. Текстовый, но в отличие от плоского CSV умеет представлять STRUCT, LIST, вложенность. Нужен, когда данные иерархичны или принимающая сторона ждёт JSON.
Практическое правило: между шагами своего пайплайна и в data lake — всегда Parquet (компактно, быстро, типизированно, поддерживает pushdown при чтении). CSV и JSON — только на границе с внешним миром, где требуется текстовый формат. Не используйте CSV как формат промежуточного хранения внутри пайплайна — это потеря и в скорости, и в объёме.
Опции формата
Каждый формат принимает свои опции в скобках. Разберём самые употребимые.
Опции CSV:
COPY sales TO 'sales.csv' (
FORMAT csv,
HEADER true, -- писать строку заголовка с именами колонок
DELIMITER ';', -- разделитель полей (по умолчанию запятая)
QUOTE '"', -- символ кавычки для значений с разделителем
NULL 'NULL' -- чем представлять NULL в тексте
);
HEADER true — почти всегда нужно: без заголовка получатель не знает имён колонок. DELIMITER меняют под локальные соглашения (в некоторых регионах принята точка с запятой). NULL задаёт текстовое представление пустых значений.
Опции Parquet:
COPY sales TO 'sales.parquet' (
FORMAT parquet,
COMPRESSION zstd, -- кодек сжатия: snappy, gzip, zstd, lz4
ROW_GROUP_SIZE 122880 -- число строк в одной row group
);
COMPRESSION выбирает кодек: snappy — быстрый, умеренное сжатие; zstd — лучше сжимает, чуть медленнее; обычно zstd — хороший баланс. ROW_GROUP_SIZE задаёт размер row group — единицы, по которой при чтении работает пропуск по статистике; значение по умолчанию разумно, менять его стоит осознанно.
Опции JSON:
COPY sales TO 'sales.json' (
FORMAT json,
ARRAY true -- весь файл как один JSON-массив; иначе newline-delimited JSON
);
ARRAY true пишет файл как единый JSON-массив объектов; по умолчанию — newline-delimited JSON (ndjson), где каждая строка — отдельный JSON-объект. ndjson удобнее для потоковой обработки, единый массив — для инструментов, ждущих цельный JSON-документ.
COPY против EXPORT DATABASE
Стоит сразу разграничить два механизма, чтобы не путать.
COPY ... TO выгружает одну таблицу или один результат запроса в один файл. Это инструмент для выборочного экспорта: «отдать вот этот результат вот в этот файл».
EXPORT DATABASE (отдельный урок модуля) выгружает всю базу целиком — все таблицы плюс SQL-схему — в каталог. Это инструмент бэкапа и миграции всей базы.
Под капотом EXPORT DATABASE использует COPY для каждой таблицы — но добавляет выгрузку схемы и организует всё как единый дамп.
COPY … FROM: симметрия чтения
COPY симметрична. У неё есть и обратная форма — COPY ... FROM, которая читает файл в существующую таблицу:
-- загрузить CSV в уже созданную таблицу
COPY sales FROM 'sales.csv' (FORMAT csv, HEADER true);
-- загрузить Parquet
COPY sales FROM 'sales.parquet' (FORMAT parquet);
Разница с INSERT INTO ... SELECT FROM 'file' из прошлого урока в основном стилистическая: COPY ... FROM — это исторический SQL-синтаксис массовой загрузки (он пришёл ещё из PostgreSQL), а INSERT ... SELECT — более гибкий, потому что между файлом и таблицей можно вставить любые преобразования. Для прямой загрузки «файл как есть в таблицу» обе формы равноценны; когда нужна фильтрация или трансформация на лету — выбирают INSERT ... SELECT. Важно, что COPY целиком — это один инструмент с двумя направлениями: TO пишет, FROM читает, и опции формата у обоих одинаковые.
COPY и параллелизм
COPY ... TO для одного файла DuckDB выполняет эффективно: данные читаются из источника векторизованно и пишутся потоково. Запись в один Parquet-файл хорошо оптимизирована, и для умеренных объёмов один файл — нормальный выбор.
Когда нужно записать очень большой объём, есть смысл писать не в один файл, а в несколько файлов с партиционированием по колонке. У этого две выгоды. Первая — запись: разные партиции пишутся параллельно, что лучше утилизирует и CPU, и диск. Вторая — чтение: партиционированный набор поддерживает partition pruning, и будущие запросы с фильтром по колонке партиционирования прочитают лишь часть данных. То есть выбор «один файл против многих партиций» — это не только про скорость записи сейчас, но и про скорость чтения потом.
Партиционированной записи (COPY ... PARTITION_BY) посвящён следующий урок — он развивает тему COPY именно в эту сторону.
Попробуй сам
- Создай таблицу
t AS SELECT range AS id, range % 7 AS grp, random() AS val FROM range(500000); - Выгрузи её в три формата:
COPY t TO 'out.parquet' (FORMAT parquet);,COPY t TO 'out.csv' (FORMAT csv, HEADER true);,COPY t TO 'out.json' (FORMAT json);Сравни размеры файлов на диске — какой компактнее и почему? - Выгрузи не всю таблицу, а результат запроса:
COPY (SELECT grp, count(*) AS cnt, avg(val) AS a FROM t GROUP BY grp) TO 'summary.parquet';Прочитай его обратно:SELECT * FROM 'summary.parquet'; - Поэкспериментируй с опциями CSV: выгрузи
tсDELIMITER ';'и сHEADER false. Открой файлы текстовым редактором — как изменился вывод? - Выгрузи
tв Parquet сCOMPRESSION zstdи сCOMPRESSION snappyв два разных файла. Сравни их размеры. Затем подумай, почему для промежуточных данных пайплайна Parquet предпочтительнее CSV сразу по двум причинам.
Сжатие данных: zstd, Snappy, LZ4 — trade-off скорость против степени