Map(K, V) хранит пары ключ-значение.
В отличие от других баз данных, в ClickHouse значения типа Map не требуют уникальности ключей, то есть map может содержать два элемента с одним и тем же ключом.
(Это связано с тем, что внутри map реализован как Array(Tuple(K, V)).)
Вы можете использовать синтаксис m[k], чтобы получить значение по ключу k в map m.
Кроме того, m[k] выполняет сканирование map, то есть время выполнения этой операции линейно зависит от размера map.
Параметры
K— Тип ключей Map. Произвольный тип, кроме Nullable и LowCardinality, вложенного в Nullable.V— Тип значений Map. Произвольный тип.
Query
key2:
Query
Response
k отсутствует в map, m[k] возвращает default value для типа значения, например 0 для целочисленных типов и '' для строковых типов.
Чтобы проверить, существует ли ключ в map, можно использовать функцию mapContains.
Query
Response
Преобразование Tuple в Map
Tuple() можно преобразовать в значения типа Map() с помощью функции CAST:
Пример
Query
Response
Чтение подстолбцов типа Map
keys и values.
Пример
Query
Response
Сериализация Map по бакетам в MergeTree
Map в MergeTree хранится как единый поток Array(Tuple(K, V)).
Чтение одного ключа через m['key'] требует сканирования всего столбца — каждой пары ключ-значение в каждой строке — даже если нужен только один ключ.
Для Map с большим количеством различных ключей это становится узким местом.
Сериализация по бакетам (with_buckets) разбивает пары ключ-значение на несколько независимых подпотоков (бакетов) по хешу ключа.
Когда запрос обращается к m['key'], с диска читается только бакет, содержащий этот ключ, а все остальные бакеты пропускаются.
Включение сериализации Map по бакетам
basic для частей нулевого уровня (создаваемых при INSERT) и использовать with_buckets только для слитых частей:
Как это работает
with_buckets:
- Среднее количество ключей на строку вычисляется по статистике блока.
- Количество бакетов определяется настроенной стратегией (см. Настройки).
- Каждая пара ключ-значение назначается бакету путём хеширования ключа:
bucket = hash(key) % num_buckets. - Каждый бакет сохраняется как независимый подпоток со своими ключами, значениями и смещениями.
- Поток метаданных
buckets_infoхранит количество бакетов и статистику.
m['key']), оптимизатор преобразует выражение в подстолбец ключа (m.key_<serialized_key>).
Уровень сериализации вычисляет, к какому бакету относится запрошенный ключ, и читает с диска только этот бакет.
Когда читается весь Map (например, SELECT m), считываются все бакеты и заново собираются в исходный Map. Это медленнее, чем сериализация basic, из-за накладных расходов на чтение и слияние нескольких подпотоков.
Порядок ключей внутри значения Map при использовании сериализации
with_buckets может отличаться от исходного порядка вставки. Ключи распределяются по бакетам по хешу и затем собираются заново в порядке бакетов, а не в порядке вставки. При сериализации basic порядок ключей во вставленных значениях Map сохраняется.basic и with_buckets могут сосуществовать в одной таблице и прозрачно сливаться.
Настройки
| Настройка | По умолчанию | Описание |
|---|---|---|
map_serialization_version | basic | Формат сериализации для столбцов Map. basic хранит данные в виде одного потока массива. with_buckets разбивает ключи по бакетам для более быстрого чтения по одному ключу. |
map_serialization_version_for_zero_level_parts | basic | Формат сериализации для частей нулевого уровня (создаются при INSERT). Позволяет использовать basic для вставок, чтобы избежать накладных расходов на запись, тогда как слитые части используют with_buckets. |
max_buckets_in_map | 32 | Верхняя граница числа бакетов. Фактическое число зависит от map_buckets_strategy. Максимально допустимое значение — 256. |
map_buckets_strategy | sqrt | Стратегия вычисления числа бакетов на основе среднего размера Map: constant — всегда использовать max_buckets_in_map; sqrt — использовать round(coefficient * sqrt(avg_size)); linear — использовать round(coefficient * avg_size). Результат ограничивается диапазоном [1, max_buckets_in_map]. |
map_buckets_coefficient | 1.0 | Множитель для стратегий sqrt и linear. Игнорируется, если выбрана стратегия constant. |
map_buckets_min_avg_size | 32 | Минимальное среднее число ключей в строке для включения разбиения по бакетам. Если среднее значение ниже этого порога, используется один бакет независимо от остальных настроек. Установите 0, чтобы отключить этот порог. |
Компромиссы производительности
with_buckets на производительность относительно сериализации basic при разных размерах Map (от 10 до 10 000 ключей на строку). Количество бакетов определялось по стратегии sqrt с ограничением 32. Точные значения зависят от типов ключей и значений, распределения данных и аппаратного обеспечения.
| Операция | 10 ключей | 100 ключей | 1 000 ключей | 10 000 ключей | Примечания |
|---|---|---|---|---|---|
Поиск по одному ключу (m['key']) | в 1.6–3.2 раза быстрее | в 4.5–7.7 раза быстрее | в 16–39 раз быстрее | в 21–49 раз быстрее | Считывается только один бакет, а не весь столбец целиком. |
| Поиск по 5 ключам | ~1x | в 1.5–3.1 раза быстрее | в 2.9–8.3 раза быстрее | в 4.5–6.7 раза быстрее | Для каждого ключа считывается свой бакет; некоторые бакеты могут пересекаться. |
PREWHERE (SELECT m WHERE m['key'] = ...) | в 1.5–3.0 раза быстрее | в 2.9–7.3 раза быстрее | в 5.3–31 раз быстрее | в 20–45 раз быстрее | Фильтр PREWHERE считывает только один бакет; полное чтение Map выполняется только для совпавших строк. Ускорение зависит от селективности — чем меньше совпавших гранул, тем меньше полный I/O для Map. |
Полное сканирование Map (SELECT m) | ~2x медленнее | ~2x медленнее | ~2x медленнее | ~2x медленнее | Нужно считать и заново собрать все бакеты. |
| INSERT | в 1.5–2.5 раза медленнее | в 1.5–2.5 раза медленнее | в 1.5–2.5 раза медленнее | в 1.5–2.5 раза медленнее | Дополнительные накладные расходы на хеширование ключей и запись в несколько подпотоков. |
Рекомендации
- Небольшие Map (в среднем < 32 ключей): Оставьте сериализацию
basic. Для небольших Map накладные расходы на бакетизацию не оправданы. Значение по умолчаниюmap_buckets_min_avg_size = 32обеспечивает это автоматически. - Средние Map (32–100 ключей): Используйте
with_bucketsсо стратегиейsqrt, если запросы часто обращаются к отдельным ключам. Для lookup по одному ключу ускорение составляет 4–8x. - Большие Map (100+ ключей): Используйте
with_buckets. Lookup по одному ключу выполняются в 16–49x быстрее. Рассмотритеmap_serialization_version_for_zero_level_parts = 'basic', чтобы сохранить скорость вставки близкой к исходному уровню. - Если в рабочей нагрузке преобладают полные сканирования Map: Оставьте
basic. Сериализация Map по бакетам добавляет ~2x накладных расходов при полном сканировании. - Смешанная рабочая нагрузка (часть lookup по ключам, часть полных сканирований): Используйте
with_buckets, установив для zero-level parts значениеbasic. ОптимизацияPREWHEREсчитывает только релевантный бакет для фильтра, а затем читает полную Map только для совпавших строк, что дает заметное суммарное ускорение.
Альтернативные подходы
Map по бакетам не подходит для вашего сценария, есть два альтернативных способа повысить производительность доступа на уровне ключей:
Использование типа данных JSON
max_dynamic_paths, попадают в общую структуру данных, где для оптимизации чтения отдельных путей может использоваться сериализация advanced. Подробный обзор сериализации advanced см. в статье блога.
| Аспект | Map с бакетами | JSON |
|---|---|---|
| Чтение одного ключа | Считывается один бакет (он может содержать и другие ключи). Десериализуются все пары ключ-значение в этом бакете. | Часто используемые пути читаются напрямую из динамических подстолбцов. Редко используемые пути попадают в общие данные; при сериализации advanced считываются данные только для точного пути. |
| Типы значений | Все значения имеют один и тот же тип V | Каждый путь может иметь собственный тип. Для путей без подсказки типа используется Dynamic. |
| Поддержка индекса пропуска данных | Работает с некоторыми типами индексов, созданных на mapKeys/mapValues | Индекс пропуска данных можно создать только для подстолбцов конкретных путей, но не сразу для всех путей/значений. |
| Чтение полного столбца | Примерно в 2 раза медленнее, чем basic, из-за повторной сборки бакетов | Дополнительные накладные расходы из-за кодирования типа Dynamic и реконструкции путей. |
| Накладные расходы на хранилище | Минимальные дополнительные метаданные | Выше из-за кодирования типа Dynamic, хранения имён путей и дополнительных метаданных в сериализации advanced. |
| Гибкость схемы | Фиксированные типы ключей и значений при создании таблицы | Полностью динамическая — ключи и типы значений могут различаться от строки к строке. Для известных путей можно объявить типизированные подсказки путей для прямого доступа к подстолбцам. |
JSON, если разным ключам требуются разные типы значений, если набор ключей существенно меняется от строки к строке или если часто используемые ключи известны заранее и могут быть объявлены как типизированные пути для прямого доступа к подстолбцам.
Ручное разбиение одного Map на несколько столбцов
Map на несколько столбцов по хешу ключа на уровне приложения:
m{hash(key) % 4}. При выполнении запросов читайте из соответствующего столбца: m{hash('target_key') % 4}['target_key'].
| Аспект | Map с бакетами | Ручное сегментирование |
|---|---|---|
| Простота использования | Прозрачно — обрабатывается движком хранения | Требует логики маршрутизации на уровне приложения для вставок и выборок |
| Вертикальное слияние | Не поддерживается — все бакеты относятся к одному столбцу | Поддерживается — каждый столбец Map является независимым столбцом и может сливаться вертикально |
| Изменения схемы | Число бакетов автоматически подстраивается для каждой части | Изменение числа сегментов требует перезаписи данных или добавления новых столбцов |
| Синтаксис запроса | m['key'] работает напрямую | Нужно вычислить правильный столбец: m0['key'], m1['key'] и т. д. |
| Гранулярность бакетов | Для каждой части, подстраивается под статистику данных | Фиксируется при создании таблицы |
- функция map()
- функция CAST()
- комбинатор -Map для типа данных Map