Понимание Индексов Пропуска Данных ClickHouse
Введение
На производительность запросов в ClickHouse влияют многие факторы. Критическим элементом в большинстве сценариев является то, может ли ClickHouse использовать первичный ключ при оценке условия WHERE в запросе. Соответственно, выбор первичного ключа, который применим к наиболее распространенным шаблонам запросов, является важным для эффективного проектирования таблицы.
Тем не менее, независимо от того, насколько тщательно настроен первичный ключ, неизбежно возникнут запросы, которые не смогут эффективно его использовать. Пользователи обычно полагаются на ClickHouse для данных временных рядов, но часто wish to analyze that same data according to other business dimensions, such as customer id, website URL, or product number. В таком случае производительность запроса может быть значительно хуже, потому что для применения условия WHERE может потребоваться полное сканирование каждого значения столбца. Хотя ClickHouse все еще относительно быстр в таких обстоятельствах, оценка миллионов или миллиардов отдельных значений приведет к тому, что "некешированные" запросы будут выполняться намного медленнее, чем те, которые основаны на первичном ключе.
В традиционной реляционной базе данных один из подходов к этой проблеме - это добавление одного или нескольких "вторичных" индексов к таблице. Это структура b-дерева, позволяющая базе данных находить все совпадающие строки на диске за O(log(n)) времени вместо O(n) времени (сканирования таблицы), где n - количество строк. Однако этот тип вторичного индекса не будет работать для ClickHouse (или других столбцовых баз данных), потому что на диске нет отдельных строк, которые можно добавить в индекс.
Вместо этого ClickHouse предоставляет другой тип индекса, который в определенных обстоятельствах может значительно улучшить скорость выполнения запросов. Эти структуры называются "индексами пропуска", потому что они позволяют ClickHouse пропускать чтение значительных объемов данных, которые гарантированно не содержат совпадающих значений.
Основная Операция
Пользователи могут использовать Индексы Пропуска Данных только на таблицах семейства MergeTree. Каждый индекс пропуска имеет четыре основных аргумента:
- Имя индекса. Имя индекса используется для создания файла индекса в каждой партиции. Также это требуется как параметр при удалении или материализации индекса.
- Выражение индекса. Выражение индекса используется для вычисления набора значений, хранящихся в индексе. Оно может быть комбинацией столбцов, простых операторов и/или подмножества функций, определенных типом индекса.
- TYPE. Тип индекса контролирует вычисление, которое определяет, возможно ли пропускать чтение и оценку каждого блока индекса.
- GRANULARITY. Каждый индексированный блок состоит из GRANULARITY гранул. Например, если гранулярность первичного индекса таблицы составляет 8192 строки, а гранулярность индекса составляет 4, каждый индексированный "блок" будет содержать 32768 строк.
Когда пользователь создает индекс пропуска данных, в каждом каталоге частей данных для таблицы будет два дополнительных файла.
skp_idx_{index_name}.idx
, который содержит упорядоченные значения выраженияskp_idx_{index_name}.mrk2
, который содержит соответствующие смещения в ассоциированных файлах данных столбцов.
Если какая-либо часть условия фильтрации WHERE совпадает с выражением индекса пропуска при выполнении запроса и чтении соответствующих файлов столбца, ClickHouse будет использовать данные файла индекса, чтобы определить, должен ли каждый соответствующий блок данных обрабатываться или может быть пропущен (при условии, что блок еще не был исключен с помощью применения первичного ключа). Для простоты рассмотрим следующий пример таблицы, загруженной предсказуемыми данными.
При выполнении простого запроса, который не использует первичный ключ, все 100 миллионов записей в столбце my_value
сканируются:
Теперь добавим очень базовый индекс пропуска:
Обычно индексы пропуска применяются только к вновь вставленным данным, поэтому просто добавление индекса не повлияет на вышеуказанный запрос.
Для индексирования уже существующих данных используйте следующую инструкцию:
Повторно выполните запрос с вновь созданным индексом:
Вместо обработки 100 миллионов строк по 800 мегабайт, ClickHouse прочитал и проанализировал только 32768 строк объемом 360 килобайт -- четыре гранулы по 8192 строки каждая.
В более наглядной форме это так, как были прочитаны и выбраны 4096 строк со значением my_value
равным 125, и как следующие строки
были пропущены без чтения с диска:

Пользователи могут получить подробную информацию о использовании индекса пропуска, включив трассировку при выполнении запросов. С помощью
clickhouse-client задайте send_logs_level
:
Это предоставит полезную отладочную информацию при настройке SQL запросов и индексов таблиц. Из вышеуказанного примера отладочный журнал показывает, что индекс пропуска исключил все, кроме двух гранул:
Типы Индексов Пропуска
minmax
Этот легковесный тип индекса не требует параметров. Он хранит минимальные и максимальные значения выражения индекса для каждого блока (если выражение является кортежем, то отдельно хранятся значения для каждого элемента кортежа). Этот тип хорошо подходит для столбцов, которые обычно слабо отсортированы по значению. Этот тип индекса чаще всего имеет наименьшие затраты при обработке запросов.
Этот тип индекса работает корректно только с скалярным или кортежным выражением -- индекс никогда не будет применяться к выражениям, которые возвращают массив или тип данных карты.
set
Этот легковесный тип индекса принимает один параметр, максимальный размер множества значений на блок (0 допускает неограниченное количество дискретных значений). Это множество содержит все значения в блоке (или пустое, если количество значений превышает максимальный размер). Этот тип индекса хорошо работает со столбцами с низкой кардинальностью в пределах каждого набора гранул (по сути, "сгруппированных вместе"), но с более высокой кардинальностью в общем.
Затраты, производительность и эффективность этого индекса зависят от кардинальности внутри блоков. Если каждый блок содержит большое количество уникальных значений, либо оценка условия запроса в отношении большого набора индекса будет очень дорогой, либо индекс не будет применен, потому что индекс пуст из-за превышения максимального размера.
Типы Фильтров Блума
Фильтр Блума это структура данных, позволяющая экономично тестировать членство в множестве с риском небольшого количества ложных срабатываний. Ложное срабатывание не является значительной проблемой в случае индексов пропуска, потому что единственное неудобство заключается в том, что необходимо прочитать несколько ненужных блоков. Однако вероятность ложных срабатываний означает, что ожидается, что индексируемое выражение должно быть истинным, в противном случае действительные данные могут быть пропущены.
Поскольку фильтры Блума могут более эффективно обрабатывать проверку на большое количество дискретных значений, они могут быть уместны для условных выражений, которые выдают больше значений для проверки. В частности, индекс фильтра Блума может применяться к массивам, где каждое значение массива проверяется, и к картам, преобразуя либо ключи, либо значения в массив с помощью функции mapKeys или mapValues.
Существует три типа Индексов Пропуска Данных, основанных на фильтрах Блума:
-
Базовый bloom_filter, который принимает один необязательный параметр допустимой ставки "ложного срабатывания" между 0 и 1 (если не указано, используется .025).
-
Специальный tokenbf_v1. Он принимает три параметра, все связанные с настройкой используемого фильтра Блума: (1) размер фильтра в байтах (большие фильтры имеют меньше ложных срабатываний, но увеличивают объем хранения), (2) количество применяемых хеш-функций (снова, большее количество хеш-функций уменьшает вероятность ложных срабатываний) и (3) начальное значение для хеш-функций фильтра Блума. Смотрите калькулятор здесь для получения дополнительной информации о том, как эти параметры влияют на функциональность фильтра Блума. Этот индекс работает только с типами данных String, FixedString и Map. Входное выражение разбивается на последовательности символов, разделенные неалфавитными символами. Например, значение столбца
This is a candidate for a "full text" search
будет содержать токеныThis
is
a
candidate
for
full
text
search
. Он предназначен для использования в LIKE, EQUALS, IN, hasToken() и подобных поисках слов и других значений в более длинных строках. Например, одно из возможных применений может заключаться в поиске небольшого количества имен классов или номеров строк в столбце произвольных строк логов приложений. -
Специальный ngrambf_v1. Этот индекс функционирует так же, как индексы токенов. Он принимает один дополнительный параметр перед настройками фильтра Блума, размер ngrams для индексации. Ngram - это строка символов длиной
n
, состоящая из любых символов, так что строкаA short string
с размером ngram равным 4 будет индексироваться как:
Этот индекс также может быть полезен для текстовых поисков, особенно в языках без пробелов между словами, таких как китайский.
Функции Индексов Пропуска
Основная цель индексов пропуска данных - ограничить количество данных, анализируемых популярными запросами. Учитывая аналитическую природу данных в ClickHouse, шаблон этих запросов в большинстве случаев включает функциональные выражения. Соответственно, индексы пропуска должны правильно взаимодействовать с общими функциями, чтобы быть эффективными. Это может происходить либо когда:
- данные вставляются, и индекс определен как функциональное выражение (результат выражения сохраняется в файлах индекса), либо
- запрос обрабатывается, и выражение применяется к сохраненным значениям индекса для определения, следует ли исключить блок.
Каждый тип индекса пропуска работает с подмножеством доступных функций ClickHouse, соответствующих реализации индекса, перечисленным здесь. В общем, индексы set и индексы на основе фильтров Блума (другой тип индекса set) являются неупорядоченными и, следовательно, не работают с диапазонами. В отличие от этого, индексы minmax работают особенно хорошо с диапазонами, так как определение того, пересекаются ли диапазоны, происходит очень быстро. Эффективность функций частичного соответствия LIKE, startsWith, endsWith и hasToken зависит от типа используемого индекса, выражения индекса и конкретной структуры данных.
Настройки Индексов Пропуска
Существуют две настройки, которые применяются к индексам пропуска.
- use_skip_indexes (0 или 1, по умолчанию 1). Не все запросы могут эффективно использовать индексы пропуска. Если конкретное условие фильтрации, вероятно, охватывает большинство гранул, применение индекса пропуска данных влечет за собой ненужные, а иногда и значительные затраты. Установите значение 0 для запросов, которые, вероятно, не получат выгоду от каких-либо индексов пропуска.
- force_data_skipping_indices (список имен индексов, разделенных запятыми). Эта настройка может быть использована для предотвращения некоторых видов неэффективных запросов. В обстоятельствах, когда запрос к таблице становится слишком дорогим, если индекс пропуска не используется, использование этой настройки с одним или несколькими именами индексов возвратит исключение для любого запроса, который не использует указанный индекс. Это предотвратит потребление ресурсов сервера плохо написанными запросами.
Лучшие Практики Пропуска
Индексы пропуска не интуитивно понятны, особенно для пользователей, привыкших к вторичным индексам на основе строк из области RDMS или обратным индексам из хранилищ документов. Чтобы получить какие-либо преимущества, применение индекса пропуска данных ClickHouse должно избежать достаточного количества чтений гранул, чтобы компенсировать затраты на вычисление индекса. Критически важно отметить, что если значение встречается даже один раз в индексированном блоке, это означает, что весь блок должен быть прочитан в память и оценен, и затраты на индекс были неразумно понесены.
Рассмотрим следующее распределение данных:

Предположим, что первичный/упорядоченный ключ - это timestamp
, и есть индекс на visitor_id
. Рассмотрите следующий запрос:
Традиционный вторичный индекс будет весьма полезен с таким распределением данных. Вместо чтения всех 32768 строк для нахождения 5 строк с запрашиваемым visitor_id, вторичный индекс будет содержать лишь пять местоположений строк, и только эти пять строк будут прочитаны с диска. Совершенно противоположное верно для индекса пропуска данных ClickHouse. Все 32768 значений в столбце visitor_id
будут проверены независимо от типа индекса пропуска.
Следовательно, естественное стремление ускорить запросы ClickHouse, просто добавив индекс к ключевым столбцам, часто является ошибочным. Эта расширенная функциональность должна использоваться только после исследования других альтернатив, таких как изменение первичного ключа (см. Как выбрать первичный ключ), использование проекций или использование материализованных представлений. Даже когда индекс пропуска данных является уместным, тщательная настройка как индекса, так и таблицы часто будет необходима.
В большинстве случаев полезный индекс пропуска требует сильной корреляции между первичным ключом и целевым, не первичным столбцом/выражением. Если корреляции нет (как на приведенной выше диаграмме), вероятность того, что условие фильтрации будет выполнено хотя бы для одной из строк в блоке из нескольких тысяч значений, высока, и немногие блоки будут пропущены. В отличие от этого, если диапазон значений для первичного ключа (например, время суток) сильно связан с значениями в потенциальном индексе (такими как возраст телезрителей), то индекс типа minmax вероятно будет полезен. Обратите внимание, что может быть возможно увеличить эту корреляцию при вставке данных, либо включая дополнительные столбцы в ключ сортировки/ORDER BY, либо группируя вставки таким образом, чтобы значения, ассоциированные с первичным ключом, были сгруппированы при вставке. Например, все события для конкретного site_id могут быть сгруппированы и вставлены вместе в процессе приема, даже если первичный ключ является временной меткой, содержащей события из большого количества сайтов. Это приведет к множеству гранул, которые содержат только несколько site_id, так что многие блоки могут быть пропущены при поиске по конкретному значению site_id.
Еще одним хорошим кандидатом для индекса пропуска являются выражения с высокой кардинальностью, где любое одно значение относительно разрежено в данных. Один из примеров может быть платформа мониторинга, которая отслеживает коды ошибок в API-запросах. Некоторые коды ошибок, хотя и редки в данных, могут быть особенно важными для поиска. Индекс установки на столбце error_code позволит пропустить большинство блоков, которые не содержат ошибок и, следовательно, значительно улучшить запросы, сосредоточенные на ошибках.
Наконец, ключевая лучшая практика - тестировать, тестировать и тестировать. В отличие от вторичных индексов b-дерева или обратных индексов для поиска документов, поведение индексов пропуска данных не столь легко предсказуемо. Их добавление в таблицу влечет за собой значительные затраты, как на прием данных, так и на запросы, которые по какой-то причине не извлекают выгоду от индекса. Их всегда следует тестировать на данных реального мира, а тестирование должно включать варианты типа, размера гранулярности и другие параметры. Тестирование часто выявляет шаблоны и ловушки, которые не очевидны из одних лишь мысленных экспериментов.