跳至主要内容
跳至主要内容

ClickHouse 中的压缩

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

磁盘上的数据越少,I/O 越少,查询和插入速度就越快。任何压缩算法相对于 CPU 的开销通常都小于 I/O 的减少。因此,在努力确保 ClickHouse 查询速度快时,首先要关注的是提高数据的压缩率。

关于为什么 ClickHouse 能够很好地压缩数据,我们建议阅读 这篇文章。 简而言之,我们的列式数据库以列的顺序写入值。 当这些值排序时,相同的值彼此相邻,压缩算法会利用数据中的连续模式。在此基础上,ClickHouse 具有编解码器和细粒度数据类型,可让您轻松进一步调整压缩。

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

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

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

选择正确的数据类型以优化压缩

让我们以 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     │
└───────────────────────┴─────────────────┴───────────────────┴────────────┘
关于紧凑型与宽型分区

如果您看到 compressed_sizeuncompressed_size 值等于 0,这可能是因为分区的类型是 compact 而不是 wide(请参阅 system.partspart_type 的描述)。 分区格式由设置 min_bytes_for_wide_partmin_rows_for_wide_part 控制,这意味着如果插入的数据导致的分区不超出上述设置的值,则该分区将是紧凑型而不是宽型,您将看不到 compressed_sizeuncompressed_size 的值。

为了演示

-- Create a table with compact parts
CREATE TABLE compact (
  number UInt32
)
ENGINE = MergeTree()
ORDER BY number 
AS SELECT * FROM numbers(100000); -- Not big enough to exceed default of min_bytes_for_wide_part = 10485760

-- Check the type of the parts
SELECT table, name, part_type from system.parts where table = 'compact';

-- Get the compressed and uncompressed column sizes for the compact table
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 = 'compact'
GROUP BY name;

-- Create a table with wide parts 
CREATE TABLE wide (
  number UInt32
)
ENGINE = MergeTree()
ORDER BY number
SETTINGS min_bytes_for_wide_part=0
AS SELECT * FROM numbers(100000);

-- Check the type of the parts
SELECT table, name, part_type from system.parts where table = 'wide';

-- Get the compressed and uncompressed sizes for the wide table
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 = 'wide'
GROUP BY name;
   ┌─table───┬─name──────┬─part_type─┐
1. │ compact │ all_1_1_0 │ Compact   │
   └─────────┴───────────┴───────────┘
   ┌─name───┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
1. │ number │ 0.00 B          │ 0.00 B            │   nan │
   └────────┴─────────────────┴───────────────────┴───────┘
   ┌─table─┬─name──────┬─part_type─┐
1. │ wide  │ all_1_1_0 │ Wide      │
   └───────┴───────────┴───────────┘
   ┌─name───┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
1. │ number │ 392.31 KiB      │ 390.63 KiB        │     1 │
   └────────┴─────────────────┴───────────────────┴───────┘

我们在这里显示压缩和未压缩的大小。 两者都很重要。 压缩大小等于我们需要从磁盘读取的内容 - 我们希望最大限度地减少查询性能(和存储成本)。 此数据在读取之前需要解压缩。 此未压缩大小将取决于在这种情况下使用的数据类型。 最小化此大小将减少查询的内存开销以及必须由查询处理的数据量,从而提高缓存利用率并最终提高查询时间。

上述查询依赖于系统数据库中的 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 支持大量的编解码器和压缩算法。 以下是一些按重要性顺序排列的建议

建议推理
始终使用 ZSTDZSTD 压缩提供最佳的压缩率。 ZSTD(1) 应该是大多数常见类型的默认值。 可以通过修改数值来尝试更高的压缩率。 我们很少看到在增加压缩成本(插入速度较慢)的情况下,高于 3 的值有足够的优势。
对于日期和整数序列使用 DeltaDelta 基于的编解码器在您拥有单调序列或连续值中的小增量时效果很好。 更具体地说,Delta 编解码器效果很好,前提是导数产生小数字。 如果没有,值得尝试 DoubleDelta(这通常在第一级导数来自 Delta 已经很小的情况下增加很少)。 单调增量均匀的序列,将更好地压缩,例如 DateTime 字段。
Delta 改进 ZSTDZSTD 是 delta 数据的有效编解码器 - 相反,delta 编码可以提高 ZSTD 压缩。 在 ZSTD 存在的情况下,其他编解码器很少提供进一步的改进。
如果可能,使用 LZ4 代替 ZSTD如果您在 LZ4ZSTD 之间获得可比的压缩,则应选择前者,因为它提供更快的解压缩速度并且需要的 CPU 更少。 但是,在大多数情况下,ZSTD 将优于 LZ4。 其中一些编解码器可能与 LZ4 结合使用更快,同时提供与 ZSTD 相比类似的压缩。 但是,这取决于数据,需要进行测试。
对于稀疏或小范围使用 T64T64 可以有效地用于稀疏数据或当块中的范围很小时。 避免对随机数使用 T64
对于未知模式使用 GorillaT64如果数据具有未知模式,则值得尝试 GorillaT64
对于仪表数据使用 GorillaGorilla 可以有效地用于浮点数据,特别是代表仪表读数的数据,即随机峰值。

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

下面,我们指定了 IdViewCountAnswerCountDelta 编解码器,假设这些编解码器将与排序键线性相关,因此应该受益于 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 与编解码器结合使用。 它对大多数数据类型和信息分布有效,因此是一个明智的通用默认值,也是为什么即使没有优化,我们最初的压缩已经非常出色。

    © . This site is unofficial and not affiliated with ClickHouse, Inc.