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 из 0
Концептуальный
Вопрос 1 из 4. В чём принципиальное различие назначения форматов Arrow и Parquet?

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

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

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

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