open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 09 · 02

Форматы файлов: CSV на краях, Parquet внутри — row groups, статистики, Arrow и компромиссы сжатия

Лестница форматов с механизмами: CSV заново парсит и угадывает типы при каждом чтении; Parquet даёт типизированную схему, row groups со статистиками, обрезку колонок и сжатие; Arrow — zero-copy стандарт в памяти. Плюс партиционирование, беда мелких файлов и эволюция схемы.

PY Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Один ежедневный экспорт, три команды-потребителя, три разные таблицы. Хранилище каждое утро выкладывало CSV; финансы, опсы и data science читали его каждый своим read_csv. Финансы получали почтовые индексы целыми числами — 02134 превращался в 2134, и региональный отчёт тихо склеил два штата. Data science получала идентификаторы клиентов как float64, потому что в одной строке была пустая ячейка, — id больше 2^53 округлялись, и джоб дедупликации начал находить «дубликаты», которые на деле были двумя разными клиентами с разницей в единицу. Опсы прибили dtype вручную — но их список разъехался с экспортом, когда добавили колонку, и их загрузчик молча залил новую колонку не тем типом. Встреча по сверке стала еженедельной. Никто не написал баг — его написал формат. CSV хранит символы, а не типы: каждый читатель заново выводит схему, и три читателя — это три схемы. Лечение оказалось скучным: тот же экспорт в Parquet, где схема едет внутри файла. Споры о dtype не разрешились — они стали невозможны.

CSV и JSON: текст на краях

Почему выбор формата важен ещё до того, как вы запустили какой-либо движок? Потому что класс отказов — не исключение времени выполнения, которое можно поймать, а тихая угадка типа, тиражирующая три разные таблицы трём командам из одного файла.

CSV — это символы, разделённые запятыми: ни типов, ни схемы, ни контракта. Каждое чтение заново парсит байты и заново угадывает их смысл: 02134 — это int или почтовый индекс, пустое поле — NaN или пустая строка, 2026-06-01 — дата или текст? Угадывание выполняется на каждого читателя, каждый день, что делает класс отказов системным, а не случайным: ведущие нули исчезают, одна пропущенная ячейка превращает колонку id во float64, локаль переворачивает десятичный разделитель. Смягчения — явные карты dtype=, parse_dates, прибитые списки колонок — это схема, которую каждый потребитель ведёт руками, вне файла и независимо. JSON и NDJSON поднимаются на полступеньки: умеют вложенность, NDJSON хорошо стримится построчно, но это по-прежнему текст — многословный (имена полей повторяются в каждой строке) и типово-двусмысленный на краях (в стандарте нет различия int и float, нет родных дат).

Parquet: файл, который везёт собственную обрезку

Parquet — колоночный и типизированный, и его раскладка и есть механизм. Файл заканчивается футером со схемой и метаданными каждой row group — горизонтального среза строк, обычно от десятков до сотни с лишним МБ, часто порядка миллиона строк на группу в зависимости от писателя. Внутри row group каждая колонка — column chunk, разбитый на страницы (~1 МБ), единицу кодирования и сжатия: словарное кодирование для повторяющихся значений, RLE для длинных серий, сверху кодек. Из раскладки напрямую следуют два эффекта времени запроса. Обрезка колонок: читателю, которому нужны 3 колонки из 40, достаточно сместиться ровно к этим column chunks и не трогать остальное. Predicate pushdown: футер хранит min/max-статистики по каждому чанку (плюс счётчики null, опционально bloom-фильтры), поэтому фильтр вроде одного дня таймстемпов пропускает каждую row group, чей диапазон не может совпасть, — если данные записаны отсортированными по этой колонке, день живёт в нескольких смежных группах, и чтение трогает считаные проценты файла.

Сжатие задаётся на column chunk, и выбор — настоящий компромисс: snappy распаковывается на гигабайтах в секунду и почти не грузит CPU, но оставляет файлы на ~20–30% больше; zstd жмёт заметно меньше за умеренный дополнительный CPU. Лейки с упором на хранение по умолчанию берут zstd; горячие скан-пути иногда оставляют snappy. Честный масштаб, одна и та же таблица на 10 млн строк и ~20 колонок на одной машине: CSV ~1,2 ГБ, gzip-CSV ~300–400 МБ, Parquet-zstd ~150–250 МБ; чтение: pandas read_csv — десятки секунд, полный Parquet — несколько секунд, проекция из 2 колонок — меньше секунды. Выбор формата — порядок величины по обеим осям ещё до какого-либо тюнинга движка.

Викторина

Запрос фильтрует Parquet-таблицу на 10 млн строк до одного дня значений ts, причём таблица записана отсортированной по ts. Почему чтение трогает лишь несколько процентов файла?

Arrow: стандарт в памяти, который заставляет форматы дружить

Arrow — не файловый формат, конкурирующий с Parquet, а колоночная раскладка в памяти, общая для pandas (Arrow-backed dtypes), polars и DuckDB. Одинаковая раскладка буферов в каждом движке означает, что передача таблицы — обмен указателями, а не цикл сериализации-десериализации: polars в DuckDB и дальше в pandas без единого копирования данных. Когда Arrow всё же идёт на диск, это формат IPC (Feather — его файловый вариант) — по сути выписанная память: почти нулевая цена парсинга при чтении, но лёгкое сжатие и без обрезки по статистикам, как у Parquet. Разделение труда: Parquet — для хранения (минимум байтов, обрезаемо), Arrow — для обмена и памяти (ноль парсинга, ноль копий).

Партиционирование и два способа его испортить

На диске датасеты режутся в hive-стиле на каталоги key=valuedt=2026-06-01/ — и движки отбрасывают целые каталоги до открытия единственного файла: запрос за один день по году данных листит один каталог из 365. Режим отказа — пере-партиционирование: добавьте второй ключ высокой кардинальности (скажем, 50 тыс. id клиентов) — и год превращается в ~18 миллионов файлов в среднем по десятку КБ. Теперь доминирует листинг, каждый файл стоит открытия плюс чтения футера, а статистики row groups не получают шанса, потому что ни один файл не достаточно велик для осмысленных row groups. Рабочие эвристики: партиционируйте только по колонкам низкой кардинальности, по которым реально фильтруете; цельтесь в файлы ~100 МБ–1 ГБ; и сортируйте внутри файлов по следующей по частоте колонке фильтрации, чтобы обрезка по статистикам подхватывала там, где партиционирование заканчивается. Эволюция схемы живёт по той же честной логике: добавление nullable-колонок дёшево (старые файлы просто отдают null), переименования — замаскированные поломки (идентичность колонки — её имя), а смена типа обычно означает переписывание истории — планируйте имена и типы как API, потому что это и есть API.

Викторина

Команда партиционирует лейк событий по dt И по customer_id (50 тыс. клиентов). Запросы стали медленнее, чем до партиционирования. Что случилось?

Правило

CSV — на краях: люди, таблицы Excel, легаси, не говорящее ни на чём другом; принимается один раз, валидируется и конвертируется. Parquet — внутри конвейера, где схема едет вместе с данными, читатели обрезают вместо того, чтобы парсить, а три команды, читающие один файл, получают одну и ту же таблицу по построению.

Вспомните перед уходом
  1. 01
    Разберите внутренности Parquet-файла — футер, row groups, column chunks, страницы — и объясните, как фильтрованное проецированное чтение использует каждый уровень.
  2. 02
    Сформулируйте правило форматов с честными числами и дисциплину партиционирования и эволюции схемы, которая держит лейк быстрым.
Итог

Форматы — это контракты, и CSV — отсутствие контракта: символы, чей смысл каждый читатель выводит заново; так один экспорт породил три таблицы — исчезнувшие ведущие нули, колонка id, поплывшая из-за единственной пустой ячейки, прибитые вручную dtype, разъехавшиеся с источником. JSON и NDJSON добавляют вложенность, оставаясь многословным типово-двусмысленным текстом. Parquet меняет физику: футер несёт типизированную схему и min/max-статистики каждой row group; данные живут в row groups из column chunks из страниц ~1 МБ, кодированных словарём и RLE под snappy (быстро) или zstd (компактно). Обрезка колонок и predicate pushdown поэтому следуют из самой раскладки файла — 3 колонки из 40 это паттерн смещений, один день сортированных таймстемпов — горстка row groups, и та же таблица падает с ~1,2 ГБ CSV до ~200 МБ Parquet-zstd, а чтения — с десятков секунд до долей секунды на проекции. Arrow — половина истории в памяти: одна колоночная раскладка, общая для pandas, polars и DuckDB, превращающая обмен в передачу указателей, с IPC/Feather как дисковой формой почти без парсинга. Партиционируйте в hive-стиле по ключам низкой кардинальности, держите файлы около 100 МБ–1 ГБ, сортируйте внутри, эволюционируйте схему аддитивно — и держите CSV там, где ему место: на краях, с Parquet внутри. Теперь, когда откроете пайплайн и увидите read_csv в середине потока, — это первое, что нужно заменить; аргументы dtype= под ним — схема, которую вручную ведёт каждый потребитель, тогда как ей место внутри самого файла.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.