Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 10.05 · 21 мин
Средний
arrowexcelunion-by-nameexternal-data

Arrow, Excel и union_by_name

Parquet, CSV и JSON покрывают большую часть внешних данных, но не всё. Аналитический пайплайн на Python обменивается данными через Apache Arrow. Бизнес-данные постоянно приходят в виде Excel-таблиц. А ещё файлы одного датасета часто имеют слегка разные схемы — в марте добавили колонку, в июне переименовали. Этот урок закрывает три темы: чтение Arrow, чтение Excel и union_by_name — механизм объединения разнородных схем, который пригодится со всеми форматами модуля.

Apache Arrow: формат памяти, а не диска

Arrow — это в первую очередь стандарт колоночного представления данных в оперативной памяти. Его главная идея: разные системы (DuckDB, Pandas, Polars, Spark) договариваются хранить колонки в памяти байт в байт одинаково. Тогда передать таблицу из одной системы в другую можно без сериализации и без копирования — обе просто смотрят на одни и те же байты. Это называется zero-copy, и подробно мы разберём его в модуле про Python-экосистему.

У Arrow есть и файловые формы. Arrow IPC (Inter-Process Communication), он же Feather v2, — это раскладка Arrow, записанная на диск или переданная по сети. Бывает двух видов: формат файла (с footer, читается в любом порядке) и формат потока (stream, читается последовательно). DuckDB читает оба.

Поскольку и DuckDB, и Arrow используют колоночное представление, чтение Arrow IPC получается особенно дешёвым: структура данных уже та, что нужна движку, перекладывать формат почти не требуется.

-- Чтение Arrow IPC-файла
SELECT * FROM read_arrow('data.arrow') LIMIT 5;

-- Короткая форма тоже работает: replacement scan по расширению
SELECT COUNT(*) FROM 'data.arrow';
Arrow как общий формат памяти
Pandas / PolarsPython-библиотеки держат колонки в памяти в формате Arrow
те же байты
Arrow в памятиКолоночное представление, на которое смотрят все системы; копирования нет
те же байты
DuckDBДвижок читает те же колоночные буферы напрямую — структура уже подходящая
NOTE

Не путайте Arrow и Parquet. Parquet — формат для долговременного хранения на диске: сжатый, с footer-статистикой, оптимизирован под объём. Arrow — формат для памяти и быстрого обмена: несжатый или легко сжатый, оптимизирован под скорость доступа и нулевое копирование. Это не конкуренты, а слои: данные лежат на диске в Parquet, а в памяти и между процессами ходят в Arrow.

Excel: чтение и запись через расширение

Excel-файлы (.xlsx) DuckDB читает через core-расширение excel. Оно построено поверх библиотеки разбора xlsx и предоставляет функцию read_xlsx. Расширение autoloadable — при первом обращении к .xlsx подтянется само.

-- Прочитать лист Excel-файла
SELECT * FROM read_xlsx('report.xlsx') LIMIT 5;

-- Конкретный лист и диапазон ячеек
SELECT * FROM read_xlsx('report.xlsx', sheet = 'Q2', range = 'B2:F500');

Полезные опции read_xlsx: sheet — имя листа (по умолчанию первый), range — диапазон ячеек в нотации Excel, header — есть ли строка заголовка, all_varchar — читать все ячейки как текст без попыток вывести тип, ignore_errors — пропускать проблемные ячейки.

Excel — формат с особенностями, и о них надо знать заранее. Ячейки внутри одной колонки Excel могут иметь разный тип (где-то число, где-то текст), потому что Excel типизирует ячейки, а не столбцы. Даты Excel хранит как числа — серийные номера дней от эпохи, — и расширение их распознаёт, но экзотические форматы могут потребовать ручной обработки. Объединённые ячейки, формулы и пустые строки тоже могут удивить.

DuckDB умеет и писать Excel — через COPY ... TO 'file.xlsx':

COPY (SELECT carrier, COUNT(*) AS flights FROM 'flights.parquet' GROUP BY carrier)
TO 'summary.xlsx' WITH (FORMAT xlsx, HEADER true);
WARNING

Excel — плохой формат для больших данных и для пайплайнов. У xlsx есть жёсткий предел около 1 048 576 строк на лист, разбор медленный, типы ненадёжны. Используйте Excel как точку входа (данные пришли от бизнеса) и точку выхода (отчёт для человека), но всю обработку между ними ведите в Parquet. Не делайте Excel промежуточным форматом пайплайна.

Проблема разнородных схем

Теперь центральная тема урока. Вы читаете датасет из многих файлов — read_parquet('sales/*.parquet'). По умолчанию DuckDB предполагает, что у всех файлов одинаковая схема: те же колонки, тех же типов, в том же порядке. Файлы он объединяет по позиции — первая колонка к первой, вторая ко второй.

Это быстро, но хрупко. Реальные датасеты эволюционируют. В файле за март 4 колонки, в файле за июнь добавили пятую — discount. Или колонку client переименовали в customer. Или в одном файле порядок колонок другой, потому что его писал другой инструмент. При объединении по позиции это даёт два исхода, оба плохие:

  • Число колонок разное — запрос падает с ошибкой несовпадения схемы.
  • Число колонок совпало, но смысл колонок разъехался — DuckDB молча подставит данные discount под колонку region, потому что они оказались на одной позиции. Запрос отработает и выдаст мусор.

Второй случай опаснее: ошибки нет, есть тихо испорченные данные.

Объединение по позиции против объединения по имени
По позиции (по умолчанию)Колонки сопоставляются по порядковому номеру; при разном порядке или составе колонок данные смешиваются или запрос падает
union_by_name => true
По имениКолонки сопоставляются по именам; недостающие в файле колонки заполняются NULL, порядок не важен

union_by_name: объединение по именам колонок

Решение — параметр union_by_name => true. С ним DuckDB сопоставляет колонки разных файлов по именам, а не по позициям. Итоговая схема становится объединением всех колонок, встреченных во всех файлах. Если в каком-то файле колонки не было, в её ячейках для строк этого файла будет NULL. Порядок колонок в файлах перестаёт иметь значение.

Файл 2026-03.parquet (4 колонки): order_id, region, amount, client. Файл 2026-06.parquet (5 колонок): order_id, region, amount, client, discount.

-- Без union_by_name: ошибка — у файлов разное число колонок
-- SELECT * FROM read_parquet('sales/*.parquet');

-- С union_by_name: схемы объединяются по именам
SELECT order_id, amount, discount
FROM read_parquet('sales/*.parquet', union_by_name = true);
┌──────────┬─────────┬──────────┐
│ order_id │ amount  │ discount │
│  int64   │ double  │  double  │
├──────────┼─────────┼──────────┤
│     1001 │  240.00 │     NULL │   <- из мартовского файла, discount не было
│     1002 │  118.50 │     NULL │
│     5500 │  300.00 │    30.00 │   <- из июньского файла, discount есть
└──────────┴─────────┴──────────┘

union_by_name — это общий механизм, он не привязан к одному формату. Параметр принимают read_parquet, read_csv и read_json. Для CSV и JSON он особенно ценен: там у файлов разных периодов набор колонок расходится постоянно.

СвойствоПо позиции (по умолчанию)union_by_name = true
Сопоставление колонокПо порядковому номеруПо имени
Разный порядок колонокДанные смешиваютсяОбрабатывается корректно
Колонка есть не вездеОшибка или мусорЗаполняется NULL
СтоимостьМинимальнаяЧтение схемы каждого файла
Когда применятьФайлы гарантированно идентичныСхемы эволюционируют
TIP

У union_by_name есть цена: DuckDB должен прочитать схему (footer для Parquet, выборку для CSV/JSON) каждого файла, чтобы построить объединение. На датасете из тысяч файлов это заметная задержка на старте запроса. Поэтому включайте union_by_name осознанно — когда схемы действительно расходятся, а не «на всякий случай». Если все файлы гарантированно идентичны, объединение по позиции быстрее.

Попробуй сам

Смоделируйте эволюцию схемы датасета.

  1. Создайте march.parquet с колонками id, region, amount и june.parquet с колонками id, region, amount, discount (через COPY (SELECT ...) TO ...). Положите оба в одну папку.
  2. Выполните SELECT * FROM read_parquet('folder/*.parquet') без union_by_name и получите ошибку несовпадения схемы. Прочитайте текст ошибки.
  3. Повторите с union_by_name = true. Убедитесь, что в строках из мартовского файла колонка discount равна NULL, а из июньского — заполнена.
  4. Создайте третий файл, где те же три колонки идут в порядке amount, id, region. Прочитайте все три файла сначала без union_by_name (посмотрите, как разъехались данные), потом с ним (данные встали по местам). Это наглядно показывает разницу между сопоставлением по позиции и по имени.
Apache Arrow: zero-copy обмен данными между системами
Проверка знанийKnowledge check
Зачем нужен параметр union_by_name при чтении нескольких файлов как одной таблицы, и какой опасный сценарий он предотвращает по сравнению с поведением по умолчанию?
ОтветAnswer
По умолчанию при чтении нескольких файлов как одной таблицы (например через glob-шаблон) DuckDB предполагает, что у всех файлов одинаковая схема, и объединяет их колонки по позиции — первая колонка одного файла сопоставляется с первой колонкой другого, вторая со второй и так далее. Параметр union_by_name => true меняет это поведение: колонки сопоставляются по именам, итоговая схема становится объединением всех колонок из всех файлов, а там, где в конкретном файле колонки не было, её ячейки заполняются значением NULL; порядок колонок в файлах перестаёт иметь значение. Опасный сценарий, который union_by_name предотвращает, — это тихое искажение данных при объединении по позиции. Если у файлов разный состав или порядок колонок, но число колонок случайно совпало, DuckDB при объединении по позиции подставит данные одной колонки под имя другой — например значения колонки discount окажутся в колонке region просто потому, что они стоят на одной позиции. Запрос при этом не упадёт с ошибкой, он отработает успешно и выдаст внешне правдоподобный, но неверный результат. Это опаснее, чем явная ошибка несовпадения числа колонок, потому что испорченные данные легко не заметить. union_by_name сопоставляет колонки по смыслу (по имени), и такой класс ошибок исчезает. Параметр работает с read_parquet, read_csv и read_json, но имеет цену — DuckDB должен прочитать схему каждого файла, поэтому на датасете из тысяч файлов включать его стоит только когда схемы действительно расходятся.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6