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 могут сосуществовать в одной таблице и прозрачно сливаться.
Настройки
Компромиссы производительности
with_buckets на производительность относительно сериализации basic при разных размерах Map (от 10 до 10 000 ключей на строку). Количество бакетов определялось по стратегии sqrt с ограничением 32. Точные значения зависят от типов ключей и значений, распределения данных и аппаратного обеспечения.
Рекомендации
- Небольшие 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 см. в статье блога.
Используйте
JSON, если разным ключам требуются разные типы значений, если набор ключей существенно меняется от строки к строке или если часто используемые ключи известны заранее и могут быть объявлены как типизированные пути для прямого доступа к подстолбцам.
Ручное разбиение одного Map на несколько столбцов
Map на несколько столбцов по хешу ключа на уровне приложения:
m{hash(key) % 4}. При выполнении запросов читайте из соответствующего столбца: m{hash('target_key') % 4}['target_key'].
Ручное сегментирование полезно, когда Вертикальное слияние важно для снижения использования памяти при слиянии таблиц с большим количеством столбцов, или когда число сегментов должно быть фиксированным и явно контролироваться. Для большинства сценариев автоматическая сериализация с бакетами проще и вполне достаточна.
См. также
- функция map()
- функция CAST()
- комбинатор -Map для типа данных Map