在ClickHouse中,String字符串类型相比其他数据类型而言,一个显著的差异是String类型的大小是不固定的。所以除了常规的列字段压缩手段之外,还延伸出了一些额外的优化思路。
在《ClickHouse原理解析与应用实践》(你没看错,这是最终敲定的书名)这本书的数据定义章节中,曾提过在一些场合可以使用Enum枚举类型代替String字符串,从而将其转换为长度固定、字节更小的数值类型,这样在存储开销、读取效率、分组、排序、去重等操作时都会得到优化。
其实本质上,这就是一种对低基数特征字段的优化思路,只不过枚举类型的使用场景比较苛刻,它要求这些数据预先可知,且能够穷举。那么对于不可预知、无法穷举的数据应该怎么优化呢?
于是,ClickHouse提供了一种修饰数据类型LowCardinality,专门针对低基数特征的字段进行优化。
虽然LowCardinality的初衷是为了优化String,但是一不做二不休,LowCardinality目前还可以支持Int、Date和DateTime类型。
(主要在String场景使用,优化效果更明显)
LowCardinality和Nullable类似,是一种修饰类型,需要和其他数据类型组合使用,例如:
LowCardinality(String)
LowCardinality(UInt32)
LowCardinality(Date)
它们也有相应的简写形式,例如:
StringWithDictionary 等同于 LowCardinality(String)
UInt32WithDictionary 等同于 LowCardinality(UInt32)
DateWithDictionary 等同于 LowCardinality(Date)
如果需要使用String以外的LowCardinality类型,需要设置
SET allow_suspicious_low_cardinality_types = 1
接下来用一个示例说明,新增一张数据表:
CREATE TABLE test2(
id UInt32,
v1 String,
v2 StringWithDictionary
)ENGINE = MergeTree()
ORDER BY id;
其中v1是普通的String类型,v2是经过优化的String类型,之后会用它们来进行比较。
查看表结构,可以看到StringWithDictionary本质是语法糖,最终字段类型还是LowCardinality的形式:
ch7.nauu.com :) desc test2;
DESCRIBE TABLE test2
┌─name─┬─type───────────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ id │ UInt32 │ │ │ │ │ │
│ v1 │ String │ │ │ │ │ │
│ v2 │ LowCardinality(String) │ │ │ │ │ │
└──────┴────────────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
3 rows in set. Elapsed: 0.007 sec.
写入3亿行测试数据:
INSERT INTO test2 WITH
(
SELECT ['A','B','C','D']
) AS dict
SELECT number, dict[number%4+1] AS v1, v1 FROM system.numbers LIMIT 300000000
现在我们来分析一下LowCardinality有何其妙之处。
第一个最直观的感受是压缩率更高了,从下面结果可知,在这份数据下v2字段的压缩率提高了一倍:
SELECT
column,
any(type) AS type,
sum(column_data_compressed_bytes) AS compressed,
sum(column_data_uncompressed_bytes) AS uncompressed,
sum(rows) AS rowsFROM system.parts_columns
WHERE (table = 'test2') AND (column LIKE 'v%')
GROUP BY columnORDER BY column ASC
┌─column─┬─type───────────────────┬─compressed─┬─uncompressed─┬──────rows─┐
│ v1 │ String │ 2737458 │ 600000000 │ 300000000 │
│ v2 │ LowCardinality(String) │ 1433440 │ 300593127 │ 300000000 │
└────────┴────────────────────────┴────────────┴──────────────┴───────────┘
2 rows in set. Elapsed: 0.006 sec.
第二个直观感受是查询变快了,查询普通String:
ch7.nauu.com :) SELECT v1, count() FROM test2 GROUP BY v1 ORDER BY v1;
SELECT
v1,
count()
FROM test2
GROUP BY v1
ORDER BY v1 ASC
┌─v1─┬──count()─┐
│ A │ 75000000 │
│ B │ 75000000 │
│ C │ 75000000 │
│ D │ 75000000 │
└────┴──────────┘
4 rows in set. Elapsed: 1.951 sec. Processed 300.00 million rows, 3.00 GB (153.74 million rows/s., 1.54 GB/s.)
查询 LowCardinality:
ch7.nauu.com :) SELECT v2, count() FROM test2 GROUP BY v2 ORDER BY v2;
SELECT
v2,
count()
FROM test2
GROUP BY v2
ORDER BY v2 ASC
┌─v2─┬──count()─┐
│ A │ 75000000 │
│ B │ 75000000 │
│ C │ 75000000 │
│ D │ 75000000 │
└────┴──────────┘
4 rows in set. Elapsed: 0.746 sec. Processed 300.00 million rows, 300.22 MB (402.23 million rows/s., 402.53 MB/s.)
查询耗时也缩短了一倍时间。
那么LowCardinality背后的原理是什么呢? 其实从StringWithDictionary的名字已经很明显了,它是通过字典压缩编码进行优化的。
在默认的情况下,声明了LowCardinality的字段会基于数据生成一个全局字典,并利用倒排索引建立Key和位置的对应关系。如果数据的基数大于 8192,也就是说不同的值多于8192个,则会将一个全局字典拆分成多个局部字典(由 low_cardinality_max_dictionary_size 参数控制, 默认8192)。
因为进一步使用了字典压缩,所以查询的IO压力变小了,这是一处优化; 其次在处理数据的某些场合,可以直接使用字典进行操作,不需要将数据全部展开。
由于字典压缩和数据特征息息相关,所以这项特性的最终受益效果,需要在大家各自的环境中进行验证。通常来说,在百万级别基数的数据下,使用LowCardinality的收益效果都是不错的。
本文分享自 ClickHouse的秘密基地 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!