В этой статье мы разберем один из способов оптимизации хранения данных и запросов, который поможет ускорить процесс выполнения задачи с помощью использования кодеков сжатия в колонках. И протестируем какие результаты можно получить при использовании кодеков.

В 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)


  1. pavel_pimenov
    00.00.0000 00:00
    +2

    Спасибо, а для повторяемости и корректировок эксперимента код тестов/отчетов опубликуете (github)?


  1. miga
    00.00.0000 00:00
    +1

    Я конечно извиняюсь, но мерять компрессию для случайных данных - это весьма специфичная забава. Меряйте компрессию своих данных.

    Если говорить о кодеках в КХ, то подход очень простой - понять что у вас за данные, и как они лежат; подобрать ключ сортировки таким образом, чтобы энтропия была минимальной (тут еще, конечно, надо учитывать то, как вы эти данные потом запрашиваете), и уже потом поиграться с кодеками (которые, кстати, можно накладывать друг на друга - сначала пожать даблдельтой, а сверху придавить LZ4, например).

    Что касается баланса степени сжатия и скорости разжатия, я б сказал что почти всегда сильнее сжатые данные будут обрабатываться быстрее - меньше данных гонять с диска в память, и из памяти в процессор.


    1. EvgenyVilkov
      00.00.0000 00:00

      Мой любимый вопрос на собеседовании как раз - как влияет на производительность сжатие. 9 из 10 дают неправильный ответ, причём далеко не джуны


  1. RogerSmith
    00.00.0000 00:00

    Спасибо за материал. Подскажите, какая версия ClickHouse использовалась при замерах?


  1. seriych
    00.00.0000 00:00
    +1

    Странно, что в тест не включили просто сжатие ZSTD без кодеков.

    От себя могу добавить:

    • в первую очередь подбираем ORDER BY таблиц, потом уже тестируем кодеки, если нужно

    • тестируйте на своих реальных данных для каждой конкретной таблицы

    • для целых чисел (включая Date, DateTime, Decimal и Enum) часто хорош вариант (T64, LZ4)

    • для строк не ошибкой будет всегда выбирать между двумя вариантами: ZSTD или LowCardinality

    • LowCardinality может быть выгоден и гораздо больше чем для 10000 уникальных значений, особенно если строки длинные

    • для коротких строк можно по умолчанию оставлять LZ4

    • LZ4HC очень медленный на вставку