> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-8c05c8a2.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Документация по типу данных Map в ClickHouse

# Map(K, V)

Тип данных `Map(K, V)` хранит пары ключ-значение.

В отличие от других баз данных, в ClickHouse значения типа Map не требуют уникальности ключей, то есть map может содержать два элемента с одним и тем же ключом.
(Это связано с тем, что внутри map реализован как `Array(Tuple(K, V))`.)

Вы можете использовать синтаксис `m[k]`, чтобы получить значение по ключу `k` в map `m`.
Кроме того, `m[k]` выполняет сканирование map, то есть время выполнения этой операции линейно зависит от размера map.

**Параметры**

* `K` — Тип ключей Map. Произвольный тип, кроме [Nullable](/ru/reference/data-types/nullable) и [LowCardinality](/ru/reference/data-types/lowcardinality), вложенного в [Nullable](/ru/reference/data-types/nullable).
* `V` — Тип значений Map. Произвольный тип.

**Примеры**

Создайте таблицу со столбцом типа Map:

```sql title="Query" theme={null}
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`:

```sql title="Query" theme={null}
SELECT m['key2'] FROM tab;
```

```text title="Response" theme={null}
┌─arrayElement(m, 'key2')─┐
│                      10 │
│                      20 │
│                      30 │
└─────────────────────────┘
```

Если запрошенный ключ `k` отсутствует в map, `m[k]` возвращает default value для типа значения, например `0` для целочисленных типов и `''` для строковых типов.
Чтобы проверить, существует ли ключ в map, можно использовать функцию [mapContains](/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsKey).

```sql title="Query" theme={null}
CREATE TABLE tab (m Map(String, UInt64)) ENGINE=Memory;
INSERT INTO tab VALUES ({'key1':100}), ({});
SELECT m['key1'] FROM tab;
```

```text title="Response" theme={null}
┌─arrayElement(m, 'key1')─┐
│                     100 │
│                       0 │
└─────────────────────────┘
```

<div id="converting-tuple-to-map">
  ## Преобразование Tuple в Map
</div>

Значения типа `Tuple()` можно преобразовать в значения типа `Map()` с помощью функции [CAST](/ru/reference/functions/regular-functions/type-conversion-functions#CAST):

**Пример**

```sql title="Query" theme={null}
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
```

```text title="Response" theme={null}
┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘
```

<div id="reading-subcolumns-of-map">
  ## Чтение подстолбцов типа Map
</div>

Чтобы не считывать Map целиком, в некоторых случаях можно использовать подстолбцы `keys` и `values`.

**Пример**

```sql title="Query" theme={null}
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)
```

```text title="Response" theme={null}
┌─m.keys─────────────────┐
│ ['key1','key2','key3'] │
└────────────────────────┘

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

<div id="bucketed-map-serialization">
  ## Сериализация Map по бакетам в MergeTree
</div>

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

Сериализация по бакетам (`with_buckets`) разбивает пары ключ-значение на несколько независимых подпотоков (бакетов) по хешу ключа.
Когда запрос обращается к `m['key']`, с диска читается только бакет, содержащий этот ключ, а все остальные бакеты пропускаются.

<div id="enabling-bucketed-serialization">
  ### Включение сериализации Map по бакетам
</div>

```sql theme={null}
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` только для слитых частей:

```sql theme={null}
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';
```

<div id="how-it-works">
  ### Как это работает
</div>

Когда часть данных записывается с сериализацией `with_buckets`:

1. Среднее количество ключей на строку вычисляется по статистике блока.
2. Количество бакетов определяется настроенной стратегией (см. [Настройки](#bucketed-map-settings)).
3. Каждая пара ключ-значение назначается бакету путём хеширования ключа: `bucket = hash(key) % num_buckets`.
4. Каждый бакет сохраняется как независимый подпоток со своими ключами, значениями и смещениями.
5. Поток метаданных `buckets_info` хранит количество бакетов и статистику.

Когда запрос читает конкретный ключ (`m['key']`), оптимизатор преобразует выражение в подстолбец ключа (`m.key_<serialized_key>`).
Уровень сериализации вычисляет, к какому бакету относится запрошенный ключ, и читает с диска только этот бакет.

Когда читается весь Map (например, `SELECT m`), считываются все бакеты и заново собираются в исходный Map. Это медленнее, чем сериализация `basic`, из-за накладных расходов на чтение и слияние нескольких подпотоков.

<Note>
  Порядок ключей внутри значения Map при использовании сериализации `with_buckets` может отличаться от исходного порядка вставки. Ключи распределяются по бакетам по хешу и затем собираются заново в порядке бакетов, а не в порядке вставки. При сериализации `basic` порядок ключей во вставленных значениях Map сохраняется.
</Note>

Количество бакетов может различаться между частями. Когда части с разным количеством бакетов сливаются, количество бакетов в новой части пересчитывается по объединённой статистике. Части с сериализацией `basic` и `with_buckets` могут сосуществовать в одной таблице и прозрачно сливаться.

<div id="bucketed-map-settings">
  ### Настройки
</div>

| Настройка                                        | По умолчанию | Описание                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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`, чтобы отключить этот порог.                                                                                     |

<div id="performance-trade-offs">
  ### Компромиссы производительности
</div>

В таблице ниже приведено сравнение влияния `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 раза медленнее | Дополнительные накладные расходы на хеширование ключей и запись в несколько подпотоков.                                                                                                                 |

<div id="recommendations">
  ### Рекомендации
</div>

* **Небольшие 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 только для совпавших строк, что дает заметное суммарное ускорение.

<div id="map-alternatives">
  ### Альтернативные подходы
</div>

Если сериализация `Map` по бакетам не подходит для вашего сценария, есть два альтернативных способа повысить производительность доступа на уровне ключей:

<div id="using-the-json-data-type">
  #### Использование типа данных JSON
</div>

Тип данных [JSON](/ru/reference/data-types/newjson) хранит каждый часто используемый путь в виде отдельного динамического подстолбца. Пути, превышающие лимит `max_dynamic_paths`, попадают в [общую структуру данных](/ru/reference/data-types/newjson#shared-data-structure), где для оптимизации чтения отдельных путей может использоваться сериализация `advanced`. Подробный обзор сериализации `advanced` см. в [статье блога](https://clickhouse.com/blog/json-data-type-gets-even-better).

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

Используйте `JSON`, если разным ключам требуются разные типы значений, если набор ключей существенно меняется от строки к строке или если часто используемые ключи известны заранее и могут быть объявлены как типизированные пути для прямого доступа к подстолбцам.

<div id="manual-sharding-into-multiple-map-columns">
  #### Ручное разбиение одного `Map` на несколько столбцов
</div>

Вы можете вручную разделить один `Map` на несколько столбцов по хешу ключа на уровне приложения:

```sql theme={null}
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']` и т. д.                              |
| Гранулярность бакетов  | Для каждой части, подстраивается под статистику данных      | Фиксируется при создании таблицы                                                                  |

Ручное сегментирование полезно, когда Вертикальное слияние важно для снижения использования памяти при слиянии таблиц с большим количеством столбцов, или когда число сегментов должно быть фиксированным и явно контролироваться. Для большинства сценариев автоматическая сериализация с бакетами проще и вполне достаточна.

**См. также**

* функция [map()](/ru/reference/functions/regular-functions/tuple-map-functions#map)
* функция [CAST()](/ru/reference/functions/regular-functions/type-conversion-functions#CAST)
* [комбинатор -Map для типа данных Map](/ru/reference/functions/aggregate-functions/combinators#-map)

<div id="related-content">
  ## Связанные материалы
</div>

* Блог: [Как создать решение для обсервабилити на базе ClickHouse — часть 2: traces](https://clickhouse.com/blog/storing-traces-and-spans-open-telemetry-in-clickhouse)
