Суть Читай реальные пути чтения PyArrow/DuckDB и конфиг писателя, предсказывай I/O и объём прочитанных байтов, выбирай решение с наибольшим рычагом для pushdown, размера и проекции.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Проблемы Parquet диагностируются в пути чтения и в конфиге писателя, а не в документации. Читай каждый сниппет, предскажи, сколько байтов движок реально трогает, и выбери решение, которое сеньор делает первым.
Цель
Отработай цикл, который ты запускаешь на каждом медленном запросе к озеру: увидь, где применяются фильтр и проекция, реши, разложил ли писатель данные так, чтобы их можно было пропускать, и тянись к решению по раскладке раньше, чем к железу.
Сниппет 1 — pushdown потерян внутри движка
import pyarrow.parquet as pq# events/ — год дневных Parquet-файлов, отсортированных по `day`table = pq.read_table("s3://lake/events/") # читает всёdf = table.to_pandas()df = df[df["day"] == "2024-02-01"] # фильтр после чтенияresult = df[["user_id", "country"]] # проекция после чтения
Викторина
Completed
Это читает ~1.9 ГБ и медленно, хотя данные отсортированы по day. Почему и какое решение даёт наибольший рычаг?
Heads-up Класс хранения не важен, когда ты читаешь 1.9 ГБ, которые не нужны. Дефект в том, что читается всё, а потом фильтруется в памяти; проталкивание фильтра и списка колонок в read_table — вот что режет байты.
Heads-up Конвертация ниже по течению от проблемы — движок уже прочитал все колонки и все row groups. Смена представления в памяти не уменьшает то, что было прочитано с S3.
Heads-up Данные уже отсортированы по day, и сортировка управляет row-group skipping, а не column pruning. Реальный баг в том, что ни предикат, ни проекция колонок вообще не протолкнуты в читатель.
Сниппет 2 — конфиг писателя
import pyarrow.parquet as pq# одна батч-запись ~50 ГБ, запросы почти всегда по `country`pq.write_table( table, "s3://lake/events_2024.parquet", row_group_size=2048, # строк на row group compression="snappy", write_statistics=True,)
Викторина
Completed
Для батч-записи 50 ГБ эта раскладка сделает фильтрованные запросы медленными, несмотря на write_statistics=True. Что не так и каково решение?
Heads-up Статистики — не проблема размера, а gzip медленнее на чтении. Дефект — гранулярность row group: 2048 строк на группу означает, что оверхед метаданных доминирует. Выбор кодека ортогонален размеру row group.
Heads-up Статистики — это ровно то, что включает row-group skipping; выключив их, ты убираешь pushdown целиком, и фильтрованные запросы сканируют всё. Проблема — крошечный размер row group, а не наличие статистик.
Heads-up Размер row group — главная ручка производительности чтения: слишком мал — доминирует чтение метаданных/футеров; слишком велик — теряется гранулярность пропуска и нужна огромная память на запись. 2048 строк — слишком мало.
Сниппет 3 — проекция колонок против SELECT *
-- DuckDB над широкой Parquet-таблицей на 120 колонок в S3SELECT *FROM read_parquet('s3://lake/wide_events/*.parquet')WHERE country = 'DE';
Викторина
Completed
Аналитику нужны только user_id и amount, но он запускает SELECT * на таблице из 120 колонок. Какова цена и каково решение?
Heads-up Колонки ничего не стоят, только когда ты их не выбираешь. SELECT * выбирает их все, так что читается каждый column chunk в каждой совпавшей row group — ровно тот случай, который column pruning должен был избежать.
Heads-up Предикат ограничивает, какие row groups читаются (измерение строк); проекция ограничивает, какие колонки читаются (измерение колонок). Они независимы — SELECT * всё равно читает все 120 колонок каждой уцелевшей row group.
Heads-up LIMIT ограничивает вывод строк после скана; он не уменьшает набор колонок, читаемых на row group. Только перечисление колонок включает column pruning.
Сниппет 4 — откат словаря
# колонка session_id высокой кардинальности, ~10М почти уникальных UUIDpq.write_table( table, # содержит колонку `session_id` из UUID "s3://lake/sessions.parquet", use_dictionary=True, # включено по умолчанию compression="zstd",)
Викторина
Completed
Колонка session_id выходит едва меньше plain-байтов, а CPU записи высокий. Что произошло?
Heads-up zstd всё же сжимает байты в какой-то мере; потраченная работа — это построение словаря, который переполнился и был выброшен. История здесь — откат на уровне encoding, а не кодек.
Heads-up Никакого обрезания или повреждения не происходит — Parquet чисто откатывается на plain encoding при переполнении словаря. Симптом это потраченный CPU и отсутствие выигрыша по размеру, а не плохие данные.
Heads-up Большая row group даёт словарю больше значений для поглощения, делая проблему кардинальности хуже, а не лучше. Решение — вообще не dictionary-кодировать почти уникальную колонку.
Итог
Каждый медленный запрос к Parquet читается в пути доступа и конфиге писателя: протолкни предикат и список колонок в читатель, иначе ни row-group skipping, ни column pruning не сработают; задавай размер row group в байтах, целясь в ~128 МБ-1 ГБ, и кластеризуй по колонке фильтра, чтобы её диапазоны были пропускаемы; SELECT * тихо читает каждый column chunk, так что всегда проецируй; а dictionary encoding на колонке высокой кардинальности переполняется и откатывается на plain, стоя CPU без выигрыша по размеру. Диагностируй по тому, где приземляются фильтр и проекция, чини раскладку, затем заново измеряй прочитанные байты.