Зачем lightweight-сжатие
В прошлом модуле мы разобрали, как DuckDB раскладывает данные по блокам, row groups и колоночным сегментам. Каждый сегмент при записи в persistent-файл сжимается. Но «сжимается» — слово ёмкое: gzip тоже сжимает, и LZ4, и Brotli. DuckDB сознательно использует не их, а особый класс схем — lightweight compression, лёгкое сжатие. Этот вводный урок модуля объясняет, что значит «лёгкое», почему для аналитической СУБД это принципиальный выбор, и почему правильная метрика тут не «во сколько раз ужалось», а «успевает ли декомпрессия за сканированием».
Сжатие в OLAP — это про скорость, а не только про место
Основы компрессии данных для аналитических форматов Внутренности алгоритмов компрессии: LZ77, Huffman, ANSПервая мысль про сжатие — экономия места на диске. Она верна, но для аналитической СУБД второстепенна. Главная причина сжимать данные в OLAP — скорость запросов.
Связь неочевидна, поэтому разберём по шагам. Аналитический запрос сканирует много данных — миллионы строк. Эти данные нужно поднять с диска в память. Диск — самое медленное звено в этой цепочке: даже быстрый SSD отдаёт данные на порядки медленнее, чем процессор их обрабатывает. Сканирование большой таблицы упирается не в вычисления, а в то, как быстро байты долетают с диска.
Теперь добавим сжатие. Сжатые данные занимают меньше байт. Меньше байт — меньше читать с диска. Если таблица сжалась вчетверо, с диска нужно поднять вчетверо меньше байт, и узкое звено — дисковый ввод-вывод — отрабатывает вчетверо быстрее. Сжатие превращает «лишнюю» работу процессора по распаковке в экономию на медленном диске. А поскольку процессор в OLAP-сканировании обычно недозагружен и ждёт диск, этот обмен почти всегда выгоден.
То же касается памяти и кэшей CPU. Сжатые данные плотнее лежат в оперативной памяти и в кэшах процессора. Больше полезных данных помещается в кэш — реже промахи, быстрее обработка. Сжатие работает на каждом уровне иерархии памяти, не только на диске.
В чём ловушка тяжёлого сжатия
Раз сжатие ускоряет, логично взять самый сильный алгоритм — gzip на максимальном уровне, или Brotli, — чтобы данные ужались как можно сильнее. Но тут ловушка, и она объясняет весь дизайн сжатия в DuckDB.
У сжатого блока есть два числа. Первое — степень сжатия: во сколько раз он стал меньше. Второе — скорость декомпрессии: как быстро его можно распаковать обратно. Эти два числа конфликтуют. Алгоритмы общего назначения вроде gzip и Brotli дают высокую степень сжатия, но платят за неё медленной распаковкой: они тратят процессорное время на разбор сложных внутренних структур — деревьев Хаффмана, словарей переменной длины, длинных цепочек обратных ссылок.
Посчитаем, к чему это ведёт в сканировании. Допустим, gzip ужал данные в 5 раз — отлично, с диска читаем в 5 раз меньше. Но распаковка gzip медленная, и процессор не успевает разжимать данные так же быстро, как теперь их подаёт диск. Узкое звено просто переехало: раньше ждали диск, теперь ждём декомпрессор. Запрос ограничен скоростью распаковки, и сильное сжатие не дало того ускорения, на которое рассчитывали.
В этом суть: для сканирующего OLAP-движка медленный декомпрессор так же вреден, как медленный диск. Степень сжатия бесполезна, если распаковка не успевает за сканированием.
Распространённая ошибка интуиции — оценивать схему сжатия только по степени сжатия (compression ratio). Для аналитической СУБД это половина картины. Схема, которая жмёт в 5 раз, но распаковывается медленно, проиграет схеме, которая жмёт в 3 раза, но распаковывается на скорости сканирования. Правильная метрика — связка обоих чисел, и часто решающее именно второе.
Lightweight compression: распаковка на скорости сканирования
Отсюда вытекает выбор DuckDB. Движок использует lightweight compression — лёгкое сжатие. «Лёгкое» — это про вычислительную стоимость распаковки: схемы спроектированы так, чтобы декомпрессия была почти бесплатной по меркам процессора.
Главный проектный критерий lightweight-схем: декомпрессия должна идти на скорости сканирования. Это значит, что разжать данные так быстро, что распаковка перестаёт быть узким звеном, — её стоимость теряется на фоне самого сканирования. Процессор поднимает сжатый блок с диска и разжимает его настолько быстро, что декомпрессор не отстаёт ни от диска, ни от последующих операторов запроса.
Достигается это за счёт простоты операций. Lightweight-схемы распаковываются не сложным разбором, а элементарными действиями процессора — сложением, битовыми сдвигами, копированием, индексацией в массив. Такие операции выполняются за единицы тактов и прекрасно векторизуются: процессор обрабатывает пачку значений одной SIMD-инструкцией. Никаких деревьев Хаффмана и цепочек обратных ссылок — только арифметика, которую CPU делает почти даром.
Цена компромисса честная: lightweight-схема обычно даёт меньшую степень сжатия, чем gzip на максимуме. Но взамен распаковка не тормозит запрос. Для OLAP это правильный размен: чуть больше байт на диске, зато сканирование не упирается в декомпрессор. DuckDB сознательно выбирает «жмём умеренно, но распаковываем молниеносно» вместо «жмём максимально, но распаковка душит запрос».
Почему именно колоночные данные жмутся хорошо
Lightweight-сжатие работает особенно эффективно именно в колоночной СУБД, и это не совпадение. В колоночном хранении значения одной колонки лежат подряд, одним потоком, — и такой поток обладает свойствами, которые лёгкие схемы используют напрямую.
Однородность типа. В колоночном сегменте все значения одного типа: все целые, все даты, все строки. Декомпрессор заранее знает, с чем имеет дело, и применяет специализированную под этот тип схему — для целых одну, для строк другую, для чисел с плавающей точкой третью. Не нужно на каждом значении выяснять его тип. В построчном хранении значения разных типов перемешаны в строке, и такой специализации нет.
Локальная похожесть значений. Соседние значения одной колонки часто похожи друг на друга. Колонка country — это десяток повторяющихся строк на миллионы записей. Колонка created_at — даты в узком диапазоне, отличающиеся младшими разрядами. Колонка status — горстка повторяющихся кодов. Эта похожесть и есть та избыточность, которую сжатие убирает: повторы кодируются один раз, узкий диапазон кодируется смещениями от общего минимума, и так далее.
В построчном хранении этой картины не видно: значения разных колонок чередуются, и похожесть соседей теряется в шуме. Колоночная раскладка собирает похожее рядом — а lightweight-схемы именно на однородных, локально похожих потоках и раскрываются в полную силу. Конкретные схемы — Constant, RLE, bit packing, FOR, dictionary, FSST, и схемы для чисел с плавающей точкой — мы разберём в следующих уроках модуля. Здесь важно понять фундамент: сжатие в DuckDB лёгкое, потому что приоритет — скорость распаковки, и оно эффективно, потому что колоночные данные однородны и локально похожи.
Попробуй сам
Убедитесь, что сжатие в DuckDB реально и измеримо.
- Создайте две базы с одинаковой таблицей в миллион строк. Сначала 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. - Посмотрите размер файла на диске:
ls -lh comp.duckdb. Прикиньте «наивный» размер несжатой таблицы: 1 млн строк, в каждой BIGINT (8 байт) плюс BIGINT плюс DOUBLE (8 байт) — порядка 24 МБ. Во сколько раз файл меньше наивной оценки? - Посмотрите распределение по сегментам и применённое сжатие через системную функцию:
SELECT column_name, compression, count(*) FROM pragma_storage_info('t') GROUP BY column_name, compression;. Какие схемы DuckDB выбрал для каждой колонки? - Сравните с in-memory: запустите
duckdbбез файла, создайте ту же таблицу, посмотритеPRAGMA database_size— полеmemory_usage. In-memory база не сжимается; сравните это с размером persistent-файла. - Поразмышляйте: колонка
catсодержит всего 10 различных значений на миллион строк. Почему она должна сжаться особенно сильно? А колонкаid— строго возрастающая последовательность — почему её тоже можно сжать, хотя все значения различны?
Этот эксперимент показывает на цифрах, что сжатие в DuckDB не абстракция: файл реально в разы меньше наивного размера, и pragma_storage_info показывает, какой именно лёгкой схемой сжата каждая колонка.