Learning Platform
Глоссарий Troubleshooting
Урок 07.04 · 35 мин
Продвинутый
Text FormatsColumnar AccessPredicate PushdownStatisticsSchema-on-ReadSplittabilityFull ScanEncoding

Ограничения текстовых форматов для аналитики

Почему текстовые форматы плохи для аналитики

CSV, JSON и XML были созданы для обмена данными — передачи от системы А к системе Б с максимальной совместимостью. Аналитические запросы предъявляют принципиально другие требования: выборочное чтение столбцов, фильтрация по условиям без полного сканирования, агрегации на миллиардах строк. Текстовые форматы не удовлетворяют ни одному из этих требований.

Требования аналитики vs возможности текстовых форматов
ТребованиеЧто нужно аналитическому движку для эффективных запросов.
Текстовые форматыПоддержка в CSV/JSON/XML.
Columnar форматыПоддержка в Parquet/ORC.
Column pruningЧтение только нужных столбцов без загрузки всей записи. SELECT name, age — читать только name и age, не трогая остальные 50 столбцов.
Невозможно. CSV/JSON/XML хранят данные построчно — чтобы прочитать один столбец, нужно распарсить всю строку.
Нативно. Столбцы хранятся отдельно. Читаем только нужные column chunks. Остальные не трогаем.
Predicate pushdownФильтрация строк по условию без чтения данных. WHERE age > 30 — пропустить row groups, где max(age) ≤ 30.
Невозможно. Нет statistics, нет min/max. Каждая строка должна быть прочитана и распарсена для проверки условия.
Min/max statistics в metadata. Bloom filters. Row group / stripe skipping. Читаем только matching groups.
Efficient encodingКомпактное кодирование значений: dictionary encoding, delta encoding, RLE.
Текст: число 1000000 = 7 байт ASCII. Boolean true = 4 байта. Нет dictionary, нет delta, нет RLE.
Dictionary encoding: 'California' → 1 byte index. Delta: timestamps → 1-2 bytes difference. RLE: повторы → count.
Schema enforcementТипы определены схемой, не угадываются при чтении.
Schema-on-read: типы определяются парсером при чтении. Разные парсеры могут интерпретировать по-разному. Ошибки типизации обнаруживаются поздно.
Schema-on-write: типы зафиксированы при записи. Reader получает типизированные данные без угадывания.
SplittabilityПараллельное чтение фрагментов файла несколькими worker'ами.
CSV: quoted newlines ломают наивный split. JSON Array: нельзя splitнуть. XML: вложенность запрещает split.
Row groups (Parquet) / stripes (ORC) — самодостаточные единицы. Точные offsets в metadata.

Read path: CSV full scan vs Parquet column pruning

Рассмотрим запрос SELECT name, age FROM users WHERE country = 'RU' на таблице с 50 столбцами и 100 миллионов строк:

Read path: CSV vs Parquet — один запрос
CSV: полный путь чтенияCSV читает ВСЕ данные. Все 50 столбцов, все 100M строк. Парсит каждую строку, конвертирует типы, проверяет условие. Потом выбрасывает 48 ненужных столбцов и 95M не-matching строк.
Parquet: оптимизированный путьParquet читает footer → определяет нужные columns (name, age, country) → проверяет min/max statistics по country → пропускает row groups без RU → читает только matching row groups, только 3 столбца.

Разница: CSV читает 100% файла для извлечения 2% данных. Parquet читает ~3% файла. На 10 GB файле: CSV scan = 10 GB I/O, Parquet = ~300 MB I/O.

I/O Amplification: размер чтения vs полезные данные
CSVAmplification = прочитано / полезно. CSV: 10 GB / 200 MB = 50x. На каждый полезный байт — 50 байт мусорного I/O.
JSONJSON ещё хуже: файл больше (overhead ключей), но полезные данные те же. Amplification > 50x.
ParquetParquet: column pruning + predicate pushdown. Читаем только нужные столбцы и matching row groups.

Schema-on-read: ошибки обнаруживаются поздно

Schema-on-read означает, что структура и типы данных определяются при чтении, а не при записи. Для CSV/JSON это единственная модель — файл не содержит схему:

Schema-on-read vs Schema-on-write
Schema-on-read (CSV/JSON)Типы определяются при чтении. Writer пишет что угодно — нет валидации. Reader обнаруживает проблемы: неправильные типы, missing fields, inconsistent formats. Ошибки — на стороне consumer'а.
Schema-on-write (Avro/Parquet)Типы фиксированы при записи. Writer валидирует данные по схеме перед записью. Reader получает типизированные данные. Ошибки — на стороне producer'а, в момент записи.
Schema-on-read: типичные сценарии ошибок
Mixed types в столбцеСтолбец 'amount' обычно содержит числа (42.50), но одна запись содержит 'N/A'. inferSchema по первым N строкам определил тип как DOUBLE. При чтении строки с 'N/A' — ошибка или null.
Schema driftCSV-файл за январь имеет 20 столбцов. За февраль — 22 (добавили два поля). Schema inference по январю не ожидает новых столбцов. Данные за февраль теряют 2 поля.
Inconsistent formatsОдин producer пишет дату как '2024-01-15', другой как '15/01/2024'. Один файл, два формата. Парсер подберёт формат по первой записи и сломается на второй.
Обнаружение: при чтенииВсе эти ошибки обнаруживаются при чтении — через часы, дни, или недели после записи. Чем дальше от момента записи, тем сложнее найти причину и исправить.
Silent corruptionПоследствия: silent data corruption (неправильные типы), data loss (отброшенные строки), inconsistent aggregates. Всё это — downstream от schema-on-read.

Splittability: параллельная обработка

Distributed processing (Spark, Flink, Trino) требует разбиения файла на фрагменты (splits) для параллельной обработки. Текстовые форматы создают проблемы:

Splittability: текстовые vs columnar форматы
ФорматФормат данных.
Splittable?Можно ли безопасно разбить файл на фрагменты для параллельной обработки.
ПроблемаЧто мешает безопасному split.
CSV (simple)CSV без quoted newlines: каждый \\n = конец записи → безопасный split.
Если нет quoted newlines — split по \\n безопасен.
Условие: нет кавычек с \\n внутри. Spark: multiLine=false.
CSV (multiLine)CSV с quoted newlines: \\n внутри значения — часть записи, не разделитель.
Нельзя splitнуть по byte offset — \\n внутри кавычек не разделитель записи.
Spark multiLine=true → один executor читает весь файл. Нет параллелизма.
JSON ArrayJSON Array: [{...}, {...}] — невозможно splitнуть без парсинга скобок.
Массив = один документ. Нельзя найти границу объекта без парсинга вложенных скобок.
Spark multiLine=true для JSON Array → один executor.
JSON LinesJSON Lines: одна JSON-запись на строку. \\n всегда разделитель (JSON экранирует внутренние \\n).
Каждый \\n = конец записи (JSON escape гарантирует). Split по byte offset → scan до \\n.
JSON Lines специально создан для splittability. Spark default mode (multiLine=false) = JSONL.
XMLXML: вложенные элементы, namespace'ы → невозможно splitнуть без парсинга.
XML = дерево. Нельзя разрезать дерево по byte offset. Нужен полный парсинг для нахождения границ записей.
spark-xml: один executor на файл. Для параллелизма: разбивайте XML на маленькие файлы заранее.
ParquetParquet: row groups с точными offsets в footer.
Footer содержит точные offsets row groups. Каждый row group = самодостаточная единица чтения.
Spark: один task на row group. Десятки/сотни tasks для одного файла.
Spark parallelism: CSV vs Parquet
CSV multiLine=trueSpark не может splitнуть CSV с quoted newlines. Один executor читает весь 10 GB файл последовательно. Остальные 99 executor'ов простаивают.
ParquetParquet footer содержит offsets для 200 row groups. Spark создаёт 200 tasks, распределяет по 100 executors. Каждый executor читает 2 row groups параллельно.

Отсутствие encoding и compression

Текстовые форматы хранят данные как ASCII/UTF-8 текст без специализированного кодирования. Columnar форматы используют encoding, специфичный для каждого столбца:

Encoding: текст vs specialized
ДанныеТип данных в столбце.
CSV/JSON (текст)Как данные хранятся в текстовых форматах.
Parquet (encoded)Как данные хранятся с encoding в Parquet.
country (5 unique / 1M rows)Столбец country с 5 уникальными значениями на 1M строк.
Каждая строка: полное текстовое значение. 'United States' × 200K раз = 2.6 MB только для одного значения.
Dictionary encoding: 5 уникальных значений → dictionary (5 entries). Каждая строка: 1-byte index. 1M × 1 byte = 1 MB вместо ~8 MB.
timestamp (monotonic)Столбец timestamp с millisecond timestamps (монотонно растущие).
Каждая запись: '2024-01-15T10:30:00.123Z' = 24 bytes. 1M × 24 = 24 MB.
Delta encoding: первый timestamp полный, остальные — разница (delta). Для monotonic: delta = маленькое число (1-2 bytes). 1M × 2 bytes = 2 MB.
is_active (boolean)Столбец is_active (boolean) — 70% true, 30% false.
CSV: 'true'/'false' = 4-5 bytes. JSON: true/false = 4-5 bytes. 1M × 4.3 avg = 4.3 MB.
RLE + bit-packing: run of 700K true (1 entry) + run of 300K false (1 entry). ~100 bytes для 1M значений.
Размер данных: один столбец на 1M строк
country (CSV)Текстовое хранение: каждая строка хранит полное имя страны. 1M записей × ~10 bytes avg = ~10 MB.
country (Parquet)Dictionary encoding: 5 уникальных строк в dictionary (~50 bytes) + 1M × 1-byte index = ~1 MB. Экономия 10×.
timestamp (JSON)ISO 8601 строки: 24 bytes каждая. 1M × 24 = 24 MB.
timestamp (Parquet)Delta encoding: base timestamp (8 bytes) + 1M deltas (avg 2 bytes) = ~2 MB. Экономия 12×.
is_active (CSV)'true'/'false': 4-5 bytes. 1M × 4.3 avg = 4.3 MB.
is_active (Parquet)RLE + bit-packing: runs of identical values. Для 70/30 split — несколько сотен байт.

Query execution: полный контраст

Query execution: SELECT country, COUNT(*) GROUP BY country
CSV executionCSV: read all bytes → parse all rows → extract country column → hash aggregate. Каждый этап работает со ВСЕМИ данными, потому что CSV не позволяет пропустить ни одного байта.
Parquet executionParquet: read footer → read only country column → dictionary-aware aggregate (aggregate on dict values, not raw strings). Минимальный I/O, минимальный CPU.

Разница: 45 секунд vs 50 миллисекунд — 900× быстрее. На более крупных данных разрыв увеличивается.

Отсутствие statistics и метаданных

Columnar форматы хранят statistics для каждого row group: min/max значения, null count, distinct count. Это позволяет пропускать целые группы строк без чтения данных:

Statistics: что знает Parquet, чего не знает CSV
Parquet Row Group MetadataКаждый row group хранит статистику для каждого столбца: min, max, null_count. Это позволяет query engine'у пропустить row groups, не содержащие matching данные.
WHERE age > 50WHERE age > 50: Row Group 0 (max=45) пропускается — не может содержать age > 50. Row Group 1 (max=67) читается. Пропущено 100K строк без I/O.
Skip RG0 (max=45)Row Group 0 skipped (max=45 < 50). Экономия: 100K строк не прочитаны.
CSV: нет statisticsCSV не знает ничего о содержимом. min? max? null count? Нет. Каждая строка должна быть прочитана и проверена.

Compression awareness

Текстовые форматы можно сжать внешним компрессором (gzip, zstd), но это создаёт дополнительные проблемы:

Compression: внешняя (CSV) vs встроенная (Parquet)
СвойствоАспект сжатия данных.
CSV + gzipCSV файл, сжатый внешним gzip.
Parquet + zstdParquet с встроенной zstd-компрессией.
Seekable?Можно ли прочитать часть файла без декомпрессии всего.
gzip — потоковый формат. Нельзя seek к середине и начать декомпрессию. Нужно прочитать с начала. Для CSV: 1 executor читает от начала.
Каждый column chunk сжат отдельно. Можно seek к нужному chunk и декомпрессировать только его.
Splittable?Можно ли splitнуть сжатый файл для параллелизма.
gzip не splittable. bzip2 — splittable, но медленный. LZO — splittable, но нужен index. Для CSV: всегда один executor.
Row groups = independent compression units. Каждый row group декомпрессируется отдельно.
Compression ratioСтепень сжатия — насколько хорошо сжимаются данные.
Text + gzip: хорошо сжимает повторяющийся текст (ключи JSON, теги XML). Но данные изначально bloated — сжимается overhead.
Columnar data + encoding + zstd: одинаковые значения рядом → высокая локальность → лучшее сжатие. 10-20× от raw.
TIP

Spark поддерживает splittable compression для CSV: bzip2 (.csv.bz2) и lzo (.csv.lzo с индексом). Но gzip (.csv.gz) — не splittable: один executor на файл. Для больших CSV в S3/HDFS: используйте bzip2 или разбивайте на много маленьких gzip-файлов (multifile parallelism).

Итог

Текстовые форматы (CSV, JSON, XML) фундаментально несовместимы с требованиями аналитических систем. Каждое ограничение — не баг, а следствие дизайна: формат без типов не может иметь encoding, формат без метаданных не может иметь predicate pushdown, формат без структуры не может быть splittable. Для аналитики используйте columnar форматы (Parquet, ORC) — они спроектированы именно для этого. Текстовые форматы — для транспорта и обмена, не для хранения и запросов.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Запрос SELECT name, age FROM users WHERE country = 'RU' на CSV-файле с 50 столбцами и 100M строк. CSV reader прочитает 100% файла. Parquet reader — ~3%. Какие две оптимизации Parquet недоступны для CSV?

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

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

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

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