跳至主要内容

ClickHouse 中的压缩

ClickHouse 查询性能的秘诀之一是压缩。

磁盘上的数据越少,I/O 就越少,查询和插入速度就越快。任何压缩算法相对于 CPU 的开销在大多数情况下都会被 IO 的减少所抵消。因此,在确保 ClickHouse 查询速度快时,首先要关注的是提高数据的压缩率。

关于 ClickHouse 为什么能如此有效地压缩数据,我们推荐您阅读这篇文章。简而言之,作为列式数据库,值将按列顺序写入。如果这些值已排序,则相同的值将彼此相邻。压缩算法利用了连续的数据模式。最重要的是,ClickHouse 拥有编解码器和细粒度的的数据类型,允许用户进一步调整压缩技术。

ClickHouse 中的压缩将受到三个主要因素的影响

  • 排序键
  • 数据类型
  • 使用的编解码器

所有这些都通过模式进行配置。

选择合适的数据类型以优化压缩

让我们以 Stack Overflow 数据集为例。让我们比较以下 posts 表的模式的压缩统计信息

  • posts - 没有类型优化的模式,也没有排序键。
  • posts_v3 - 类型优化的模式,为每列使用适当的类型和比特大小,并使用排序键 (PostTypeId, toDate(CreationDate), CommentCount)

使用以下查询,我们可以测量每列当前的压缩和未压缩大小。让我们检查一下初始未排序模式 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
└───────────────────────┴─────────────────┴───────────────────┴─────────┘

我们在这里同时显示压缩和未压缩的大小。两者都很重要。压缩大小等同于我们需要从磁盘读取的内容 - 这是我们为了查询性能(和存储成本)而想要最小化的内容。在读取之前需要对这些数据进行解压缩。此未压缩大小将取决于在此案例中使用的数据类型。最小化此大小将减少查询的内存开销以及必须由查询处理的数据量,从而提高缓存利用率并最终提高查询时间。

上述查询依赖于系统数据库中的 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
└─────────────────┴───────────────────┴───────┘

完整的列细分显示了 BodyTitleTagsCreationDate 列获得了相当大的节省,这是通过在压缩前对数据进行排序并使用适当的类型实现的。

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 的一级导数已经很小,这通常不会增加太多)。序列中单调增量一致的地方,压缩效果会更好,例如日期时间字段。
  • Delta 改善了 ZSTD - ZSTD 是对增量数据有效的编解码器 - 相反,增量编码可以改善 ZSTD 压缩。在存在 ZSTD 的情况下,其他编解码器很少能提供进一步的改进。
  • 如果可能,优先选择 LZ4 而不是 ZSTD - 如果您在 LZ4ZSTD 之间获得可比的压缩率,则优先选择前者,因为它提供了更快的解压缩速度并且需要更少的 CPU。但是,在大多数情况下,ZSTD 的性能将大大优于 LZ4。其中一些编解码器可能与 LZ4 结合使用时速度更快,同时提供与不带编解码器的 ZSTD 相似的压缩率。但是,这将是特定于数据的,需要进行测试。
  • 对于稀疏或小范围使用 T64 - T64 在稀疏数据或块中的范围较小时可以有效。避免对随机数使用 T64
  • 对于未知模式,可以使用 GorillaT64 - 如果数据具有未知模式,则可能值得尝试 GorillaT64
  • 对于量规数据使用 Gorilla - Gorilla 可以有效地处理浮点数据,特别是表示量规读数的数据,即随机峰值

请参阅此处以获取更多选项。

在下面,我们为 IdViewCountAnswerCount 指定了 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 与编解码器的组合。它对大多数数据类型和信息分布都有效,因此是一个合理的通用默认值,也是为什么我们最初的早期压缩即使没有优化也已经非常出色的原因。