В этой статье мы разберем один из способов оптимизации хранения данных и запросов, который поможет ускорить процесс выполнения задачи с помощью использования кодеков сжатия в колонках. И протестируем какие результаты можно получить при использовании кодеков.
В clickhouse есть несколько алгоритмов сжатия: LZ4 (по умолчанию), ZSTD, LZ4HC и экспериментальный DEFLATE_QPL. Подробнее про них можно прочитать в документации.
В clickhouse есть несколько кодеков для обработки данных:
Delta(delta_bytes) — Метод сжатия, при котором необработанные значения заменяются разностью двух соседних значений, за исключением первого значения, которое остается неизменным.
DoubleDelta - Вычисляет дельту дельт и записывает ее в компактной двоичной форме.
Gorilla - Вычисляет XOR между текущим и предыдущим значением с плавающей запятой и записывает его в компактной двоичной форме.
FPC — Новый кодек сжатия для чисел с плавающей точкой, появился в версии 22.6.
T64 — Обрезает лишние биты для целых значений, включая типы даты/времени.
А как оптимизировать хранение для строк?
Также есть способы оптимизации для строк, но с ними меньше вариантов, если в колонке со строками вариативность значений меньше 10 тысяч, то отличный эффект по скорости и сжатию даст тип колонки LowCardinality(String)
https://clickhouse.com/docs/ru/sql-reference/data-types/lowcardinality/
Какой кодек когда использовать?
И второй вопрос, кодеки сжимают данные, но как влияют на скорость запросов?
С одной стороны применение кодека требует выполнять операцию по декодированию данных, с другой, сжатые данные могут занимать меньше места на диске, а следовательно операция чтения при запросе будет происходить быстрее.
Кодеки также дают разные результат на 32-битных и 64-битных числах, поэтому будем сравнить и разряд чисел.
Рассмотрим работу кодеков для нескольких типов распределения данных в таблице:
rand_seq - cлучайная последовательность чисел;
const_seq - монотонно-возрастающая последовательность;
gauss_seq - гауссово распределение;
Для следующих типов float32, float64, int32, int64 и DateTime. Алгоритм сжатия LZ4.
Заполним таблицу 100 млн. строк и сравним результаты с кодеками и без. Скорость выполнения запросов будем проверять таким запросом
SELECT max(test_column) from test_table
Код создания таблицы и кодеков для колонок
create table test_table
(
rand_seq_Int64_raw Int64,
rand_seq_Int64_T64 Int64 CODEC(T64, LZ4),
rand_seq_Int64_Delta Int64 CODEC(Delta(8), LZ4),
rand_seq_Int64_DoubleDelta Int64 CODEC(DoubleDelta, LZ4),
rand_seq_Int64_Gorilla Int64 CODEC(Gorilla, LZ4),
rand_seq_Int32_raw Int32,
rand_seq_Int32_T64 Int32 CODEC(T64, LZ4),
rand_seq_Int32_Delta Int32 CODEC(Delta(8), LZ4),
rand_seq_Int32_DoubleDelta Int32 CODEC(DoubleDelta, LZ4),
rand_seq_Int32_Gorilla Int32 CODEC(Gorilla, LZ4),
rand_seq_DateTime_raw DateTime,
rand_seq_DateTime_T64 DateTime CODEC(T64, LZ4),
rand_seq_DateTime_Delta DateTime CODEC(Delta(8), LZ4),
rand_seq_DateTime_DoubleDelta DateTime CODEC(DoubleDelta, LZ4),
rand_seq_DateTime_Gorilla DateTime CODEC(Gorilla, LZ4),
const_seq_Int64_raw Int64,
const_seq_Int64_T64 Int64 CODEC(T64, LZ4),
const_seq_Int64_Delta Int64 CODEC(Delta(8), LZ4),
const_seq_Int64_DoubleDelta Int64 CODEC(DoubleDelta, LZ4),
const_seq_Int64_Gorilla Int64 CODEC(Gorilla, LZ4),
const_seq_Int32_raw Int32,
const_seq_Int32_T64 Int32 CODEC(T64, LZ4),
const_seq_Int32_Delta Int32 CODEC(Delta(8), LZ4),
const_seq_Int32_DoubleDelta Int32 CODEC(DoubleDelta, LZ4),
const_seq_Int32_Gorilla Int32 CODEC(Gorilla, LZ4),
const_seq_DateTime_raw DateTime,
const_seq_DateTime_T64 DateTime CODEC(T64, LZ4),
const_seq_DateTime_Delta DateTime CODEC(Delta(8), LZ4),
const_seq_DateTime_DoubleDelta DateTime CODEC(DoubleDelta, LZ4),
const_seq_DateTime_Gorilla DateTime CODEC(Gorilla, LZ4),
gauss_float_seq_Float64_raw Float64,
gauss_float_seq_Float64_Delta Float64 CODEC(Delta(8), LZ4),
gauss_float_seq_Float64_DoubleDelta Float64 CODEC(DoubleDelta, LZ4),
gauss_float_seq_Float64_Gorilla Float64 CODEC(Gorilla, LZ4),
gauss_float_seq_Float64_FPC Float64 CODEC(FPC, LZ4),
gauss_float_seq_Float32_raw Float32,
gauss_float_seq_Float32_Delta Float32 CODEC(Delta(8), LZ4),
gauss_float_seq_Float32_DoubleDelta Float32 CODEC(DoubleDelta, LZ4),
gauss_float_seq_Float32_Gorilla Float32 CODEC(Gorilla, LZ4),
gauss_float_seq_Float32_FPC Float32 CODEC(FPC, LZ4),
rand_float_seq_Float64_raw Float64,
rand_float_seq_Float64_Delta Float64 CODEC(Delta(8), LZ4),
rand_float_seq_Float64_DoubleDelta Float64 CODEC(DoubleDelta, LZ4),
rand_float_seq_Float64_Gorilla Float64 CODEC(Gorilla, LZ4),
rand_float_seq_Float64_FPC Float64 CODEC(FPC, LZ4),
rand_float_seq_Float32_raw Float32,
rand_float_seq_Float32_Delta Float32 CODEC(Delta(8), LZ4),
rand_float_seq_Float32_DoubleDelta Float32 CODEC(DoubleDelta, LZ4),
rand_float_seq_Float32_Gorilla Float32 CODEC(Gorilla, LZ4),
rand_float_seq_Float32_FPC Float32 CODEC(FPC, LZ4),
const_float_seq_Float64_raw Float64,
const_float_seq_Float64_Delta Float64 CODEC(Delta(8), LZ4),
const_float_seq_Float64_DoubleDelta Float64 CODEC(DoubleDelta, LZ4),
const_float_seq_Float64_Gorilla Float64 CODEC(Gorilla, LZ4),
const_float_seq_Float64_FPC Float64 CODEC(FPC, LZ4),
const_float_seq_Float32_raw Float32,
const_float_seq_Float32_Delta Float32 CODEC(Delta(8), LZ4),
const_float_seq_Float32_DoubleDelta Float32 CODEC(DoubleDelta, LZ4),
const_float_seq_Float32_Gorilla Float32 CODEC(Gorilla, LZ4),
const_float_seq_Float32_FPC Float32 CODEC(FPC, LZ4),
gauss_int_seq_Int64_raw Int64,
gauss_int_seq_Int64_T64 Int64 CODEC(T64, LZ4),
gauss_int_seq_Int64_Delta Int64 CODEC(Delta(8), LZ4),
gauss_int_seq_Int64_DoubleDelta Int64 CODEC(DoubleDelta, LZ4),
gauss_int_seq_Int64_Gorilla Int64 CODEC(Gorilla, LZ4),
gauss_int_seq_Int32_raw Int32,
gauss_int_seq_Int32_T64 Int32 CODEC(T64, LZ4),
gauss_int_seq_Int32_Delta Int32 CODEC(Delta(8), LZ4),
gauss_int_seq_Int32_DoubleDelta Int32 CODEC(DoubleDelta, LZ4),
gauss_int_seq_Int32_Gorilla Int32 CODEC(Gorilla, LZ4),
dt DateTime default now()
)
engine = MergeTree ORDER BY dt;
Результаты замеров
Выводы
Для случайных данных - заметный прирост по скорости и сжатию дает применение кодека Т64, но только если большая часть значений в колонке значительно меньше квинтиллиона. В большинстве случаев int64 используется для хранения куда меньших чисел.
Если вам повезло и данные монотонно возрастающие то вариантов оптимизации много, в идеальном случае для int лучшее сжатие DoubleDelta, Delta оптимальнее по сжатию и скорости чтения. Для float лучший результат сжатия дает новый кодек FPC, но чтение будет медленнее.
Для распределения гаусса (например данные метрик) - для int64 и int32 лучшее сжатие и время выполнение обеспечивает кодек T64. Для float сжатие дает только новый кодек FPC
Как добавить кодек к существующей таблице?
ALTER TABLE test_table MODIFY COLUMN column_a CODEC(ZSTD(2));
но работает только с новыми данными в таблицу, чтобы применить кодек к старым данным:
ALTER TABLE test_table UPDATE column_a = column_a WHERE 1
Комментарии (5)
miga
00.00.0000 00:00+1Я конечно извиняюсь, но мерять компрессию для случайных данных - это весьма специфичная забава. Меряйте компрессию своих данных.
Если говорить о кодеках в КХ, то подход очень простой - понять что у вас за данные, и как они лежат; подобрать ключ сортировки таким образом, чтобы энтропия была минимальной (тут еще, конечно, надо учитывать то, как вы эти данные потом запрашиваете), и уже потом поиграться с кодеками (которые, кстати, можно накладывать друг на друга - сначала пожать даблдельтой, а сверху придавить LZ4, например).
Что касается баланса степени сжатия и скорости разжатия, я б сказал что почти всегда сильнее сжатые данные будут обрабатываться быстрее - меньше данных гонять с диска в память, и из памяти в процессор.
EvgenyVilkov
00.00.0000 00:00Мой любимый вопрос на собеседовании как раз - как влияет на производительность сжатие. 9 из 10 дают неправильный ответ, причём далеко не джуны
RogerSmith
00.00.0000 00:00Спасибо за материал. Подскажите, какая версия ClickHouse использовалась при замерах?
seriych
00.00.0000 00:00+1Странно, что в тест не включили просто сжатие ZSTD без кодеков.
От себя могу добавить:
в первую очередь подбираем ORDER BY таблиц, потом уже тестируем кодеки, если нужно
тестируйте на своих реальных данных для каждой конкретной таблицы
для целых чисел (включая Date, DateTime, Decimal и Enum) часто хорош вариант (T64, LZ4)
для строк не ошибкой будет всегда выбирать между двумя вариантами: ZSTD или LowCardinality
LowCardinality может быть выгоден и гораздо больше чем для 10000 уникальных значений, особенно если строки длинные
для коротких строк можно по умолчанию оставлять LZ4
LZ4HC очень медленный на вставку
pavel_pimenov
Спасибо, а для повторяемости и корректировок эксперимента код тестов/отчетов опубликуете (github)?