ClickHouse 中的压缩
ClickHouse 查询性能的秘诀之一是压缩。
磁盘上更少的数据意味着更少的 I/O 以及更快的查询和插入。任何压缩算法相对于 CPU 的开销在大多数情况下都会被 I/O 的减少所抵消。因此,在致力于确保 ClickHouse 查询快速时,改进数据压缩应该是首要关注点。
关于 ClickHouse 为何能如此出色地压缩数据,我们推荐这篇文章。 总结来说,作为面向列的数据库,值将按列顺序写入。 如果这些值已排序,则相同的值将彼此相邻。 压缩算法利用数据的连续模式。 最重要的是,ClickHouse 具有编解码器和细粒度数据类型,允许用户进一步调整压缩技术。
ClickHouse 中的压缩将受到 3 个主要因素的影响
- 排序键
- 数据类型
- 使用的编解码器
所有这些都通过 schema 配置。
选择正确的数据类型以优化压缩
让我们使用 Stack Overflow 数据集作为示例。 让我们比较 posts
表以下 schema 的压缩统计信息
posts
- 一个非类型优化的 schema,没有排序键。posts_v3
- 一个类型优化的 schema,每个列都具有适当的类型和位大小,排序键为(PostTypeId, toDate(CreationDate), CommentCount)
。
使用以下查询,我们可以测量每个列的当前压缩和未压缩大小。 让我们检查初始优化的 schema posts
的大小,它没有排序键。
SELECT name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table = 'posts'
GROUP BY name
┌─name──────────────────┬─compressed_size─┬─uncompressed_size─┬───ratio─┐
│ Body │ 46.14 GiB │ 127.31 GiB │ 2.76 │
│ Title │ 1.20 GiB │ 2.63 GiB │ 2.19 │
│ Score │ 84.77 MiB │ 736.45 MiB │ 8.69 │
│ Tags │ 475.56 MiB │ 1.40 GiB │ 3.02 │
│ ParentId │ 210.91 MiB │ 696.20 MiB │ 3.3 │
│ Id │ 111.17 MiB │ 736.45 MiB │ 6.62 │
│ AcceptedAnswerId │ 81.55 MiB │ 736.45 MiB │ 9.03 │
│ ClosedDate │ 13.99 MiB │ 517.82 MiB │ 37.02 │
│ LastActivityDate │ 489.84 MiB │ 964.64 MiB │ 1.97 │
│ CommentCount │ 37.62 MiB │ 565.30 MiB │ 15.03 │
│ OwnerUserId │ 368.98 MiB │ 736.45 MiB │ 2 │
│ AnswerCount │ 21.82 MiB │ 622.35 MiB │ 28.53 │
│ FavoriteCount │ 280.95 KiB │ 508.40 MiB │ 1853.02 │
│ ViewCount │ 95.77 MiB │ 736.45 MiB │ 7.69 │
│ LastEditorUserId │ 179.47 MiB │ 736.45 MiB │ 4.1 │
│ ContentLicense │ 5.45 MiB │ 847.92 MiB │ 155.5 │
│ OwnerDisplayName │ 14.30 MiB │ 142.58 MiB │ 9.97 │
│ PostTypeId │ 20.93 MiB │ 565.30 MiB │ 27 │
│ CreationDate │ 314.17 MiB │ 964.64 MiB │ 3.07 │
│ LastEditDate │ 346.32 MiB │ 964.64 MiB │ 2.79 │
│ LastEditorDisplayName │ 5.46 MiB │ 124.25 MiB │ 22.75 │
│ CommunityOwnedDate │ 2.21 MiB │ 509.60 MiB │ 230.94 │
└───────────────────────┴─────────────────┴───────────────────┴─────────┘
我们在此处显示压缩大小和未压缩大小。 两者都很重要。 压缩大小等同于我们需要从磁盘读取的内容 - 这是我们希望最小化的以提高查询性能(和存储成本)的内容。 此数据需要在读取之前解压缩。 此未压缩大小的大小将取决于本例中使用的数据类型。 最小化此大小将减少查询的内存开销以及查询必须处理的数据量,从而提高缓存的利用率并最终缩短查询时间。
上述查询依赖于 system 数据库中的表
columns
。 此数据库由 ClickHouse 管理,是一个有用的信息宝库,从查询性能指标到后台集群日志。 我们为好奇的读者推荐《系统表和 ClickHouse 内部原理的窗口》以及随附的文章[1][2]。
为了总结表的总大小,我们可以简化上面的查询
SELECT formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table = 'posts'
┌─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ 50.16 GiB │ 143.47 GiB │ 2.86 │
└─────────────────┴───────────────────┴───────┘
对 posts_v3
(具有优化类型和排序键的表)重复此查询,我们可以看到未压缩和压缩大小的显着减少。
SELECT
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE `table` = 'posts_v3'
┌─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ 25.15 GiB │ 68.87 GiB │ 2.74 │
└─────────────────┴───────────────────┴───────┘
完整的列细分显示,通过在压缩之前对数据进行排序并使用适当的类型,Body、Title、Tags 和 CreationDate 列节省了大量空间。
SELECT
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE `table` = 'posts_v3'
GROUP BY name
┌─name──────────────────┬─compressed_size─┬─uncompressed_size─┬───ratio─┐
│ Body │ 23.10 GiB │ 63.63 GiB │ 2.75 │
│ Title │ 614.65 MiB │ 1.28 GiB │ 2.14 │
│ Score │ 40.28 MiB │ 227.38 MiB │ 5.65 │
│ Tags │ 234.05 MiB │ 688.49 MiB │ 2.94 │
│ ParentId │ 107.78 MiB │ 321.33 MiB │ 2.98 │
│ Id │ 159.70 MiB │ 227.38 MiB │ 1.42 │
│ AcceptedAnswerId │ 40.34 MiB │ 227.38 MiB │ 5.64 │
│ ClosedDate │ 5.93 MiB │ 9.49 MiB │ 1.6 │
│ LastActivityDate │ 246.55 MiB │ 454.76 MiB │ 1.84 │
│ CommentCount │ 635.78 KiB │ 56.84 MiB │ 91.55 │
│ OwnerUserId │ 183.86 MiB │ 227.38 MiB │ 1.24 │
│ AnswerCount │ 9.67 MiB │ 113.69 MiB │ 11.76 │
│ FavoriteCount │ 19.77 KiB │ 147.32 KiB │ 7.45 │
│ ViewCount │ 45.04 MiB │ 227.38 MiB │ 5.05 │
│ LastEditorUserId │ 86.25 MiB │ 227.38 MiB │ 2.64 │
│ ContentLicense │ 2.17 MiB │ 57.10 MiB │ 26.37 │
│ OwnerDisplayName │ 5.95 MiB │ 16.19 MiB │ 2.72 │
│ PostTypeId │ 39.49 KiB │ 56.84 MiB │ 1474.01 │
│ CreationDate │ 181.23 MiB │ 454.76 MiB │ 2.51 │
│ LastEditDate │ 134.07 MiB │ 454.76 MiB │ 3.39 │
│ LastEditorDisplayName │ 2.15 MiB │ 6.25 MiB │ 2.91 │
│ CommunityOwnedDate │ 824.60 KiB │ 1.34 MiB │ 1.66 │
└───────────────────────┴─────────────────┴───────────────────┴─────────┘
选择正确的列压缩编解码器
使用列压缩编解码器,我们可以更改用于编码和压缩每个列的算法(及其设置)。
编码和压缩的工作方式略有不同,但目标相同:减少我们的数据大小。 编码将映射应用于我们的数据,通过利用数据类型的属性,根据函数转换值。 相反,压缩使用通用算法在字节级别压缩数据。
通常,先应用编码,然后再使用压缩。 由于不同的编码和压缩算法在不同的值分布上有效,因此我们必须了解我们的数据。
ClickHouse 支持大量的编解码器和压缩算法。 以下是一些按重要性排序的建议
建议 | 原因 |
---|---|
始终使用 ZSTD | ZSTD 压缩提供最佳压缩率。 ZSTD(1) 应该是大多数常见类型的默认设置。 可以通过修改数值来尝试更高的压缩率。 对于高于 3 的值,我们很少看到足够的收益来抵消压缩成本的增加(插入速度较慢)。 |
用于日期和整数序列的 Delta | 当您具有单调序列或连续值中的小增量时,基于 Delta 的编解码器效果良好。 更具体地说,Delta 编解码器效果良好,前提是导数产生小数字。 如果不是,则值得尝试 DoubleDelta (如果来自 Delta 的一级导数已经很小,这通常几乎没有增加)。 单调增量均匀的序列将压缩得更好,例如 DateTime 字段。 |
Delta 改进 ZSTD | ZSTD 是 delta 数据上有效的编解码器 - 反之,delta 编码可以改进 ZSTD 压缩。 在存在 ZSTD 的情况下,其他编解码器很少能提供进一步的改进。 |
如果可能,使用 LZ4 而不是 ZSTD | 如果您在 LZ4 和 ZSTD 之间获得相当的压缩率,则优先选择前者,因为它提供更快的解压缩并且需要更少的 CPU。 然而,在大多数情况下,ZSTD 将大大优于 LZ4。 其中一些编解码器与 LZ4 结合使用时可能会更快,同时与不使用编解码器的 ZSTD 相比提供相似的压缩率。 然而,这将是特定于数据的,并且需要测试。 |
用于稀疏或小范围的 T64 | T64 在稀疏数据或块中的范围较小时可能有效。 避免对随机数使用 T64 。 |
Gorilla 和 T64 用于未知模式? | 如果数据具有未知模式,则值得尝试 Gorilla 和 T64 。 |
Gorilla 用于仪表数据 | Gorilla 在浮点数据上可能有效,特别是代表仪表读数的数据,即随机峰值。 |
有关更多选项,请参见此处。
下面我们为 Id
、ViewCount
和 AnswerCount
指定 Delta 编解码器,假设这些将与排序键线性相关,因此应该受益于 Delta 编码。
CREATE TABLE posts_v4
(
`Id` Int32 CODEC(Delta, ZSTD),
`PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
`AcceptedAnswerId` UInt32,
`CreationDate` DateTime64(3, 'UTC'),
`Score` Int32,
`ViewCount` UInt32 CODEC(Delta, ZSTD),
`Body` String,
`OwnerUserId` Int32,
`OwnerDisplayName` String,
`LastEditorUserId` Int32,
`LastEditorDisplayName` String,
`LastEditDate` DateTime64(3, 'UTC'),
`LastActivityDate` DateTime64(3, 'UTC'),
`Title` String,
`Tags` String,
`AnswerCount` UInt16 CODEC(Delta, ZSTD),
`CommentCount` UInt8,
`FavoriteCount` UInt8,
`ContentLicense` LowCardinality(String),
`ParentId` String,
`CommunityOwnedDate` DateTime64(3, 'UTC'),
`ClosedDate` DateTime64(3, 'UTC')
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CommentCount)
这些列的压缩改进如下所示
SELECT
`table`,
name,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE (name IN ('Id', 'ViewCount', 'AnswerCount')) AND (`table` IN ('posts_v3', 'posts_v4'))
GROUP BY
`table`,
name
ORDER BY
name ASC,
`table` ASC
┌─table────┬─name────────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ posts_v3 │ AnswerCount │ 9.67 MiB │ 113.69 MiB │ 11.76 │
│ posts_v4 │ AnswerCount │ 10.39 MiB │ 111.31 MiB │ 10.71 │
│ posts_v3 │ Id │ 159.70 MiB │ 227.38 MiB │ 1.42 │
│ posts_v4 │ Id │ 64.91 MiB │ 222.63 MiB │ 3.43 │
│ posts_v3 │ ViewCount │ 45.04 MiB │ 227.38 MiB │ 5.05 │
│ posts_v4 │ ViewCount │ 52.72 MiB │ 222.63 MiB │ 4.22 │
└──────────┴─────────────┴─────────────────┴───────────────────┴───────┘
6 rows in set. Elapsed: 0.008 sec
ClickHouse Cloud 中的压缩
在 ClickHouse Cloud 中,我们默认使用 ZSTD 压缩算法(默认值为 1)。 虽然此算法的压缩速度可能会因压缩级别(越高 = 越慢)而异,但它具有解压缩速度始终如一(约 20% 的差异)的优势,并且还受益于并行化的能力。 我们的历史测试还表明,该算法通常非常有效,甚至可以胜过 LZ4 与编解码器的组合。 它对大多数数据类型和信息分布都有效,因此是一个明智的通用默认设置,也是为什么我们最初的早期压缩即使没有优化也已经非常出色的原因。