Алгоритмы выбора кодировки
В предыдущих уроках мы разобрали что кодировки делают — dictionary, delta, FOR, bit-packing, prefix, RLE. Теперь ключевой вопрос: кто решает, какую кодировку использовать для конкретной колонки? В большинстве случаев — не человек, а writer.
Каждый формат реализует свой алгоритм выбора: от детерминистических правил Parquet до sampling-based cascade BtrBlocks. Понимание этих алгоритмов объясняет, почему одни и те же данные сжимаются по-разному в разных форматах — и когда ручное переопределение оправдано.
Parquet: детерминистические правила + fallback
Parquet writer использует type-based default + dictionary probe. Алгоритм жёстко зашит в спецификации:
Начало column chunk → type из schema
Начало записи column chunk. Writer знает physical type колонки из schema (BOOLEAN, INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY) и logical type (STRING, DATE, TIMESTAMP, DECIMAL, UUID, ...).BOOLEAN? → RLE (всегда)
Первое решение: тип данных. BOOLEAN всегда → RLE. INT32/INT64 → dictionary по умолчанию (с fallback). BYTE_ARRAY → dictionary по умолчанию. FLOAT/DOUBLE → BYTE_STREAM_SPLIT или PLAIN. Решение детерминистическое — не зависит от данных.Начать dictionary encoding
Writer начинает dictionary encoding. Каждое новое значение: lookup в словаре. Если найдено — записать индекс. Если нет — добавить в словарь. Словарь растёт по мере записи.dict_page > 1 MB?
Проверка: dictionary page size > dictionary_page_size_limit (по умолчанию 1 MB). Это происходит автоматически по мере записи — нет pre-scan данных.→ RLE_DICTIONARY
Если словарь не превысил лимит к концу column chunk — оставляем RLE_DICTIONARY. Индексы кодируются RLE/Bit-Packed Hybrid. Словарь записывается как dictionary page.→ Fallback: DELTA_BINARY_PACKED / PLAIN
Если словарь превысил лимит — ВЕСЬ column chunk переписывается с fallback encoding. Для INT: DELTA_BINARY_PACKED. Для STRING: DELTA_BYTE_ARRAY или PLAIN. Атомарное решение: нельзя часть pages оставить dictionary.Детали fallback по типу
Parquet writer не анализирует данные перед выбором encoding. Он начинает с dictionary и переключается по порогу. Это означает: writer не знает, что dictionary будет бесполезен, пока не потратит CPU на insertion 500K unique значений в hash map. Для заведомо high-cardinality колонок — manual override на PLAIN/DELTA экономит CPU при записи.
ORC: stripe-level statistics
ORC принимает решение per-stripe (по умолчанию ~256 MB) на основе статистик, собранных во время записи:
Начало stripe: буферизация в memory
Начало записи stripe. ORC writer буферизирует данные в memory — не записывает до stripe flush. Это позволяет анализировать данные перед выбором encoding.Сбор статистик: cardinality, min/max, run_lengths
По мере записи ORC собирает статистики per-column: cardinality (через HLL или exact), min/max, total bytes, run lengths. Статистики используются для решения о encoding при flush.STRING: dict_ratio > 0.8? → Direct
При stripe flush: для каждой строковой колонки — если dictionary ratio > threshold (dictionary_bytes / total_data_bytes), переключиться на direct encoding. По умолчанию threshold ≈ 0.8 (если словарь > 80% от raw данных — бесполезен).INTEGER: RLEv2 (авто-подкодировка)
Integer колонки: ORC всегда использует RLEv2. Автоматический выбор подкодировки: Short Repeat для серий, Direct для dense packed, Patched Base для outliers, Delta для sequential. Writer анализирует паттерн данных и выбирает подкодировку per-chunk.STRING: Dictionary или Direct per-stripe
Решение о кодировке фиксируется per-stripe. Следующий stripe может выбрать другую кодировку — данные меняются между stripes.RLEv2: автоматический выбор подкодировки
Ключевое отличие ORC от Parquet: ORC не пробует dictionary и не откатывает. Writer анализирует статистики и принимает решение до записи stripe. Нет wasted CPU на dictionary insertion для high-cardinality данных.
DuckDB: Analyze → Try → Measure → Commit
DuckDB использует самый продвинутый подход — sample-based analysis per segment:
Segment buffer: ~122K values в памяти
Данные буферизируются в Vector (2048 values) → Segment (~122K values). При flush сегмента — запускается Analyzer. Данные уже в памяти — можно анализировать без I/O.Analyze: cardinality, min/max, null_count, runs
Шаг 1: Analyze — быстрый scan данных (или sample). Определяет: cardinality (approximate), min/max, null_count, constant check, run_length distribution. Не вычисляет энтропию — только быстрые O(n) статистики.Candidate selection: Constant → Dict → RLE → Delta → FOR → FSST
Шаг 2: Candidate selection — на основе статистик определяется список кандидатов. Constant? → Constant encoding (1 value). Low cardinality? → Dictionary. Long runs? → RLE. Sequential? → Delta. Else? → FOR+BitPacking / FSST / Uncompressed.Estimate size для каждого кандидата
Шаг 3: Try — для каждого кандидата, вычислить предполагаемый размер. Не пробное кодирование — а estimate на основе статистик. Dictionary: dict_size + index_bits × count. FOR: header + bit_width × count.Выбрать минимальный размер
Шаг 4: Measure — выбрать кандидат с минимальным estimated size. В случае tie — предпочтение decode speed (Constant > Dictionary > RLE > FOR > Delta).Commit: закодировать + записать сегмент
Шаг 5: Commit — закодировать сегмент выбранной кодировкой и записать. Решение per-segment — разные сегменты одной колонки могут использовать разные кодировки. Row group 0: Dictionary, Row group 1: FSST, Row group 2: Constant.DuckDB: encoding candidates по типу
DuckDB принимает решение per-segment (~122K строк), а не per-file или per-stripe. Это означает: если первые 122K строк — department names (10 unique → Dictionary), а следующие 122K — UUID (122K unique → FSST) — DuckDB использует разные кодировки для разных сегментов одной колонки. Ни Parquet, ни ORC этого не умеют.
BtrBlocks: Sampling Cascade
BtrBlocks (SIGMOD 2023) использует самый агрессивный подход — sampling + cascade evaluation. Вместо одной кодировки — цепочка кодировок, где выход одной становится входом следующей:
Блок данных (~65K values)
BtrBlocks получает блок данных (обычно ~65K values). Вместо полного analysis — берёт sample (~1-5% значений, random).Sample 1–5%: cardinality, distribution, patterns
Шаг 1: Sample — выбрать случайные 1-5% значений. Вычислить быстрые статистики на sample: cardinality estimate, value distribution, run pattern. O(sample_size) вместо O(n).Evaluate на sample: 8 кандидатов из пула
Шаг 2: Evaluate pool — для каждой кодировки из пула (8 кандидатов) оценить compression ratio на sample. Пул: Uncompressed, OneValue, Dictionary, RLE, Frequency, Pseudodecimal, FOR, FSST. Каждый кандидат — O(sample_size) evaluation.Cascade: best₁ → encode → evaluate best₂ → …
Шаг 3: Cascade — лучший первый кандидат применяется, его выход подаётся обратно в пул для второго уровня. Пример: Dictionary → FOR на индексах → BitPacking. Cascade останавливается, когда дальнейшее улучшение < threshold.Commit: encoding chain (e.g. Dict → FOR → BP)
Результат: цепочка кодировок, применённых последовательно. Decode — в обратном порядке. Пример: FOR(Dictionary(data)) → decode: un-FOR → un-Dictionary. Metadata хранит цепочку.BtrBlocks encoding pool
Manual Override: когда переопределять
Иногда автоматический выбор неоптимален. API для ручного управления:
Когда manual override оправдан
Сводная таблица: алгоритмы выбора
Ключевые выводы
- Parquet: probe-and-fallback — начинает с dictionary, откатывает при overflow. Простой, но тратит CPU на заведомо бесполезный dictionary для high-cardinality колонок. Manual override (
column_encoding) экономит CPU. - ORC: statistics-driven — собирает stats во время буферизации, решает при stripe flush. Нет wasted work, но гранулярность = stripe (~256 MB) — крупная.
- DuckDB: analyze-and-pick — per-segment analysis с candidate evaluation. Самая мелкая гранулярность (122K rows), самый богатый набор кандидатов (включая FSST и ALP). Разные сегменты одной колонки могут использовать разные кодировки.
- BtrBlocks: sampling cascade — единственный подход с цепочками кодировок. Dictionary → FOR → BitPacking даёт лучшую compression ratio, чем любая одиночная кодировка. Sampling (1–5%) делает evaluation быстрым.
- Manual override оправдан когда: (a) данные заведомо high-cardinality (UUID → skip dictionary probe), (b) тип данных указывает на оптимальную кодировку (sorted timestamps → DELTA), (c) benchmarks показывают выигрыш конкретной кодировки для вашего workload.
- Тренд: от детерминистических правил к data-driven selection. Parquet (2012) → ORC (2016) → DuckDB (2020) → BtrBlocks (2023): каждое поколение делает более информированный выбор на основе анализа данных.