欢迎来到我们的 2024 年首个版本!ClickHouse 24.1 版本包含 26 个新功能 🎁 22 项性能优化 🛷 47 个错误修复 🐛
像往常一样,我们将在本博客文章中重点介绍一小部分新功能和改进,但此版本还包括生成 shingles、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 等平台上找到您。
您还可以查看演示文稿中的幻灯片。
Variant 类型
由 Pavel Kruglov 贡献
此版本引入了 Variant 类型,尽管它仍处于实验模式,因此您需要配置以下设置才能使其工作。
SET allow_experimental_variant_type=1,
use_variant_as_common_type = 1;
Variant 类型是向 ClickHouse 添加半结构化列的长期项目的一部分。此类型是嵌套列的可区分联合。例如,Variant(Int8, Array(String))
的每个值要么是 Int8
,要么是 Array(String)
。
在处理映射时,这种新类型将派上用场。例如,假设我们想要创建一个具有不同类型值的映射:此版本引入了 Variant 类型,尽管它仍处于实验模式,因此您需要配置以下设置才能使其工作。
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)
而现在它返回 Variant 类型
Row 1:
──────
x: {'Hello':1,'World':'Mark'}
type: Map(String, Variant(String, UInt8))
我们也可以在从 CSV 文件读取时使用此类型。例如,假设我们有以下具有混合类型的文件
$ cat foo.csv
value
1
"Mark"
2.3
在处理文件时,我们可以添加模式推断提示以使其使用 Variant 类型
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 函数,并且将使用 Variant 类型
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 运算符和 match 运算符的用户通常会被其性能所震惊。根据要匹配的表达式,这可以映射到正则表达式,也可以使用相当不为人知的 Volnitsky 字符串搜索算法的高效实现来执行子字符串搜索。ClickHouse 还尽最大努力利用主键和 跳跃索引来加速 LIKE / 正则表达式匹配。
虽然字符串匹配有许多应用,从数据清理到可观测性用例中的搜索日志,但很难用 LIKE 模式或正则表达式来表达两个字符串之间的“模糊”关系。真实世界的数据集通常更加“混乱”,并且需要比子字符串搜索提供的更多灵活性,例如,查找拼写错误的字符串或因光学字符识别 (OCR)而造成的错误。
为了应对这些挑战,存在许多著名的字符串相似度算法,包括 Levenshtein、Damerau Levenshtein、Jaro 相似度和 Jaro Winkler。这些算法广泛应用于拼写检查、抄袭检测以及更广泛的自然语言处理、计算语言学和生物信息学领域。
所有这些算法都计算搜索字符串和目标标记集之间的字符串相似度(编辑距离)。此度量旨在量化两个字符串彼此之间的差异程度,方法是计算将一个字符串转换为另一个字符串所需的最少操作数。每种算法在其允许的操作中有所不同,以计算此距离,有些算法在计算计数时还会对特定操作进行加权。
在 24.1 版本中,我们使用 Damerau Levenshtein、Jaro 相似度和 Jaro Winkler 的新函数扩展了我们对 Levenshtein 距离的现有支持。
可能最著名的实现此概念的算法(以至于经常与编辑距离互换使用)是 Levenshtein 距离。此指标计算将一个单词更改为另一个单词所需的最少单字符编辑操作数。编辑操作仅限于 3 种类型
- 插入:向字符串添加单个字符。
- 删除:从字符串中删除单个字符。
- 替换:将字符串中的一个字符替换为另一个字符。
两个字符串之间的 Levenshtein 距离是将一个字符串转换为另一个字符串所需的最少操作数。Damerau-Levenshtein 在此概念的基础上添加了换位,即交换相邻字符。
例如,考虑“example”和“exmalpe”的 Levenshtein 距离和 Damerau Levenshtein 距离之间的差异。
使用 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 劫持。这是一种网络抢注(坐在别人的品牌或版权下的网站上)形式,目标是错误地将网站地址键入 Web 浏览器的互联网用户(例如,“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 分。此方法大致反映了网站受欢迎程度分布中的 Zipf 定律和“长尾效应”的观察结果。”
前 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.
这似乎是一个合理的起始列表。请随意使用您自己的域名重复此操作,并告知我们它是否有用!
用于 FINAL 和 ReplacingMergeTree 的垂直算法。
由 Duc Canh Le 和 Joris Giovannangeli 贡献
上个月的版本已经附带了对带有 FINAL 修饰符的 SELECT 查询的重大优化。我们当前的版本在使用 ReplacingMergeTree 表引擎时,为 FINAL 带来了一些额外的优化。
提醒一下,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 函数探索了一些有争议的国际域名质量。我们剪掉了下面视频的这一部分供您观看。