Перейти к основному содержанию
Тип данных 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
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':1, 'key2':10}), ({'key1':2,'key2':20}), ({'key1':3,'key2':30});
Чтобы выбрать значения key2:
Query
SELECT m['key2'] FROM tab;
Response
┌─arrayElement(m, 'key2')─┐
│                      10 │
│                      20 │
│                      30 │
└─────────────────────────┘
Если запрошенный ключ k отсутствует в map, m[k] возвращает default value для типа значения, например 0 для целочисленных типов и '' для строковых типов. Чтобы проверить, существует ли ключ в map, можно использовать функцию mapContains.
Query
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':100}), ({});
SELECT m['key1'] FROM tab;
Response
┌─arrayElement(m, 'key1')─┐
│                     100 │
│                       0 │
└─────────────────────────┘

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

Значения типа Tuple() можно преобразовать в значения типа Map() с помощью функции CAST: Пример
Query
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
Response
┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘

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

Чтобы не считывать Map целиком, в некоторых случаях можно использовать подстолбцы keys и values. Пример
Query
CREATE TABLE tab (m Map(String, UInt64)) ENGINE = Memory;
INSERT INTO tab VALUES (map('key1', 1, 'key2', 2, 'key3', 3));

SELECT m.keys FROM tab; --   то же самое, что mapKeys(m)
SELECT m.values FROM tab; -- то же самое, что mapValues(m)
Response
┌─m.keys─────────────────┐
│ ['key1','key2','key3'] │
└────────────────────────┘

┌─m.values─┐
│ [1,2,3]  │
└──────────┘

Сериализация Map по бакетам в MergeTree

По умолчанию столбец Map в MergeTree хранится как единый поток Array(Tuple(K, V)). Чтение одного ключа через m['key'] требует сканирования всего столбца — каждой пары ключ-значение в каждой строке — даже если нужен только один ключ. Для Map с большим количеством различных ключей это становится узким местом. Сериализация по бакетам (with_buckets) разбивает пары ключ-значение на несколько независимых подпотоков (бакетов) по хешу ключа. Когда запрос обращается к m['key'], с диска читается только бакет, содержащий этот ключ, а все остальные бакеты пропускаются.

Включение сериализации Map по бакетам

CREATE TABLE tab (id UInt64, m Map(String, UInt64))
ENGINE = MergeTree ORDER BY id
SETTINGS
    map_serialization_version = 'with_buckets',
    max_buckets_in_map = 32,
    map_buckets_strategy = 'sqrt';
Чтобы не замедлять вставки, можно оставить сериализацию basic для частей нулевого уровня (создаваемых при INSERT) и использовать with_buckets только для слитых частей:
CREATE TABLE tab (id UInt64, m Map(String, UInt64))
ENGINE = MergeTree ORDER BY id
SETTINGS
    map_serialization_version = 'with_buckets',
    map_serialization_version_for_zero_level_parts = 'basic',
    max_buckets_in_map = 32,
    map_buckets_strategy = 'sqrt';

Как это работает

Когда часть данных записывается с сериализацией 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 могут сосуществовать в одной таблице и прозрачно сливаться.

Настройки

НастройкаПо умолчаниюОписание
map_serialization_versionbasicФормат сериализации для столбцов Map. basic хранит данные в виде одного потока массива. with_buckets разбивает ключи по бакетам для более быстрого чтения по одному ключу.
map_serialization_version_for_zero_level_partsbasicФормат сериализации для частей нулевого уровня (создаются при INSERT). Позволяет использовать basic для вставок, чтобы избежать накладных расходов на запись, тогда как слитые части используют with_buckets.
max_buckets_in_map32Верхняя граница числа бакетов. Фактическое число зависит от map_buckets_strategy. Максимально допустимое значение — 256.
map_buckets_strategysqrtСтратегия вычисления числа бакетов на основе среднего размера Map: constant — всегда использовать max_buckets_in_map; sqrt — использовать round(coefficient * sqrt(avg_size)); linear — использовать round(coefficient * avg_size). Результат ограничивается диапазоном [1, max_buckets_in_map].
map_buckets_coefficient1.0Множитель для стратегий sqrt и linear. Игнорируется, если выбрана стратегия constant.
map_buckets_min_avg_size32Минимальное среднее число ключей в строке для включения разбиения по бакетам. Если среднее значение ниже этого порога, используется один бакет независимо от остальных настроек. Установите 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

Тип данных JSON хранит каждый часто используемый путь в виде отдельного динамического подстолбца. Пути, превышающие лимит max_dynamic_paths, попадают в общую структуру данных, где для оптимизации чтения отдельных путей может использоваться сериализация advanced. Подробный обзор сериализации advanced см. в статье блога.
АспектMap с бакетамиJSON
Чтение одного ключаСчитывается один бакет (он может содержать и другие ключи). Десериализуются все пары ключ-значение в этом бакете.Часто используемые пути читаются напрямую из динамических подстолбцов. Редко используемые пути попадают в общие данные; при сериализации advanced считываются данные только для точного пути.
Типы значенийВсе значения имеют один и тот же тип VКаждый путь может иметь собственный тип. Для путей без подсказки типа используется Dynamic.
Поддержка индекса пропуска данныхРаботает с некоторыми типами индексов, созданных на mapKeys/mapValuesИндекс пропуска данных можно создать только для подстолбцов конкретных путей, но не сразу для всех путей/значений.
Чтение полного столбцаПримерно в 2 раза медленнее, чем basic, из-за повторной сборки бакетовДополнительные накладные расходы из-за кодирования типа Dynamic и реконструкции путей.
Накладные расходы на хранилищеМинимальные дополнительные метаданныеВыше из-за кодирования типа Dynamic, хранения имён путей и дополнительных метаданных в сериализации advanced.
Гибкость схемыФиксированные типы ключей и значений при создании таблицыПолностью динамическая — ключи и типы значений могут различаться от строки к строке. Для известных путей можно объявить типизированные подсказки путей для прямого доступа к подстолбцам.
Используйте JSON, если разным ключам требуются разные типы значений, если набор ключей существенно меняется от строки к строке или если часто используемые ключи известны заранее и могут быть объявлены как типизированные пути для прямого доступа к подстолбцам.

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

Вы можете вручную разделить один Map на несколько столбцов по хешу ключа на уровне приложения:
CREATE TABLE tab (
    id UInt64,
    m0 Map(String, UInt64),
    m1 Map(String, UInt64),
    m2 Map(String, UInt64),
    m3 Map(String, UInt64)
) ENGINE = MergeTree ORDER BY id;
Во время вставки направляйте каждую пару ключ-значение в столбец m{hash(key) % 4}. При выполнении запросов читайте из соответствующего столбца: m{hash('target_key') % 4}['target_key'].
АспектMap с бакетамиРучное сегментирование
Простота использованияПрозрачно — обрабатывается движком храненияТребует логики маршрутизации на уровне приложения для вставок и выборок
Вертикальное слияниеНе поддерживается — все бакеты относятся к одному столбцуПоддерживается — каждый столбец Map является независимым столбцом и может сливаться вертикально
Изменения схемыЧисло бакетов автоматически подстраивается для каждой частиИзменение числа сегментов требует перезаписи данных или добавления новых столбцов
Синтаксис запросаm['key'] работает напрямуюНужно вычислить правильный столбец: m0['key'], m1['key'] и т. д.
Гранулярность бакетовДля каждой части, подстраивается под статистику данныхФиксируется при создании таблицы
Ручное сегментирование полезно, когда Вертикальное слияние важно для снижения использования памяти при слиянии таблиц с большим количеством столбцов, или когда число сегментов должно быть фиксированным и явно контролироваться. Для большинства сценариев автоматическая сериализация с бакетами проще и вполне достаточна. См. также
Последнее изменение 10 июня 2026 г.