Map(K, V) stores key-value pairs.
Unlike other databases, maps are not unique in ClickHouse, i.e. a map can contain two elements with the same key.
(The reason for that is that maps are internally implemented as Array(Tuple(K, V)).)
You can use syntax m[k] to obtain the value for key k in map m.
Also, m[k] scans the map, i.e. the runtime of the operation is linear in the size of the map.
Parameters
K— The type of the Map keys. Arbitrary type except Nullable and LowCardinality nested with Nullable types.V— The type of the Map values. Arbitrary type.
Query
key2 values:
Query
Response
k is not contained in the map, m[k] returns the value type’s default value, e.g. 0 for integer types and '' for string types.
To check whether a key exists in a map, you can use function mapContains.
Query
Response
Converting Tuple to Map
Values of typeTuple() can be cast to values of type Map() using function CAST:
Example
Query
Response
Reading subcolumns of Map
To avoid reading the entire map, you can use subcolumnskeys and values in some cases.
Example
Query
Response
Bucketed Map Serialization in MergeTree
By default, aMap column in MergeTree is stored as a single Array(Tuple(K, V)) stream.
Reading a single key with m['key'] requires scanning the entire column — every key-value pair for every row — even if only one key is needed.
For maps with many distinct keys this becomes a bottleneck.
Bucketed serialization (with_buckets) splits the key-value pairs into multiple independent substreams (buckets) by hashing the key.
When a query accesses m['key'], only the bucket that contains that key is read from disk, skipping all other buckets.
Enabling Bucketed Serialization
basic serialization for zero-level parts (created during INSERT) and only use with_buckets for merged parts:
How It Works
When a data part is written withwith_buckets serialization:
- The average number of keys per row is computed from the block statistics.
- The number of buckets is determined by the configured strategy (see Settings).
- Each key-value pair is assigned to a bucket by hashing the key:
bucket = hash(key) % num_buckets. - Each bucket is stored as an independent substream with its own keys, values, and offsets.
- A
buckets_infometadata stream records the bucket count and statistics.
m['key']), the optimizer rewrites the expression to a key subcolumn (m.key_<serialized_key>).
The serialization layer computes which bucket the requested key belongs to and reads only that single bucket from disk.
When the full map is read (e.g., SELECT m), all buckets are read and reassembled into the original map. This is slower than basic serialization due to the overhead of reading and merging multiple substreams.
The order of keys within a map value may differ from the original insertion order when using
with_buckets serialization. Keys are distributed across buckets by hash and are reassembled in bucket order, not insertion order. With basic serialization, the key order from inserted maps is preserved.basic and with_buckets serialization can coexist in the same table and are merged transparently.
Settings
Performance Trade-offs
The following table summarizes the performance impact ofwith_buckets compared to basic serialization at various map sizes (10 to 10,000 keys per row). The bucket count was determined by the sqrt strategy capped at 32. The exact numbers depend on key/value types, data distribution, and hardware.
Recommendations
- Small maps (< 32 keys on average): Keep
basicserialization. The overhead of bucketing is not justified for small maps. The defaultmap_buckets_min_avg_size = 32enforces this automatically. - Medium maps (32–100 keys): Use
with_bucketswithsqrtstrategy if queries frequently access individual keys. The speedup is 4–8x for single-key lookups. - Large maps (100+ keys): Use
with_buckets. Single-key lookups are 16–49x faster. Considermap_serialization_version_for_zero_level_parts = 'basic'to keep insert speed close to the baseline. - Full map scans dominate the workload: Keep
basic. Bucketed serialization adds ~2x overhead for full scans. - Mixed workload (some key lookups, some full scans): Use
with_bucketswith zero-level parts set tobasic. ThePREWHEREoptimization reads only the relevant bucket for the filter, then reads the full map only for matching rows, giving a significant net speedup.
Alternative Approaches
If bucketedMap serialization does not fit your use case, there are two alternative approaches for improving key-level access performance:
Using the JSON Data Type
The JSON data type stores each frequent path as a separate dynamic subcolumn. Paths that exceed themax_dynamic_paths limit go into a shared data structure, which can use advanced serialization for optimized single-path reads. See the blog post for a detailed overview of the advanced serialization.
Use
JSON when different keys need different value types, when the set of keys varies significantly across rows, or when frequently accessed keys are known in advance and can be declared as typed paths for direct subcolumn access.
Manual Sharding into Multiple Map Columns
You can manually split a singleMap into multiple columns by key hash at the application level:
m{hash(key) % 4}. During queries, read from the specific column: m{hash('target_key') % 4}['target_key'].
Manual sharding is beneficial when vertical merges are important for reducing memory usage during merges of tables with many columns, or when the number of shards must be fixed and controlled explicitly. For most use cases, automatic bucketed serialization is simpler and sufficient.
See Also
- map() function
- CAST() function
- -Map combinator for Map datatype