Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 22 мин
Средний
compressionlightweightinternals

Зачем lightweight-сжатие

В прошлом модуле мы разобрали, как DuckDB раскладывает данные по блокам, row groups и колоночным сегментам. Каждый сегмент при записи в persistent-файл сжимается. Но «сжимается» — слово ёмкое: gzip тоже сжимает, и LZ4, и Brotli. DuckDB сознательно использует не их, а особый класс схем — lightweight compression, лёгкое сжатие. Этот вводный урок модуля объясняет, что значит «лёгкое», почему для аналитической СУБД это принципиальный выбор, и почему правильная метрика тут не «во сколько раз ужалось», а «успевает ли декомпрессия за сканированием».

Сжатие в OLAP — это про скорость, а не только про место

Основы компрессии данных для аналитических форматов Внутренности алгоритмов компрессии: LZ77, Huffman, ANS

Первая мысль про сжатие — экономия места на диске. Она верна, но для аналитической СУБД второстепенна. Главная причина сжимать данные в OLAP — скорость запросов.

Связь неочевидна, поэтому разберём по шагам. Аналитический запрос сканирует много данных — миллионы строк. Эти данные нужно поднять с диска в память. Диск — самое медленное звено в этой цепочке: даже быстрый SSD отдаёт данные на порядки медленнее, чем процессор их обрабатывает. Сканирование большой таблицы упирается не в вычисления, а в то, как быстро байты долетают с диска.

Теперь добавим сжатие. Сжатые данные занимают меньше байт. Меньше байт — меньше читать с диска. Если таблица сжалась вчетверо, с диска нужно поднять вчетверо меньше байт, и узкое звено — дисковый ввод-вывод — отрабатывает вчетверо быстрее. Сжатие превращает «лишнюю» работу процессора по распаковке в экономию на медленном диске. А поскольку процессор в OLAP-сканировании обычно недозагружен и ждёт диск, этот обмен почти всегда выгоден.

То же касается памяти и кэшей CPU. Сжатые данные плотнее лежат в оперативной памяти и в кэшах процессора. Больше полезных данных помещается в кэш — реже промахи, быстрее обработка. Сжатие работает на каждом уровне иерархии памяти, не только на диске.

Почему сжатие ускоряет сканирование
Без сжатияС диска поднимается полный объём байт. Дисковый ввод-вывод — узкое звено, и оно работает на полную нагрузку.
медленно
CPU ждёт дискПроцессор простаивает: он быстрее, чем диск отдаёт данные. Запрос ограничен скоростью I/O.
Со сжатиемС диска поднимается вчетверо меньше байт. Дисковый ввод-вывод отрабатывает быстрее.
быстрее
CPU распаковываетПростаивавший процессор тратит часть мощности на распаковку — обмен дешёвой работы CPU на экономию дорогого I/O.

В чём ловушка тяжёлого сжатия

Раз сжатие ускоряет, логично взять самый сильный алгоритм — gzip на максимальном уровне, или Brotli, — чтобы данные ужались как можно сильнее. Но тут ловушка, и она объясняет весь дизайн сжатия в DuckDB.

У сжатого блока есть два числа. Первое — степень сжатия: во сколько раз он стал меньше. Второе — скорость декомпрессии: как быстро его можно распаковать обратно. Эти два числа конфликтуют. Алгоритмы общего назначения вроде gzip и Brotli дают высокую степень сжатия, но платят за неё медленной распаковкой: они тратят процессорное время на разбор сложных внутренних структур — деревьев Хаффмана, словарей переменной длины, длинных цепочек обратных ссылок.

Посчитаем, к чему это ведёт в сканировании. Допустим, gzip ужал данные в 5 раз — отлично, с диска читаем в 5 раз меньше. Но распаковка gzip медленная, и процессор не успевает разжимать данные так же быстро, как теперь их подаёт диск. Узкое звено просто переехало: раньше ждали диск, теперь ждём декомпрессор. Запрос ограничен скоростью распаковки, и сильное сжатие не дало того ускорения, на которое рассчитывали.

В этом суть: для сканирующего OLAP-движка медленный декомпрессор так же вреден, как медленный диск. Степень сжатия бесполезна, если распаковка не успевает за сканированием.

WARNING

Распространённая ошибка интуиции — оценивать схему сжатия только по степени сжатия (compression ratio). Для аналитической СУБД это половина картины. Схема, которая жмёт в 5 раз, но распаковывается медленно, проиграет схеме, которая жмёт в 3 раза, но распаковывается на скорости сканирования. Правильная метрика — связка обоих чисел, и часто решающее именно второе.

Lightweight compression: распаковка на скорости сканирования

Отсюда вытекает выбор DuckDB. Движок использует lightweight compression — лёгкое сжатие. «Лёгкое» — это про вычислительную стоимость распаковки: схемы спроектированы так, чтобы декомпрессия была почти бесплатной по меркам процессора.

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

Достигается это за счёт простоты операций. Lightweight-схемы распаковываются не сложным разбором, а элементарными действиями процессора — сложением, битовыми сдвигами, копированием, индексацией в массив. Такие операции выполняются за единицы тактов и прекрасно векторизуются: процессор обрабатывает пачку значений одной SIMD-инструкцией. Никаких деревьев Хаффмана и цепочек обратных ссылок — только арифметика, которую CPU делает почти даром.

Цена компромисса честная: lightweight-схема обычно даёт меньшую степень сжатия, чем gzip на максимуме. Но взамен распаковка не тормозит запрос. Для OLAP это правильный размен: чуть больше байт на диске, зато сканирование не упирается в декомпрессор. DuckDB сознательно выбирает «жмём умеренно, но распаковываем молниеносно» вместо «жмём максимально, но распаковка душит запрос».

Тяжёлое сжатие против lightweight
gzip / BrotliВысокая степень сжатия, но медленная распаковка: деревья Хаффмана, словари переменной длины, цепочки ссылок. Декомпрессор становится узким звеном сканирования.
DuckDB выбирает
Lightweight (RLE, FOR, dictionary, ...)Умеренная степень сжатия, но распаковка элементарными операциями — сложение, сдвиг, индексация. Декомпрессия идёт на скорости сканирования и не тормозит запрос.

Почему именно колоночные данные жмутся хорошо

Lightweight-сжатие работает особенно эффективно именно в колоночной СУБД, и это не совпадение. В колоночном хранении значения одной колонки лежат подряд, одним потоком, — и такой поток обладает свойствами, которые лёгкие схемы используют напрямую.

Однородность типа. В колоночном сегменте все значения одного типа: все целые, все даты, все строки. Декомпрессор заранее знает, с чем имеет дело, и применяет специализированную под этот тип схему — для целых одну, для строк другую, для чисел с плавающей точкой третью. Не нужно на каждом значении выяснять его тип. В построчном хранении значения разных типов перемешаны в строке, и такой специализации нет.

Локальная похожесть значений. Соседние значения одной колонки часто похожи друг на друга. Колонка country — это десяток повторяющихся строк на миллионы записей. Колонка created_at — даты в узком диапазоне, отличающиеся младшими разрядами. Колонка status — горстка повторяющихся кодов. Эта похожесть и есть та избыточность, которую сжатие убирает: повторы кодируются один раз, узкий диапазон кодируется смещениями от общего минимума, и так далее.

В построчном хранении этой картины не видно: значения разных колонок чередуются, и похожесть соседей теряется в шуме. Колоночная раскладка собирает похожее рядом — а lightweight-схемы именно на однородных, локально похожих потоках и раскрываются в полную силу. Конкретные схемы — Constant, RLE, bit packing, FOR, dictionary, FSST, и схемы для чисел с плавающей точкой — мы разберём в следующих уроках модуля. Здесь важно понять фундамент: сжатие в DuckDB лёгкое, потому что приоритет — скорость распаковки, и оно эффективно, потому что колоночные данные однородны и локально похожи.

Попробуй сам

Убедитесь, что сжатие в DuckDB реально и измеримо.

  1. Создайте две базы с одинаковой таблицей в миллион строк. Сначала persistent: duckdb comp.duckdb "CREATE TABLE t AS SELECT range AS id, range % 10 AS cat, (range % 100)::DOUBLE AS val FROM range(1000000);" и затем CHECKPOINT.
  2. Посмотрите размер файла на диске: ls -lh comp.duckdb. Прикиньте «наивный» размер несжатой таблицы: 1 млн строк, в каждой BIGINT (8 байт) плюс BIGINT плюс DOUBLE (8 байт) — порядка 24 МБ. Во сколько раз файл меньше наивной оценки?
  3. Посмотрите распределение по сегментам и применённое сжатие через системную функцию: SELECT column_name, compression, count(*) FROM pragma_storage_info('t') GROUP BY column_name, compression;. Какие схемы DuckDB выбрал для каждой колонки?
  4. Сравните с in-memory: запустите duckdb без файла, создайте ту же таблицу, посмотрите PRAGMA database_size — поле memory_usage. In-memory база не сжимается; сравните это с размером persistent-файла.
  5. Поразмышляйте: колонка cat содержит всего 10 различных значений на миллион строк. Почему она должна сжаться особенно сильно? А колонка id — строго возрастающая последовательность — почему её тоже можно сжать, хотя все значения различны?

Этот эксперимент показывает на цифрах, что сжатие в DuckDB не абстракция: файл реально в разы меньше наивного размера, и pragma_storage_info показывает, какой именно лёгкой схемой сжата каждая колонка.


Проверка знанийKnowledge check
Почему DuckDB использует lightweight-сжатие, а не сильные алгоритмы общего назначения вроде gzip, и почему степень сжатия — неполная метрика для оценки схемы?
ОтветAnswer
Главная причина сжимать данные в OLAP — не экономия места, а скорость запросов. Аналитический скан поднимает миллионы строк с диска, и диск — самое медленное звено: процессор обычно недозагружен и ждёт I/O. Сжатие уменьшает объём байт, которые надо прочитать с диска, и тем самым ускоряет узкое звено — а простаивающий CPU тратит часть мощности на распаковку, выгодный обмен. Но у сжатого блока два конфликтующих числа: степень сжатия и скорость декомпрессии. Сильные алгоритмы вроде gzip и Brotli дают высокую степень сжатия ценой медленной распаковки — они разбирают деревья Хаффмана, словари переменной длины, цепочки обратных ссылок. Если применить их в скане, узкое звено просто переезжает с диска на декомпрессор: CPU не успевает разжимать данные так же быстро, как теперь подаёт диск, и сильное сжатие не даёт ожидаемого ускорения. Поэтому степень сжатия — неполная метрика: схема, жмущая в 5 раз, но распаковывающаяся медленно, проиграет схеме, жмущей в 3 раза, но распаковывающейся на скорости сканирования. DuckDB выбирает lightweight-схемы: их декомпрессия идёт на скорости сканирования, потому что они распаковываются элементарными операциями процессора — сложением, битовыми сдвигами, индексацией в массив, — которые занимают единицы тактов и хорошо векторизуются. Цена честная: умеренная степень сжатия вместо максимальной, зато распаковка перестаёт быть узким звеном.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какая главная причина сжимать данные в аналитической СУБД вроде DuckDB?

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

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

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

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