欢迎来到我们 2024 年的第一个版本!ClickHouse 24.1 版本包含 **26 个新功能** 🎁 **22 个性能优化** 🛷 **47 个错误修复** 🐛
像往常一样,我们将在本博文中重点介绍新功能和改进的一小部分,但该版本还包括生成分片的功, Punycode 函数,来自 Datadog 的分位数草图,写入文件时的压缩控制,加快 HTTP 输出和并行副本的速度,以及对 keeper 和合并的内存优化。
在集成方面,我们也很高兴地宣布 ClickHouse Grafana 插件 v4 的正式发布,它在使用 ClickHouse 用于可观察性用例方面进行了重大改进。
新贡献者
与往常一样,我们向所有 24.1 中的新贡献者表示热烈的欢迎!ClickHouse 的流行在很大程度上要归功于社区的贡献。看到社区不断壮大总是让人感到谦卑。
以下是新贡献者的姓名
Aliaksei Khatskevich、Artem Alperin、Blacksmith、Blargian、Eyal Halpern Shalev、Jayme Bird、Lino Uruñuela、Maksim Alekseev、Mark Needham、Mathieu Rey、MochiXu、Nikolay Edigaryev、Roman Glinskikh、Shaun Struwig、Tim Liou、Waterkin、Zheng Miao、avinzhang、chenwei、edpyt、mochi 和 sunny19930321。
提示:如果您好奇我们是如何生成此列表的……点击此处.
如果您在这里看到您的姓名,请与我们联系……但我们也会在 Twitter 等平台上找到您。
您还可以查看演示文稿的幻灯片.
变体类型
由 Pavel Kruglov 贡献
此版本引入了变体类型,尽管它仍处于实验阶段,因此您需要配置以下设置才能使其工作。
SET allow_experimental_variant_type=1,
use_variant_as_common_type = 1;
变体类型是向 ClickHouse 添加半结构化列的长期项目的一部分。此类型是嵌套列的判别联合。例如,Variant(Int8, Array(String))
将每个值都作为 Int8
或 Array(String)
。
在处理映射时,这种新类型将非常有用。例如,假设我们要创建一个映射,该映射具有不同类型的 value:此版本引入了变体类型,尽管它仍处于实验阶段,因此您需要配置以下设置才能使其工作。
SELECT
map('Hello', 1, 'World', 'Mark') AS x,
toTypeName(x) AS type
FORMAT Vertical;
这通常会导致异常
Received exception:
Code: 386. DB::Exception: There is no supertype for types UInt8, String because some of them are String/FixedString and some of them are not: While processing map('Hello', 1, 'World', 'Mark') AS x, toTypeName(x) AS type. (NO_COMMON_TYPE)
而现在它返回一个变体类型
Row 1:
──────
x: {'Hello':1,'World':'Mark'}
type: Map(String, Variant(String, UInt8))
我们还可以在从 CSV 文件读取时使用这种类型。例如,假设我们有以下具有混合类型的文件
$ cat foo.csv
value
1
"Mark"
2.3
处理文件时,我们可以添加一个模式推断提示,使其使用变体类型
SELECT *, * APPLY toTypeName
FROM file('foo.csv', CSVWithNames)
SETTINGS
schema_inference_make_columns_nullable = 0,
schema_inference_hints = 'value Variant(Int, Float32, String)'
┌─value─┬─toTypeName(value)───────────────┐
│ 1 │ Variant(Float32, Int32, String) │
│ Mark │ Variant(Float32, Int32, String) │
│ 2.3 │ Variant(Float32, Int32, String) │
└───────┴─────────────────────────────────┘
目前,它不适用于文字数组,因此以下操作会导致异常
SELECT
arrayJoin([1, true, 3.4, 'Mark']) AS value,
toTypeName(value)
Received exception:
Code: 386. DB::Exception: There is no supertype for types UInt8, Bool, Float64, String because some of them are String/FixedString and some of them are not: While processing arrayJoin([1, true, 3.4, 'Mark']) AS value, toTypeName(value). (NO_COMMON_TYPE)
但您可以改为使用 array 函数,并将使用变体类型
select arrayJoin(array(1, true, 3.4, 'Mark')) AS value, toTypeName(value);
┌─value─┬─toTypeName(arrayJoin([1, true, 3.4, 'Mark']))─┐
│ 1 │ Variant(Bool, Float64, String, UInt8) │
│ true │ Variant(Bool, Float64, String, UInt8) │
│ 3.4 │ Variant(Bool, Float64, String, UInt8) │
│ Mark │ Variant(Bool, Float64, String, UInt8) │
└───────┴───────────────────────────────────────────────┘
我们还可以按类型从 Variant 对象读取单个值
SELECT
arrayJoin([1, true, 3.4, 'Mark']) AS value,
variantElement(value, 'Bool') AS bool,
variantElement(value, 'UInt8') AS int,
variantElement(value, 'Float64') AS float,
variantElement(value, 'String') AS str;
┌─value─┬─bool─┬──int─┬─float─┬─str──┐
│ 1 │ ᴺᵁᴸᴸ │ 1 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │
│ true │ true │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │
│ 3.4 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ 3.4 │ ᴺᵁᴸᴸ │
│ Mark │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Mark │
└───────┴──────┴──────┴───────┴──────┘
字符串相似度函数
由 prashantr36 和 Robert Schulze 贡献
ClickHouse 的新用户在尝试使用 LIKE 运算符和匹配运算符时,通常会对它的性能感到惊讶。根据要匹配的表达式,这可以映射到正则表达式,或者使用对不太知名的Volnitsky 的字符串搜索算法的有效实现来执行子字符串搜索。ClickHouse 还利用主键和跳过索引来尽力加速 LIKE/正则表达式匹配。
虽然字符串匹配有很多应用,从数据清理到可观察性用例中的搜索日志,但很难将两个字符串之间的“模糊”关系表达为 LIKE 模式或正则表达式。现实世界中的数据集通常更“混乱”,需要比子字符串搜索提供的更灵活的方式,例如,查找拼写错误的字符串或由于光学字符识别 (OCR)而导致的错误。
为了解决这些挑战,存在许多知名的字符串相似度算法,包括Levenshtein、Damerau Levenshtein、Jaro Similarity 和Jaro Winkler。这些算法被广泛应用于拼写检查、剽窃检测以及更广泛的自然语言处理、计算语言学和生物信息学领域。
所有这些算法都计算搜索字符串和目标标记集之间的字符串相似度 (编辑距离)。此度量旨在通过计算将一个字符串转换为另一个字符串所需的最小操作数来量化两个字符串之间的差异。每个算法在计算此距离时允许的操作都不同,其中一些算法在计算计数时还会对特定操作赋予更高的权重。
在 24.1 中,我们使用新的函数扩展了对Levenshtein 距离的现有支持,这些函数适用于Damerau Levenshtein、Jaro Similarity 和Jaro Winkler。
可能最知名的实现此概念的算法(以至于它经常与编辑距离互换使用)是 Levenshtein 距离。此度量计算将一个单词更改为另一个单词所需的最小单字符编辑操作数。编辑操作限于 3 种类型
- **插入:**在字符串中添加单个字符。
- **删除:**从字符串中删除单个字符。
- **替换:**将字符串中的一个字符替换为另一个字符。
两个字符串之间的 Levenshtein 距离是将一个字符串转换为另一个字符串所需的最小操作数。Damerau-Levenshtein 在此概念的基础上增加了**换位**,即交换相邻字符。
例如,考虑 Levenshtein 和 Damerau Levenshtein 之间的差异,它们分别为“example”和“exmalpe”。
使用 Levenshtein 距离,我们需要 3 个操作:
在 ClickHouse 中确认
SELECT levenshteinDistance('example', 'exmalpe') AS d
┌─d─┐
│ 3 │
└───┘
使用 Damerau Levenshtein 距离,由于转置操作,我们只需要 2 个操作:
确认新的 ClickHouse damerauLevenshteinDistance 函数
SELECT damerauLevenshteinDistance('example', 'exmalpe') AS d
┌─d─┐
│ 2 │
└───┘
Jaro 相似度 和 Jaro-Winkler 相似度 算法具有等效的功能,并提供了一种计算编辑距离度量的替代方法,通过考虑转置和具有定义距离位置的公共字符数量来实现。
为了举例说明这种功能及其可能的应用,让我们考虑 错拼域名劫持 问题,也称为 URL 劫持。这是一种网络域名抢注(在别人的品牌或版权下注册域名)形式,针对在网络浏览器中错误输入网站地址的互联网用户(例如,输入“gooogle.com”而不是“google.com”)。由于这些网站通常是恶意的,因此品牌可能需要知道哪些最常访问的域名劫持了他们的域名。
检测这些错拼域名是字符串相似度函数的典型应用。我们只需要从目标网站中找到编辑距离小于 N 的最受欢迎的域名。为此,我们需要一个排名的域名集。 Tranco 数据集通过提供一个排名的最受欢迎域名集来解决这个问题。
排名的域名列表在网络安全和互联网测量中具有应用,但传统上易于操纵和影响。Tranco 旨在解决这个问题,并通过可重复的方法提供最新的列表。
我们可以使用两个简单的命令将完整的列表(包括子域名)插入 ClickHouse,包括每个网站的排名。
CREATE TABLE domains
(
`domain` String,
`rank` Float64
)
ENGINE = MergeTree
ORDER BY domain
INSERT INTO domains SELECT
c2 AS domain,
1 / c1 AS rank
FROM url('https://tranco-list.eu/download/PNZLJ/full', CSV)
0 rows in set. Elapsed: 4.374 sec. Processed 7.02 million rows, 204.11 MB (1.60 million rows/s., 46.66 MB/s.)
Peak memory usage: 116.77 MiB.
注意,我们使用 1/rank 作为 Tranco 建议,即:
“第一个域名获得 1 分,第二个获得 1/2 分,...,最后一个获得 1/N 分,未排名的域名获得 0 分。这种方法大致反映了对网站流行度分布的齐夫定律和“长尾效应”的观察。”
前 10 个域名应该很熟悉
SELECT *
FROM domains
ORDER BY rank DESC
LIMIT 10
┌─domain─────────┬────────────────rank─┐
│ google.com │ 1 │
│ amazonaws.com │ 0.5 │
│ facebook.com │ 0.3333333333333333 │
│ a-msedge.net │ 0.25 │
│ microsoft.com │ 0.2 │
│ apple.com │ 0.16666666666666666 │
│ googleapis.com │ 0.14285714285714285 │
│ youtube.com │ 0.125 │
│ www.google.com │ 0.1111111111111111 │
│ akamaiedge.net │ 0.1 │
└────────────────┴─────────────────────┘
10 rows in set. Elapsed: 0.313 sec. Processed 7.02 million rows, 254.36 MB (22.44 million rows/s., 813.00 MB/s.)
Peak memory usage: 34.56 MiB.
我们可以使用一个简单的查询来测试字符串距离函数识别错拼域名的有效性,以“facebook.com”为例
SELECT
domain,
levenshteinDistance(domain, 'facebook.com') AS d1,
damerauLevenshteinDistance(domain, 'facebook.com') AS d2,
jaroSimilarity(domain, 'facebook.com') AS d3,
jaroWinklerSimilarity(domain, 'facebook.com') AS d4,
rank
FROM domains
ORDER BY d1 ASC
LIMIT 10
┌─domain────────┬─d1─┬─d2─┬─────────────────d3─┬─────────────────d4─┬────────────────────rank─┐
│ facebook.com │ 0 │ 0 │ 1 │ 1 │ 0.3333333333333333 │
│ facebook.cm │ 1 │ 1 │ 0.9722222222222221 │ 0.9833333333333333 │ 1.4258771318823703e-7 │
│ acebook.com │ 1 │ 1 │ 0.9722222222222221 │ 0.9722222222222221 │ 0.000002449341494539193 │
│ faceboook.com │ 1 │ 1 │ 0.9188034188034188 │ 0.9512820512820512 │ 0.000002739643462799751 │
│ faacebook.com │ 1 │ 1 │ 0.9743589743589745 │ 0.9794871794871796 │ 5.744693196042826e-7 │
│ faceboom.com │ 1 │ 1 │ 0.8838383838383838 │ 0.9303030303030303 │ 3.0411914171495823e-7 │
│ facebool.com │ 1 │ 1 │ 0.9444444444444443 │ 0.9666666666666666 │ 5.228971429945901e-7 │
│ facebooks.com │ 1 │ 1 │ 0.9743589743589745 │ 0.9846153846153847 │ 2.7956239539124616e-7 │
│ facebook.co │ 1 │ 1 │ 0.9722222222222221 │ 0.9833333333333333 │ 0.00000286769597834316 │
│ facecbook.com │ 1 │ 1 │ 0.9049145299145299 │ 0.9429487179487179 │ 5.685177604948379e-7 │
└───────────────┴────┴────┴────────────────────┴────────────────────┴─────────────────────────┘
10 rows in set. Elapsed: 0.304 sec. Processed 5.00 million rows, 181.51 MB (16.44 million rows/s., 597.38 MB/s.)
Peak memory usage: 38.87 MiB.
这些看起来像是可信的错拼域名劫持尝试,虽然我们不建议测试它们!
品牌所有者可能希望针对其中最受欢迎的域名,让这些网站下线,甚至尝试获取 DNS 条目并添加重定向到正确的网站。例如,对于 facebool.com,这种情况已经存在。
开发一个健壮的度量标准来识别要针对的列表超出了本文档的范围。为了举例说明,我们将找到所有 Damerau-Levenshtein 距离为 1 的域名,并按其实际流行度排序,排除任何 第一个显著子域名 为“facebook”的情况。
SELECT domain, rank, damerauLevenshteinDistance(domain, 'facebook.com') AS d
FROM domains
WHERE (d <= 1) AND (firstSignificantSubdomain(domain) != 'facebook')
ORDER BY rank DESC
LIMIT 10
┌─domain────────┬─────────────────────rank─┬─d─┐
│ facebok.com │ 0.000005683820436744763 │ 1 │
│ facbook.com │ 0.000004044178607104004 │ 1 │
│ faceboook.com │ 0.000002739643462799751 │ 1 │
│ acebook.com │ 0.000002449341494539193 │ 1 │
│ faceboo.com │ 0.0000023974606097221825 │ 1 │
│ facebbook.com │ 0.000001914476505544324 │ 1 │
│ facebbok.com │ 0.0000014273133538010068 │ 1 │
│ faceook.com │ 7.014964321891459e-7 │ 1 │
│ faceboock.com │ 6.283680527628087e-7 │ 1 │
│ faacebook.com │ 5.744693196042826e-7 │ 1 │
└───────────────┴──────────────────────────┴───┘
10 rows in set. Elapsed: 0.318 sec. Processed 6.99 million rows, 197.65 MB (21.97 million rows/s., 621.62 MB/s.)
Peak memory usage: 12.77 MiB.
这似乎是一个合理的起点。请随意用您自己的域名重复此操作,并告诉我们它是否有用!
使用 ReplacingMergeTree 的 FINAL 的垂直算法。
由 Duc Canh Le 和 Joris Giovannangeli 贡献
上个月的版本已经 发布了对使用 FINAL 修饰符的 SELECT 查询的重大优化。我们目前的版本在将 FINAL 与 ReplacingMergeTree 表引擎一起使用时带来了额外的优化。
提醒一下,FINAL 可以用作使用 ReplacingMergeTree、AggregatingMergeTree 和 CollapsingMergeTree 引擎创建的表的查询修饰符,以便在查询时动态应用缺失数据转换。从 ClickHouse 23.12 开始,与查询 WHERE 子句匹配的表数据将根据排序键值划分为不相交和相交范围。不相交范围是仅存在于单个分区中的数据区域,因此不需要转换。相反,相交范围内的行可能存在于(基于排序键值)多个分区中,需要特殊处理。所有不相交的数据范围都像查询中没有使用 FINAL 修饰符一样并行处理。这只会留下相交的数据范围,对于这些范围,表引擎的合并逻辑会在查询时动态应用。
提醒一下,下图显示了查询时如何通过 查询管道 合并这些数据范围。
来自所选数据范围的数据以 块(组合数据范围的多个相邻行)的粒度以物理顺序流式传输,并使用 k 路归并排序 算法合并。
ReplacingMergeTree 表引擎在合并期间仅保留行的最新插入版本(基于其包含分区的创建时间戳),较旧的版本被丢弃。为了合并来自流式数据块的行,该算法按行迭代块的列,并将数据复制到一个新的块中。为了使 CPU 有效地执行此逻辑,块需要驻留在 CPU 缓存中,例如 L1/L2/L3 缓存。块中包含的列越多,块需要从 CPU 缓存中重复驱逐的可能性就越大,从而导致 缓存抖动。下图说明了这一点: 我们假设 CPU 缓存可以同时容纳来自我们示例数据的两个块。当上述合并算法按顺序迭代来自所有三个选定匹配数据范围的块以按行合并它们的数据时,运行时将受到每次迭代中最坏情况下缓存驱逐的影响。这需要一次又一次地将数据从主内存复制到 CPU 缓存,由于不必要的内存访问导致整体性能下降。
ClickHouse 24.1 试图通过更缓存友好的查询时合并算法来防止这种情况,该算法专门用于 ReplacingMergeTree,其工作原理类似于 垂直 后台合并算法。下图概述了该算法的工作原理: 合并排序期间,该算法不是复制每行的所有列值,而是分为两个阶段。在阶段 1 中,该算法只合并来自排序键列的数据。我们假设列 c1 是我们上面示例中的排序键列。此外,基于排序键列的合并,该算法为数据范围创建了一个临时的行级过滤位图,指示哪些行将在常规合并中存活。在阶段 2 中,这些位图用于相应地过滤数据范围,并从进一步的处理步骤中删除所有旧行。这种过滤是逐列进行的,并且仅针对所有非排序键列。请注意,阶段 1 和阶段 2 各自需要的 CPU 缓存空间都比以前的 23.12 合并算法少,从而导致更少的 CPU 缓存驱逐,从而降低内存延迟。
我们用一个具体的例子演示使用 FINAL 的新的垂直查询时合并算法。与之前的版本文章一样,我们稍微 修改了来自英国房价 样本数据集 的表格,并假设该表格存储了关于当前房产报价的信息,而不是之前出售的房产信息。我们使用的是 ReplacingMergeTree 表引擎,允许我们通过简单地插入具有相同排序键值的新的行来更新报价的价格和其他特征。
CREATE OR REPLACE TABLE uk_property_offers
(
id UInt32,
price UInt32,
date Date,
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
type Enum8('terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'other' = 0),
is_new UInt8,
duration Enum8('freehold' = 1, 'leasehold' = 2, 'unknown' = 0),
addr1 String,
addr2 String,
street LowCardinality(String),
locality LowCardinality(String),
town LowCardinality(String),
district LowCardinality(String),
county LowCardinality(String)
)
ENGINE = ReplacingMergeTree(date)
ORDER BY (id);
接下来,我们 插入 了大约 1500 万行到表格中。
我们在 ClickHouse 版本 24.1 上使用 FINAL 修饰符运行一个典型的分析查询,其中新的垂直查询时合并算法被禁用,选择三个最昂贵的初级邮政编码。
SELECT
postcode1,
formatReadableQuantity(avg(price))
FROM uk_property_offers
GROUP BY postcode1
ORDER BY avg(price) DESC
LIMIT 3
SETTINGS enable_vertical_final = 0;
┌─postcode1─┬─formatReadableQuantity(avg(price))─┐
│ W1A │ 163.58 million │
│ NG90 │ 68.59 million │
│ CF99 │ 47.00 million │
└───────────┴────────────────────────────────────┘
0 rows in set. Elapsed: 0.011 sec. Processed 9.29 thousand rows, 74.28 KB (822.68 thousand rows/s., 6.58 MB/s.)
Peak memory usage: 1.10 MiB.
我们使用新的垂直查询时合并算法启用运行相同的查询。
SELECT
postcode1,
formatReadableQuantity(avg(price))
FROM uk_property_offers
GROUP BY postcode1
ORDER BY avg(price) DESC
LIMIT 3
SETTINGS enable_vertical_final = 1;
┌─postcode1─┬─formatReadableQuantity(avg(price))─┐
│ W1A │ 163.58 million │
│ NG90 │ 68.59 million │
│ CF99 │ 47.00 million │
└───────────┴────────────────────────────────────┘
0 rows in set. Elapsed: 0.004 sec. Processed 9.29 thousand rows, 111.42 KB (2.15 million rows/s., 25.81 MB/s.)
Peak memory usage: 475.21 KiB.
请注意,第二个查询运行速度更快,并且消耗更少的内存。
国际域名的世界
最后,Alexey 使用此版本中添加的新的 punycode 函数对国际域名的质量进行了探讨。我们剪掉了下面视频中的那部分内容,供您欣赏。