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 值的类型。可以是任意类型。
Query
key2 对应的值:
Query
Response
k 不在 Map 中,m[k] 会返回该值类型的默认值,例如整数类型返回 0,字符串类型返回 ''。
要检查 Map 中是否存在某个键,可以使用函数 mapContains。
Query
Response
将 Tuple 转换为 Map
Tuple() 类型的值可以使用函数 CAST 转换成 Map() 类型的值:
示例
Query
Response
读取 Map 的子列
keys 和 values。
示例
Query
Response
MergeTree 中的分桶 Map 序列化
Map 列存储为单个 Array(Tuple(K, V)) stream。
使用 m['key'] 读取某个键时,需要扫描整列——每一行中的每一个键值对——即使只需要其中一个键也是如此。
对于包含大量不同键的 Map,这会成为性能瓶颈。
分桶序列化 (with_buckets) 会通过对键进行哈希,将键值对拆分到多个彼此独立的子stream (桶) 中。
当查询访问 m['key'] 时,只会从磁盘读取包含该键的那个桶,跳过其他所有桶。
启用分桶序列化
INSERT 期间创建) 保留 basic 序列化,仅对已合并的 parts 使用 with_buckets:
工作原理
with_buckets 序列化方式写入数据分区片段时:
- 根据块统计信息计算每行的平均键数。
- 根据已配置的策略确定桶的数量 (参见 设置) 。
- 通过对键进行哈希,为每个键值对分配桶:
bucket = hash(key) % num_buckets。 - 每个桶都会作为独立子stream存储,并拥有各自的键、值和偏移量。
buckets_info元数据stream会记录桶数量及相关统计信息。
m['key']) 时,优化器会将该表达式重写为键子列 (m.key_<serialized_key>) 。
序列化层会计算所请求的键属于哪个桶,并且只从磁盘读取这一个桶。
当读取完整的 Map 时 (例如 SELECT m) ,系统会读取所有桶,并将其重新组装为原始 Map。由于需要读取和合并多个子stream,这会比 basic 序列化更慢。
使用
with_buckets 序列化时,Map 值中键的顺序可能与原始插入顺序不同。键会按哈希分布到各个桶中,并按桶顺序而非插入顺序重新组装。使用 basic 序列化时,会保留插入的 Map 中的键顺序。basic 和 with_buckets 序列化的 parts 可以在同一张表中共存,并会被透明地合并。
设置
性能权衡
with_buckets 相比 basic 序列化的性能影响。桶数由 sqrt 策略确定,最大不超过 32。实际数值取决于键/值类型、数据分布和硬件。
建议
- 小型 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 设为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 组合器