Перейти к основному содержанию
Тип данных 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. Произвольный тип.
Примеры Создайте таблицу со столбцом типа Map:
Query
Чтобы выбрать значения key2:
Query
Response
Если запрошенный ключ k отсутствует в map, m[k] возвращает default value для типа значения, например 0 для целочисленных типов и '' для строковых типов. Чтобы проверить, существует ли ключ в map, можно использовать функцию mapContains.
Query
Response

Преобразование Tuple в Map

Значения типа Tuple() можно преобразовать в значения типа Map() с помощью функции CAST: Пример
Query
Response

Чтение подстолбцов типа Map

Чтобы не считывать 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:
  1. Среднее количество ключей на строку вычисляется по статистике блока.
  2. Количество бакетов определяется настроенной стратегией (см. Настройки).
  3. Каждая пара ключ-значение назначается бакету путём хеширования ключа: bucket = hash(key) % num_buckets.
  4. Каждый бакет сохраняется как независимый подпоток со своими ключами, значениями и смещениями.
  5. Поток метаданных 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

Тип данных JSON хранит каждый часто используемый путь в виде отдельного динамического подстолбца. Пути, превышающие лимит max_dynamic_paths, попадают в общую структуру данных, где для оптимизации чтения отдельных путей может использоваться сериализация advanced. Подробный обзор сериализации advanced см. в статье блога. Используйте JSON, если разным ключам требуются разные типы значений, если набор ключей существенно меняется от строки к строке или если часто используемые ключи известны заранее и могут быть объявлены как типизированные пути для прямого доступа к подстолбцам.

Ручное разбиение одного Map на несколько столбцов

Вы можете вручную разделить один Map на несколько столбцов по хешу ключа на уровне приложения:
Во время вставки направляйте каждую пару ключ-значение в столбец m{hash(key) % 4}. При выполнении запросов читайте из соответствующего столбца: m{hash('target_key') % 4}['target_key']. Ручное сегментирование полезно, когда Вертикальное слияние важно для снижения использования памяти при слиянии таблиц с большим количеством столбцов, или когда число сегментов должно быть фиксированным и явно контролироваться. Для большинства сценариев автоматическая сериализация с бакетами проще и вполне достаточна. См. также
Последнее изменение 10 июня 2026 г.