Learning Platform
Глоссарий Troubleshooting
Урок 12.02 · 22 мин
Средний
arrowzero-copypythoninterop

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. Это сериализация плюс копирование: процессорное время на перекладку и удвоенный расход памяти — данные временно существуют в обоих форматах сразу.

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

Передача данных с копированием против zero-copy
С копированиемФормат A читается значение за значением, конвертируется в формат B, размещается в новой памяти — процессорное время и удвоенная память
zero-copy убирает этот шаг
Zero-copyОбе системы используют один формат памяти и смотрят на одни и те же буферы — перекладывать нечего

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]]
Arrow как общий мост памяти
Polars / PyArrowPython-библиотеки хранят данные в формате Arrow в памяти процесса
передача указателя
DuckDB читает те же буферыДвижок DuckDB читает чужие Arrow-буферы напрямую — формат общий, конвертация не нужна
.arrow()
Результат как ArrowРезультат запроса отдаётся как Arrow-структура поверх буферов DuckDB, без сериализации

Где 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 -> DuckDBZero-copy: передача указателя на буферы
DuckDB -> .arrow() / .pl()Zero-copy: результат поверх буферов DuckDB
Pandas (числовые колонки) -> DuckDBОбычно без копирования
Pandas (строковые колонки) -> DuckDBВозможна лёгкая конвертация представления
Через клиент-серверную СУБДСериализация плюс сеть плюс десериализация
NOTE

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.

  1. Создайте крупный PyArrow Table (миллионы строк — например через pa.table поверх диапазона). Выполните над ним агрегирующий SQL-запрос в DuckDB и заберите результат через .arrow(). Прочувствуйте, что вход и выход прошли границу без сериализации.
  2. Сделайте то же с Polars DataFrame, забрав результат через .pl(). Сравните, насколько единообразно ощущается работа с Arrow-нативными библиотеками.
  3. Создайте большой Pandas DataFrame с числовой и со строковой колонкой. Прогоните запрос. Подумайте, какая из колонок переносится в DuckDB без копирования, а для какой может понадобиться конвертация представления — и почему.
  4. Сформулируйте словами: что конкретно НЕ копируется при zero-copy, а что DuckDB всё равно строит в своей памяти при исполнении запроса. Это граница между «передачей данных» и «обработкой».

Apache Arrow: единый стандарт памяти для аналитических систем
Проверка знанийKnowledge check
Что такое zero-copy interop между DuckDB и Python, за счёт чего Apache Arrow его обеспечивает, и в каких случаях передача данных всё-таки требует конвертации?
ОтветAnswer
Zero-copy interop — это передача данных между DuckDB и Python-библиотеками без копирования: обе стороны смотрят на одни и те же байты в памяти. Чтобы понять, что это даёт, нужно увидеть, чего оно избегает. Обычная передача таблицы между двумя системами с разными внутренними форматами требует сериализации и копирования: данные проходятся значение за значением, конвертируются в формат принимающей системы и размещаются в новой области памяти — это процессорное время на перекладку и удвоенный расход памяти, а для клиент-серверной СУБД ещё и кодирование в сетевой протокол с пересылкой. Apache Arrow обеспечивает zero-copy тем, что задаёт единый стандарт расположения колоночных данных в оперативной памяти — побайтово точную спецификацию того, как лежит колонка чисел, колонка строк, как кодируются NULL. Если две системы обе хранят данные в формате Arrow, передача сводится к передаче указателя: принимающая сторона получает адрес буферов и читает их напрямую, потому что формат уже общий — конвертировать нечего. И DuckDB (внутри колоночный), и Polars (хранит данные в Arrow нативно), и PyArrow (реализация Arrow) говорят на Arrow, поэтому replacement scan читает DataFrame, беря ссылку на его Arrow-буферы, а метод .arrow() отдаёт результат запроса как Arrow-структуру поверх буферов DuckDB. Конвертация всё же требуется на границе с тем, что не является чистым Arrow: классический Pandas на NumPy-массивах не во всех случаях совместим с Arrow побайтово — числовые колонки фиксированной ширины обычно переносятся без копирования, а строковые колонки в NumPy-представлении хранятся иначе, чем в Arrow, и для них может понадобиться лёгкая конвертация. Поэтому самый чистый zero-copy даёт связка DuckDB с Polars или PyArrow. Важно, что zero-copy экономит на передаче входа и выхода через границу DuckDB и Python, но не на самой обработке: промежуточные структуры запроса — хеш-таблицы для GROUP BY, результат JOIN, отсортированные данные — DuckDB всё равно строит в своей памяти.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое zero-copy interop между DuckDB и Python-библиотеками?

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

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

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

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