Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 14 мин
Средний
Apache ArrowColumnar FormatRow FormatMemory LayoutIPCCross-Language

Apache Arrow: колоночный формат данных

Проблема: строчное хранение vs аналитика

Большинство OLTP-баз данных (PostgreSQL, MySQL) хранят данные по строкам. Это логично для транзакционной нагрузки: INSERT, UPDATE, DELETE работают с целой строкой. Но для аналитических запросов строчное хранение — катастрофа.

Допустим, у вас таблица employees с 6 колонками и 10 миллионов строк. Аналитический запрос SELECT AVG(salary) FROM employees читает только 1 колонку из 6. Но при строчном хранении с диска/из памяти вычитываются все 6 колонок — 83% прочитанных данных выбрасываются.

Row vs Columnar

SELECT AVG(salary) — нужна только 1 колонка из 6

Row-oriented (строчное хранение)
id=1name="Анна"age=28dept="IT"salary=95K
id=2name="Борис"age=34dept="HR"salary=78K
id=3name="Вера"age=41dept="IT"salary=112K
← читаем ВСЁ, 83% данных выбрасываются
Columnar (колоночное хранение)
id:[1, 2, 3]← пропускаем
name:[Анна, …]← пропускаем
age:[28, 34, 41]← пропускаем
dept:[IT, HR, IT]← пропускаем
salary:[95K, 78K, 112K]← читаем ТОЛЬКО это!
← читаем только salary

Apache Arrow: единый колоночный стандарт

Apache Arrow — это спецификация колоночного формата данных в оперативной памяти (in-memory). Ключевое отличие от Parquet: Parquet — формат хранения на диске, Arrow — формат для обработки в RAM.

Arrow определяет точный byte layout для каждого типа данных. Это означает, что данные в формате Arrow в Python, Java, C++, Rust и Go занимают одни и те же байты в памяти. Никакой сериализации при передаче между языками — просто передаём указатель на ту же память.

Почему колоночный формат лучше для аналитики

1. Column Pruning (отсечение колонок): При SELECT salary, age читаются только 2 буфера из 6. При строчном хранении читаются все 6.

2. SIMD-векторизация: Колоночные данные лежат в памяти последовательно (contiguous). Процессор может обработать 4-8 значений за одну SIMD-инструкцию (AVX2/AVX-512). Для строчных данных это невозможно — значения разбросаны по памяти.

3. Компрессия: Значения одного типа в одной колонке сжимаются значительно лучше. Run-Length Encoding для колонки dept с повторяющимися “IT”, “HR” даёт 10-50x сжатие. Для строчного формата это невозможно.

4. Cache locality: При обработке колонки salary все значения лежат рядом в RAM. CPU cache line (64 байт) загружает 8 значений int64 за раз. При строчном хранении каждое следующее значение salary находится через всю строку — cache miss на каждом обращении.

Row vs Columnar: Apache Arrow Memory Layout

Row-Oriented Storage

1
Анна
28
95000
2
Борис
34
78000
3
Вера
41
112000
4
Григорий
25
67000
5
Дарья
38
103000
6
Евгений
29
88000

Columnar Storage (Arrow)

id (int)
123456
name (string)
АннаБорисВераГригорийДарьяЕвгений
age (int)
283441253829
salary (double)
95000780001120006700010300088000
Row: байт прочитано168 байт
Columnar: байт прочитано168 байт
id (int)name (string)age (int)salary (double)

Arrow Memory Layout

Arrow определяет три типа буферов для каждой колонки:

Fixed-width буферы (числа, boolean)

Колонка age (int32), 4 значения: [28, 34, 41, NULL]

Validity Bitmap: [1, 1, 1, 0]  → 4-й элемент NULL
                  ↑  ↑  ↑  ↑
                  28 34 41 null

Data Buffer: [28 00 00 00 | 22 00 00 00 | 29 00 00 00 | 00 00 00 00]
              ← 4 bytes →   ← 4 bytes →   ← 4 bytes →   ← padding →

Каждое значение занимает ровно 4 байта (int32). Доступ к i-му элементу — O(1): buffer[i * 4].

Variable-width буферы (строки)

Колонка name (utf8), 3 значения: ["Анна", "Борис", "Вера"]

Offsets Buffer: [0, 8, 18, 26]  → где начинается каждая строка (UTF-8 байты)
Data Buffer:    [Анна|Борис|Вера]  → все строки подряд, без разделителей

Строка i: data[offsets[i] : offsets[i+1]]

Offsets buffer позволяет найти любую строку за O(1), а сами данные лежат компактно без gaps.

Validity Bitmap (обработка NULL)

Validity Bitmap для 8 значений: [1, 1, 0, 1, 1, 1, 0, 1]
Один бит на значение. 0 = NULL, 1 = valid.
В памяти: один байт = 0b10110111 = 8 значений
TIP

Arrow vs Parquet

Arrow и Parquet — не конкуренты, а дополнение друг друга. Parquet — колоночный формат на диске с компрессией (Snappy, Zstd). Arrow — колоночный формат в памяти без компрессии, оптимизированный для вычислений. Типичный pipeline: читаем Parquet с диска, декодируем в Arrow в памяти, обрабатываем, записываем обратно в Parquet.

Cross-Language: один формат, все языки

Традиционно каждая система хранила данные в своём формате:

Без Arrow:
Python (NumPy)  →  serialize  →  Java (JVM arrays)  →  serialize  →  C++ (std::vector)
                   ~100ms           ~100ms                ~100ms         ~100ms
Итого: 4 копии данных, 4 сериализации

С Arrow:
Python (PyArrow)  ─── Arrow Buffer (shared memory) ─── Java (Arrow) ─── C++ (Arrow)
                        одна копия данных, zero-copy

Arrow IPC (Inter-Process Communication) позволяет передавать данные между процессами на одной машине через shared memory (mmap) или Unix domain sockets. Данные уже в нужном формате — никакой конвертации.

Именно поэтому Pandas UDF в Spark работают в 3-100x быстрее обычных Python UDF. Обычный Python UDF сериализует каждую строку через pickle (медленно). Pandas UDF передаёт целые batches через Arrow — один zero-copy transfer вместо миллионов сериализаций. Подробнее об этом мы говорили в модуле UDF Performance.

Arrow IPC Format

Arrow IPC определяет wire format для передачи Arrow-данных между процессами:

import pyarrow as pa
import pyarrow.ipc as ipc

# Создаём таблицу
table = pa.table({
    'id': [1, 2, 3],
    'name': ['Анна', 'Борис', 'Вера'],
    'salary': [95000, 78000, 112000]
})

# Записываем в IPC формат (файл или поток)
with pa.OSFile('/tmp/data.arrow', 'wb') as f:
    writer = ipc.new_file(f, table.schema)
    writer.write_table(table)
    writer.close()

# Читаем обратно -- zero deserialization
with pa.OSFile('/tmp/data.arrow', 'rb') as f:
    reader = ipc.open_file(f)
    table_back = reader.read_all()
    # table_back уже в Arrow format, ready to process

IPC поддерживает два режима:

  • File format (IPC File): случайный доступ, метаданные в footer
  • Stream format (IPC Stream): последовательный доступ, для real-time передачи
Проверка знанийKnowledge check
Почему Arrow позволяет передавать данные между Python и Java без сериализации?
ОтветAnswer
Arrow определяет единый byte layout для каждого типа данных. Данные в PyArrow и Java Arrow занимают идентичные байты в памяти. При передаче через shared memory (mmap) оба процесса ссылаются на ту же физическую память -- никакой конвертации или копирования не нужно. Это zero-copy transfer: вместо serialize-copy-deserialize (3 шага) происходит передача указателя (1 шаг).
Проверка знанийKnowledge check
Чем Arrow отличается от Parquet и почему они дополняют друг друга?
ОтветAnswer
Parquet -- колоночный формат для хранения на диске с компрессией (Snappy, Zstd, LZ4). Arrow -- колоночный формат в оперативной памяти без компрессии, оптимизированный для вычислений и передачи между процессами. Parquet минимизирует размер хранения, Arrow минимизирует стоимость обработки. Типичный pipeline: чтение Parquet -> декодирование в Arrow -> обработка -> запись в Parquet.
TIP

Для углублённого изучения внутренней архитектуры Arrow (memory layout, type system, IPC format, Feather) см. курс Storage Formats Deep-Dive.

Arrow memory layout: bit-level DataFusion: Arrow как фундамент Бинарные форматы в Python

Что дальше?

В следующем уроке мы разберём zero-copy transfer — механизм, который позволяет Arrow передавать гигабайты данных между процессами без единого копирования. Вы узнаете, как shared memory и memory-mapped files делают это возможным.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Аналитический запрос SELECT AVG(salary) FROM employees (6 колонок, 10 млн строк). Почему колоночное хранение эффективнее строчного?

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

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

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

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