Zero-copy interop через Arrow
В прошлом уроке мы видели: DuckDB читает Pandas/Polars/PyArrow DataFrame прямо по имени переменной, и делает это дёшево. Слово «дёшево» требует объяснения. По-настоящему дёшево означает zero-copy — данные не копируются вообще. DuckDB и Python-библиотека смотрят на одни и те же байты в памяти. Этот урок про то, как такое возможно, потому что zero-copy — не оптимизация на полях, а фундамент производительности всей связки DuckDB и Python.
Что обычно стоит передача данных
Arrow Memory Layout: буферы, bitmap и RecordBatch PyArrow: Arrow memory model, zero-copy, Table и RecordBatchСначала про то, чего zero-copy избегает. Представьте передачу таблицы из системы A в систему B классическим способом. У A свой внутренний формат данных в памяти, у B — свой. Чтобы B прочитал данные A, нужно: пройти все данные A, для каждого значения построить представление в формате B, разместить это в новой области памяти B. Это сериализация плюс копирование: процессорное время на перекладку и удвоенный расход памяти — данные временно существуют в обоих форматах сразу.
Для клиент-серверной СУБД к этому добавляется сеть: данные ещё и кодируются в протокол передачи, идут через сокет, декодируются на другой стороне. На больших таблицах перекладка форматов и пересылка нередко стоят дороже самого запроса.
Apache Arrow: договор о формате памяти
Корень проблемы — у каждой системы свой формат. Решение очевидно: договориться об общем. Именно это и есть Apache Arrow — стандарт того, как колоночные данные располагаются в оперативной памяти. Arrow задаёт точно, байт в байт: как лежит колонка целых чисел, как — колонка строк, как кодируются NULL-значения, как описываются вложенные типы.
Если две системы обе хранят данные в формате Arrow, передача между ними сводится к передаче указателя. Система B получает адрес буферов системы A и читает их напрямую — конвертировать нечего, потому что формат уже общий. Это и есть zero-copy: ноль скопированных байт данных.
Ключевые свойства формата Arrow, которые делают это возможным:
- Колоночность. Значения одной колонки лежат непрерывным массивом. Это естественно и для DuckDB (колоночный движок), и для Polars, и для аналитики вообще.
- Точная спецификация раскладки. Не «примерно так», а побайтовый стандарт — поэтому чужие буферы можно читать без догадок.
- Разделяемость. Arrow-буфер описывается так, что на него может ссылаться кто угодно; владение и время жизни буфера управляются явно.
Как это работает в связке DuckDB и Python
Теперь соберём картину. И DuckDB, и ключевые Python-библиотеки говорят на Arrow:
- Polars хранит данные в Arrow нативно.
- PyArrow — это и есть реализация Arrow для Python.
- Pandas исторически имел свой формат (на основе NumPy), но умеет отдавать и принимать данные в Arrow-представлении; современные версии Pandas всё теснее интегрированы с Arrow.
- DuckDB внутри колоночный и умеет и читать данные из Arrow-буферов, и отдавать свой результат как Arrow.
Поэтому когда replacement scan из прошлого урока «читает» Polars DataFrame — он не перекладывает данные, он берёт ссылку на его Arrow-буферы. Когда вы забираете результат запроса методом .arrow() — DuckDB не сериализует результат, он отдаёт его как Arrow-структуру поверх своих буферов.
import duckdb
import pyarrow as pa
# PyArrow Table — данные уже в формате Arrow
tbl = pa.table({"city": ["Berlin", "Lyon", "Berlin"], "sales": [100, 200, 150]})
# Чтение tbl в запрос — zero-copy: DuckDB читает Arrow-буферы напрямую
res = duckdb.sql("SELECT city, SUM(sales) AS total FROM tbl GROUP BY city")
# Результат обратно как Arrow — тоже без сериализации
arrow_result = res.arrow()
print(arrow_result)
pyarrow.Table
city: string
total: int64
----
city: [["Berlin","Lyon"]]
total: [[250,200]]
Где zero-copy строгий, а где почти
Важно быть точным. Не каждая передача данных в этой связке абсолютно бесплатна — это зависит от форматов на обоих концах.
Строгий zero-copy получается, когда формат на обоих концах совместим с Arrow напрямую: Polars и PyArrow на стороне Python. Здесь действительно передаётся только указатель.
Почти zero-copy или лёгкая конвертация — на границе с тем, что не является чистым Arrow. Классический Pandas-DataFrame на NumPy-массивах не во всех случаях совместим с Arrow побайтово: например строковые колонки в NumPy-представлении хранятся иначе, чем в Arrow, и для них может потребоваться конвертация. Числовые колонки фиксированной ширины часто переносятся без копирования, строковые — не всегда.
Практический вывод: связка DuckDB плюс Polars или PyArrow даёт самый чистый zero-copy. Связка с Pandas всё равно очень быстра (Pandas всё лучше дружит с Arrow), но именно для абсолютного нуля копий предпочтительны Arrow-нативные библиотеки.
| Сценарий | Стоимость передачи |
|---|---|
| Polars / PyArrow -> DuckDB | Zero-copy: передача указателя на буферы |
DuckDB -> .arrow() / .pl() | Zero-copy: результат поверх буферов DuckDB |
| Pandas (числовые колонки) -> DuckDB | Обычно без копирования |
| Pandas (строковые колонки) -> DuckDB | Возможна лёгкая конвертация представления |
| Через клиент-серверную СУБД | Сериализация плюс сеть плюс десериализация |
Zero-copy не означает, что запрос ничего не считает. Скопированы не будут исходные данные — буферы DataFrame. Но промежуточные результаты запроса — хеш-таблицы для GROUP BY, результат JOIN, отсортированные данные — DuckDB всё равно строит в своей памяти, это работа движка. Zero-copy экономит именно на передаче входа и выхода через границу DuckDB и Python, а не на самой обработке.
Почему это важнее, чем кажется
Zero-copy — причина, по которой DuckDB ощущается «бесшовным» в Python, и это меняет архитектуру пайплайнов.
Без zero-copy каждый переход «Python -> SQL -> Python» облагался бы налогом на перекладку данных. Это вынуждало бы делать переходы редко: либо весь анализ в Pandas, либо весь в SQL, а смешивать дорого. С zero-copy налога нет. Можно строить пайплайн из коротких шагов, на каждом выбирая лучший инструмент: загрузка и тяжёлая агрегация — SQL в DuckDB; специфичная трансформация с Python-библиотекой — Polars или Pandas; снова агрегат — снова DuckDB. Данные ходят туда-сюда десятки раз, и это не стоит почти ничего.
То же касается интеграции DuckDB с экосистемой ML и аналитики: данные подаются в модель, в визуализацию, в другую Arrow-совместимую систему — и везде граница пройдена без копирования. Arrow выступает универсальным «языком памяти», а DuckDB — SQL-движком, который на этом языке свободно говорит. Эту тему мы продолжим в уроках про Relational API и про DuckDB в пайплайнах.
Попробуй сам
Для задания: pip install duckdb polars pyarrow pandas.
- Создайте крупный PyArrow Table (миллионы строк — например через
pa.tableповерх диапазона). Выполните над ним агрегирующий SQL-запрос в DuckDB и заберите результат через.arrow(). Прочувствуйте, что вход и выход прошли границу без сериализации. - Сделайте то же с Polars DataFrame, забрав результат через
.pl(). Сравните, насколько единообразно ощущается работа с Arrow-нативными библиотеками. - Создайте большой Pandas DataFrame с числовой и со строковой колонкой. Прогоните запрос. Подумайте, какая из колонок переносится в DuckDB без копирования, а для какой может понадобиться конвертация представления — и почему.
- Сформулируйте словами: что конкретно НЕ копируется при zero-copy, а что DuckDB всё равно строит в своей памяти при исполнении запроса. Это граница между «передачей данных» и «обработкой».
Apache Arrow: единый стандарт памяти для аналитических систем