跳转到主要内容
数据类型 Map(K, V) 用于存储键值对。 与其他数据库不同,ClickHouse 中的 Map 不要求键唯一,也就是说,一个 Map 中可以包含两个键相同的元素。 (这是因为 Map 在内部实现为 Array(Tuple(K, V))。) 你可以使用语法 m[k] 获取 Map m 中键 k 对应的值。 另外,m[k] 会扫描整个 Map`,也就是说,该操作的运行时间与 Map 的大小呈线性关系。 参数
  • K — Map 键的类型。除 Nullable 以及嵌套了 Nullable 类型的 LowCardinality 外,可以是任意类型。
  • 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 不在映射中,m[k] 会返回该值类型的默认值,例如整数类型返回 0,字符串类型返回 ''。 要检查映射中是否存在某个键,可以使用函数 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() 类型的值可以使用函数 CAST 转换成 Map() 类型的值: 示例
Query
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
Response
┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘

读取 Map 的子列

为避免读取整个 Map,在某些情况下可以使用子列 keysvalues 示例
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]  │
└──────────┘

MergeTree 中的分桶 Map 序列化

默认情况下,MergeTree 中的 Map 列存储为单个 Array(Tuple(K, V)) stream。 使用 m['key'] 读取某个键时,需要扫描整列——每一行中的每一个键值对——即使只需要其中一个键也是如此。 对于包含大量不同键的 Map,这会成为性能瓶颈。 分桶序列化 (with_buckets) 会通过对键进行哈希,将键值对拆分到多个彼此独立的子stream (桶) 中。 当查询访问 m['key'] 时,只会从磁盘读取包含该键的那个桶,跳过其他所有桶。

启用分桶序列化

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';
为避免影响插入速度,你可以对零级 parts (在 INSERT 期间创建) 保留 basic 序列化,仅对已合并的 parts 使用 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. 每个桶都会作为独立子stream存储,并拥有各自的键、值和偏移量。
  5. buckets_info 元数据stream会记录桶数量及相关统计信息。
当查询读取某个特定键 (m['key']) 时,优化器会将该表达式重写为键子列 (m.key_<serialized_key>) 。 序列化层会计算所请求的键属于哪个桶,并且只从磁盘读取这一个桶。 当读取完整的 Map 时 (例如 SELECT m) ,系统会读取所有桶,并将其重新组装为原始 Map。由于需要读取和合并多个子stream,这会比 basic 序列化更慢。
使用 with_buckets 序列化时,Map 值中键的顺序可能与原始插入顺序不同。键会按哈希分布到各个桶中,并按桶顺序而非插入顺序重新组装。使用 basic 序列化时,会保留插入的 Map 中的键顺序。
不同 parts 的桶数量可能不同。当合并桶数量不同的 parts 时,新 part 的桶数量会根据合并后的统计信息重新计算。采用 basicwith_buckets 序列化的 parts 可以在同一张表中共存,并会被透明地合并。

设置

设置默认值说明
map_serialization_versionbasicMap 列的序列化格式。basic 将数据存储为单个数组 stream。with_buckets 会将键拆分到多个桶中,以加快单键读取。
map_serialization_version_for_zero_level_partsbasic零级 parts (由 INSERT 创建) 的序列化格式。可让插入操作继续使用 basic 以避免写入开销,同时让合并后的 parts 使用 with_buckets
max_buckets_in_map32桶数量的上限。实际数量取决于 map_buckets_strategy。允许的最大值为 256。
map_buckets_strategysqrt根据 map 平均大小计算桶数量的策略:constant — 始终使用 max_buckets_in_mapsqrt — 使用 round(coefficient * sqrt(avg_size))linear — 使用 round(coefficient * avg_size)。结果会被限制在 [1, max_buckets_in_map] 范围内。
map_buckets_coefficient1.0sqrtlinear 策略的乘数。策略为 constant 时会被忽略。
map_buckets_min_avg_size32启用分桶所需的每行平均最小键数。如果平均值低于该阈值,则无论其他设置如何,都会只使用一个桶。设为 0 可禁用该阈值。

性能权衡

下表汇总了在不同 Map 大小下 (每行 10 到 10,000 个键) ,with_buckets 相比 basic 序列化的性能影响。桶数由 sqrt 策略确定,最大不超过 32。实际数值取决于键/值类型、数据分布和硬件。
Operation10 keys100 keys1,000 keys10,000 keysNotes
单键 lookup (m['key'])快 1.6–3.2 倍快 4.5–7.7 倍快 16–39 倍快 21–49 倍只需读取一个桶,而不必读取整个列。
5 个键的 lookup~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。加速效果取决于选择性——匹配的粒度越少,完整 Map 的 I/O 就越少。
完整 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 个键) : 如果查询经常访问单个键,可使用采用 sqrt 策略的 with_buckets。对于单键 lookup,速度可提升 4–8 倍。
  • 大型 Map (100+ 个键) : 使用 with_buckets。单键 lookup 可快 16–49 倍。可考虑设置 map_serialization_version_for_zero_level_parts = 'basic',使 insert 速度接近基线水平。
  • 完整 Map 扫描在 workload 中占主导: 保持 basic。分桶序列化会使完整扫描的开销增加约 2 倍。
  • 混合 workload (部分键 lookup,部分完整扫描) : 使用 with_buckets,并将零级 parts 设为 basicPREWHERE 优化只会为过滤器读取相关的桶,然后仅对匹配的行读取完整 Map,从而带来显著的整体加速。

替代方案

如果分桶 Map 序列化不适用于你的使用场景,还可以采用另外两种方法来提升键级访问性能:

使用 JSON 数据类型

JSON 数据类型会将每个高频路径存储为独立的动态子列。超过 max_dynamic_paths 限制的路径会进入共享数据结构,该结构可使用 advanced 序列化来优化单一路径读取。有关 advanced 序列化的详细说明,请参阅这篇博客文章
方面带桶的 MapJSON
单键读取读取一个桶 (可能包含其他键) 。桶中的所有键值对都会被反序列化。高频路径可直接从动态子列读取。低频路径会进入共享数据;使用 advanced 序列化时,只会读取该精确路径的数据。
值类型所有值共享同一种类型 V每条路径都可以有自己的类型。没有类型提示的路径会使用 Dynamic
跳过索引支持适用于某些基于 mapKeys/mapValues 创建的索引类型跳过索引只能创建在特定路径的子列上,不能一次性对所有路径/值创建。
普通列读取由于需要重新组装桶,比 basic 慢约 ~2x存在 Dynamic 类型编码和路径重建带来的开销。
存储开销额外元数据极少更高,因为有 Dynamic 类型编码、路径名称存储,以及 advanced 序列化中的额外元数据。
schema 灵活性在创建表时固定键类型和值类型完全动态——键和值类型可因行而异。可为已知路径声明带类型的路径提示,以便直接通过子列访问。
当不同键需要不同的值类型、各行中的键集合差异很大,或者已知会频繁访问的键可提前声明为带类型的路径以便直接访问子列时,请使用 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 列都是独立列,可以进行垂直合并
schema 变更每个 parts 的桶数量都会自动适配更改分片数量需要重写数据或新增列
查询语法m['key'] 可直接使用必须计算正确的列:m0['key']m1['key']
桶粒度按 parts 划分,并根据数据统计信息自动适配在创建表时固定
当垂直合并对于减少多列表在合并期间的内存使用至关重要时,或者当分片数量必须固定并显式控制时,手动分片更有优势。对于大多数用例,自动分桶序列化更简单,也已足够。 另请参见
最后修改于 2026年6月10日