难以置信,3 月份已经到了!时间飞逝,但新月到来也意味着我们又有一个 ClickHouse 版本发布,供大家享用!
ClickHouse 24.2 版本包含 **18 个新功能** 🎁 **18 项性能优化** 🛷 **49 个错误修复** 🐛
新贡献者
一如既往,我们对 24.2 版本的所有新贡献者表示热烈的欢迎!ClickHouse 的受欢迎程度很大程度上归功于社区的贡献。看到社区的不断发展,我们总是感到谦卑。
以下是新贡献者的姓名列表
johnnymatthews、AlexeyGrezz、Aris Tritas、Charlie、Fille、HowePa、Joshua Hildred、Juan Madurga、Kirill Nikiforov、Nickolaj Jepsen、Nikolai Fedorovskikh、Pablo Musa、Ronald Bradford、YenchangChan、conicliu、jktng、mikhnenko、rogeryk、una、Кирилл Гарбар_
提示:如果您好奇我们如何生成此列表……点击此处。
如果您在这里看到您的姓名,请与我们联系……但我们也会在 Twitter 等平台上找到您。
您还可以查看演示文稿的幻灯片。
好的,让我们来了解一下这些功能!
自动检测文件格式
由 Pavel Kruglov 贡献
在处理文件时,ClickHouse 会自动检测文件类型,即使它没有有效的扩展名。例如,以下文件 foo
包含 JSON 行格式的数据
$ cat foo
{"name": "John Doe", "age": 30, "city": "New York"}
{"name": "Jane Doe", "age": 25, "city": "Los Angeles"}
{"name": "Jim Beam", "age": 35, "city": "Chicago"}
{"name": "Jill Hill", "age": 28, "city": "Houston"}
{"name": "Jack Black", "age": 40, "city": "Philadelphia"}
让我们尝试使用 file
函数处理该文件
SELECT *
FROM file('foo')
┌─name───────┬─age─┬─city─────────┐
│ John Doe │ 30 │ New York │
│ Jane Doe │ 25 │ Los Angeles │
│ Jim Beam │ 35 │ Chicago │
│ Jill Hill │ 28 │ Houston │
│ Jack Black │ 40 │ Philadelphia │
└────────────┴─────┴──────────────┘
5 rows in set. Elapsed: 0.003 sec.
非常棒。现在让我们将内容写入 Parquet 格式
SELECT *
FROM file('foo')
INTO OUTFILE 'bar'
FORMAT Parquet
我们可以在不告诉 ClickHouse 格式的情况下读取它吗?
SELECT *
FROM file('bar')
┌─name───────┬─age─┬─city─────────┐
│ John Doe │ 30 │ New York │
│ Jane Doe │ 25 │ Los Angeles │
│ Jim Beam │ 35 │ Chicago │
│ Jill Hill │ 28 │ Houston │
│ Jack Black │ 40 │ Philadelphia │
└────────────┴─────┴──────────────┘
5 rows in set. Elapsed: 0.003 sec.
当然可以!自动检测在从 URL 读取时也有效。因此,如果我们在上述文件周围启动一个本地 HTTP 服务器
python -m http.server
然后我们可以像这样读取它们
SELECT *
FROM url('https://127.0.0.1:8000/bar')
┌─name───────┬─age─┬─city─────────┐
│ John Doe │ 30 │ New York │
│ Jane Doe │ 25 │ Los Angeles │
│ Jim Beam │ 35 │ Chicago │
│ Jill Hill │ 28 │ Houston │
│ Jack Black │ 40 │ Philadelphia │
└────────────┴─────┴──────────────┘
现在这些示例就足够了,但您也可以将此功能与 s3
、hdfs
和 azureBlobStorage
表函数一起使用。
更漂亮的格式
RogerYK
如果您曾经需要快速解释查询结果中的大数字,那么此功能非常适合您。当您返回单个数值列时,如果该列中的值大于 100 万,则可读数量将作为注释与该值本身一起显示。
SELECT 765432198
┌─765432198─┐
│ 765432198 │ -- 765.43 million
└───────────┘
视图的安全
由 Artem Brustovetskii 贡献
在此版本之前,如果您在表上定义了一个视图,则用户要访问该视图,还需要访问该表。这并不理想,在 24.2 版本中,我们添加了 SQL SECURITY
和 DEFINER
规范 用于 CREATE VIEW
查询来解决此问题。
假设我们有一个公司薪资表,其中包含员工姓名、部门、薪资和地址详细信息。我们可能希望使人力资源团队能够访问所有信息,但也允许其他用户查看员工姓名和部门。
首先让我们创建一个表并填充它
CREATE TABLE payroll (
name String,
address String,
department LowCardinality(String),
salary UInt32
)
Engine = MergeTree
ORDER BY name;
INSERT INTO payroll (`name`, `address`, `department`, `salary`) VALUES
('John Doe', '123 Maple Street, Anytown, AT 12345', 'HR', 50000),
('Jane Smith', '456 Oak Road, Sometown, ST 67890', 'Marketing', 55000),
('Emily Jones', '789 Pine Lane, Thistown, TT 11223', 'IT', 60000),
('Michael Brown', '321 Birch Blvd, Othertown, OT 44556', 'Sales', 52000),
('Sarah Davis', '654 Cedar Ave, Newcity, NC 77889', 'HR', 53000),
('Daniel Wilson', '987 Elm St, Oldtown, OT 99000', 'IT', 62000),
('Laura Martinez', '123 Spruce Way, Mytown, MT 22334', 'Marketing', 56000),
('James Garcia', '456 Fir Court, Yourtown, YT 33445', 'Sales', 51000);
我们有两个用户 - Alice 是人力资源团队的成员,Bob 是工程团队的成员。Alice 属于人力资源团队,可以访问薪资表,Bob 则无法访问!
CREATE USER alice IDENTIFIED WITH sha256_password BY 'alice';
GRANT SELECT ON default.payroll TO alice;
GRANT SELECT ON default.employees TO alice WITH GRANT OPTION;
CREATE USER bob IDENTIFIED WITH sha256_password BY 'bob';
Alice 创建了一个名为 employees 的视图
CREATE VIEW employees
DEFINER = alice SQL SECURITY DEFINER
AS
SELECT name, department
FROM payroll;
然后 Alice 将对该视图的访问权限授予自己和 Bob
GRANT SELECT ON default.employees TO alice;
GRANT SELECT ON default.employees TO bob;
如果我们然后以 Bob 的身份登录
clickhouse client -u bob
我们可以查询 employees
表
SELECT *
FROM employees
┌─name───────────┬─department─┐
│ Daniel Wilson │ IT │
│ Emily Jones │ IT │
│ James Garcia │ Sales │
│ Jane Smith │ Marketing │
│ John Doe │ HR │
│ Laura Martinez │ Marketing │
│ Michael Brown │ Sales │
│ Sarah Davis │ HR │
└────────────────┴────────────┘
但他无法查询底层的薪资表,这正是我们期望的结果
SELECT *
FROM payroll
Received exception from server (version 24.3.1):
Code: 497. DB::Exception: Received from localhost:9000. DB::Exception: bob: Not enough privileges. To execute this query, it's necessary to have the grant SELECT(name, address, department, salary) ON default.payroll. (ACCESS_DENIED)
矢量化距离函数
由 Robert Schulze 贡献
在最近的博文中,我们探讨了如何将 ClickHouse 用作向量数据库,当用户需要对准确结果进行高性能线性扫描和/或能够通过 SQL 将向量搜索与元数据上的过滤和聚合相结合时。用户可以使用这些功能通过检索增强生成 (RAG) 管道为基于 LLM 的应用程序提供上下文。我们对向量搜索底层支持的投资仍在继续,最近的重点是提高线性扫描的性能 - 特别是 距离函数系列,用于计算两个向量之间的距离。作为背景信息,我们推荐 这篇文章 以及我们自己的 Mark 最近发布的一个视频
虽然向量搜索的查询性能对于较大的数据集很容易受 I/O 限制,但许多用户只需要搜索适合内存的较小数据集的 SQL。在这些情况下,ClickHouse 中的性能可能会受 CPU 限制。因此,确保此代码正确矢量化并使用最新的指令集的努力可以提供显着的改进,并将性能提升到受内存带宽限制的程度 - 对于使用 DDR-5 的机器来说,这意味着更高的扫描性能。
在 24.2 版本中,我们很高兴地宣布 cosineDistance、dotProduct 和 L2Distance(欧几里得距离)函数都已进行了优化,以利用最新的指令集。对于 x86 来说,这意味着使用融合乘加 (FMA) 和水平加法归约运算以及 AVX-512 指令,以及针对 ARM 的自动矢量化。
例如,考虑以下 glove 数据集,它在几乎官方的 ANN 基准测试 中广受欢迎。这个特定的子集,我们已将其 以 Parquet 格式提供(2.5 GiB),包含从 8400 亿个 CommonCrawl 令牌训练的 210 万个向量。此集合中的每个向量都有 300 个维度,并表示一个单词。鉴于简单的架构,将其加载到 ClickHouse 中需要几秒钟
CREATE TABLE glove
(
`word` String,
`vector` Array(Float32)
)
ENGINE = MergeTree
ORDER BY word;
INSERT INTO glove SELECT *
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/glove/glove_840b_300d.parquet')
0 rows in set. Elapsed: 49.779 sec. Processed 2.20 million rows, 2.66 GB (44.12 thousand rows/s., 53.44 MB/s.)
Peak memory usage: 1.03 GiB.
机器规格:i3en.3xlarge - 12vCPU Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz,96GiB RAM
我们可以通过查找最接近特定单词的向量的单词来比较 ClickHouse 版本之间的性能。例如,在 23.12 版本中
WITH 'dog' AS search_term,
(
SELECT vector
FROM glove
WHERE word = search_term
LIMIT 1
) AS target_vector
SELECT word, cosineDistance(vector, target_vector) AS score
FROM glove
WHERE lower(word) != lower(search_term)
ORDER BY score ASC
LIMIT 5
┌─word────┬──────score─┐
│ dogs │ 0.11640692 │
│ puppy │ 0.14147866 │
│ pet │ 0.19425482 │
│ cat │ 0.19831467 │
│ puppies │ 0.24826884 │
└─────────┴────────────┘
5 rows in set. Elapsed: 0.407 sec. Processed 2.14 million rows, 2.60 GB (5.25 million rows/s., 6.38 GB/s.)
Peak memory usage: 248.94 MiB.
这里重要的是,此数据集适合 FS 缓存,如磁盘上的压缩大小所示
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 LIKE 'glove'
GROUP BY name
ORDER BY name DESC
┌─name───┬─compressed_size─┬─uncompressed_size─┬─ratio─┐
│ word │ 13.41 MiB │ 18.82 MiB │ 1.4 │
│ vector │ 2.46 GiB │ 2.47 GiB │ 1 │
└────────┴─────────────────┴───────────────────┴───────┘
2 rows in set. Elapsed: 0.003 sec.
单词和向量列的总大小约为 2.65 GB,可以轻松放入内存。
在 24.2 版本中,相同的查询性能提升了 25% 以上。
5 rows in set. Elapsed: 0.286 sec. Processed 1.91 million rows, 2.32 GB (6.68 million rows/s., 8.12 GB/s.)
Peak memory usage: 216.89 MiB.
这些差异会因处理器、数据集大小、向量基数和 RAM 性能而异。
关于点积的一点说明
在 24.2 版本之前,dotProduct 函数 未进行向量化。在确保其效率最大化的过程中,我们注意到该函数还会对任何常量参数进行必要的解包(最常见的使用案例,即我们传递一个常量向量进行比较),这导致了不必要的内存复制。这意味着实际函数运行时间主要受内存操作支配 - 在这种情况下,向量化带来的提升相对较小。一旦消除了这些内存操作,性能提升了惊人的 270 倍,在自动化基准测试中取得了显著的效果!
虽然不值得将该函数的性能与早期版本进行比较(它们有点令人尴尬 :)),但我们认为可以借此机会指出,这些改进使我们能够展示用户现在可以利用的一种不错的性能优化。
读者可能还记得余弦距离和点积密切相关。更具体地说,余弦距离测量多维空间中两个向量之间夹角的余弦值。它源自余弦相似度,余弦距离定义为 1 - 余弦相似度。余弦相似度计算为两个向量的点积除以其模长的乘积,即 1- ((a.b)/||a||||b||)
。反之,点积测量两个数字序列(即向量 A 和 B,其分量为 ai
和 bi
)对应项的乘积之和,点积为 A⋅B=∑aibi
。
当您对向量进行归一化(即两个向量的模长都为 1)时,余弦距离和点积变得特别相关。在这种情况下,余弦相似度恰好等于点积,因为余弦相似度公式中的分母(向量的模长)都为 1,从而抵消了。因此,我们的余弦距离简化为 1-(a.b)
。
我们如何利用这一点?
我们可以在插入时使用 L2Norm 函数(向量模长)对向量进行归一化,从而允许我们在查询时使用 dotProduct
函数。这样做的主要动机是 dotProduct
函数在计算上更简单(我们不必计算每个向量的模长),从而有可能节省一些查询时间。
在查询时执行归一化
INSERT INTO glove
SETTINGS schema_inference_make_columns_nullable = 0
SELECT
word,
vector / L2Norm(vector) AS vector
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/glove/glove_840b_300d.parquet')
SETTINGS schema_inference_make_columns_nullable = 0
0 rows in set. Elapsed: 51.699 sec. Processed 2.20 million rows, 2.66 GB (42.48 thousand rows/s., 51.46 MB/s.)
因此,我们之前的查询变为
WITH
'dog' AS search_term,
(
SELECT vector
FROM glove
WHERE word = search_term
LIMIT 1
) AS target_vector
SELECT
word,
1 - dotProduct(vector, target_vector) AS score
FROM glove
WHERE lower(word) != lower(search_term)
ORDER BY score ASC
LIMIT 5
┌─────────┬─────────────────────┐
│ word │ score │
├─────────┼─────────────────────┤
│ dogs │ 0.11640697717666626 │
│ puppy │ 0.1414787769317627 │
│ pet │ 0.19425475597381592 │
│ cat │ 0.19831448793411255 │
│ puppies │ 0.24826878309249878 │
└─────────┴─────────────────────┘
5 rows in set. Elapsed: 0.262 sec. Processed 1.99 million rows, 2.42 GB (7.61 million rows/s., 9.25 GB/s.)
Peak memory usage: 226.29 MiB.
与使用余弦相似度时的 0.286 秒相比,此查询耗时 0.262 秒 - 与原始查询相比,节省的时间很短,但每一毫秒都很重要!
有一个尚未合并的 PR 将修复 dotProduct
函数的正确性和性能问题,该 PR 应该很快就会合并。
自适应异步插入
由 Julia Kartseva 贡献
使用传统的插入查询,数据会同步插入到表中:当 ClickHouse 接收到查询时,数据会立即以数据部分的形式写入数据库存储。为了获得最佳性能,需要对数据进行批处理,通常情况下,我们应该避免过于频繁地创建太多小的插入操作。
异步插入将数据批处理从客户端转移到服务器端:插入查询的数据首先插入到缓冲区中,然后稍后或异步写入数据库存储。这非常方便,尤其是在许多并发客户端频繁地将数据插入到表中、需要实时分析数据且客户端批处理导致的延迟不可接受的场景中。例如,可观察性用例经常涉及数千个监控代理持续发送少量事件和指标数据。此类场景可以利用异步插入模式,如下面的图表所示
在上面图表中的示例场景中,ClickHouse 在特定表一段时间内没有插入活动后,收到针对该表的异步插入查询 1。收到插入查询 1 后,查询的数据会被插入到内存缓冲区中,并且默认的缓冲区刷新超时计数器开始计时。在计数器结束(因此缓冲区被刷新)之前,来自同一客户端或其他客户端的其他异步插入查询的数据可以收集到缓冲区中。刷新缓冲区将在磁盘上创建一个数据部分,其中包含在刷新之前收到的所有插入查询的组合数据。
请注意,使用默认返回行为,所有插入查询只有在缓冲区刷新后才会向发送方返回插入确认。换句话说,发送插入查询的客户端调用会被阻塞,直到下一次缓冲区刷新发生。因此,不频繁的插入操作具有更高的延迟。我们将在下面对此进行说明
上面显示了不频繁插入查询的极端情况场景。ClickHouse 在一段时间内没有表插入活动后,收到异步插入查询 1。这将触发一个新的缓冲区刷新周期,并使用默认的缓冲区刷新超时,这意味着该查询的发送方需要等待完整的默认缓冲区刷新时间(OSS 为 200 毫秒,ClickHouse Cloud 为 1000 毫秒),然后才能收到插入确认。同样,插入查询 2 的发送方也会遇到较高的插入延迟。
ClickHouse 24.2 版本中引入的自适应异步插入缓冲区刷新超时通过使用自适应算法根据插入频率自动调整缓冲区刷新超时来解决此问题。
对于之前显示的一些不频繁插入查询的极端情况场景,缓冲区刷新超时计数器现在以最小值(50 毫秒)开始。因此,这些查询的数据会更快地写入磁盘。几乎立即将少量数据写入磁盘是可以的,因为频率很低。因此,没有Too many parts
保护措施生效的风险。
频繁插入操作与以前一样。它们将被延迟并组合在一起。
当 ClickHouse 在一段时间内没有表插入活动后收到插入查询 1 时,缓冲区刷新超时计数器将以最小值开始,当其他插入操作频繁发生时,它会自动向上调整到最大值(并且当插入频率降低时也会向下调整)。
总之,频繁插入操作与以前一样。它们将被延迟并组合在一起。但是,不频繁的插入操作不会延迟太多,并且表现得像同步插入操作一样。您可以直接启用异步插入,而无需担心。😀
让我们用一个例子来演示这一点。为此,我们启动一个 ClickHouse 24.2 实例,并创建我们 UpClick 可观察性示例应用程序中表的简化版本。
CREATE TABLE default.upclick_metrics (
`url` String,
`status_code` UInt8,
`city_name_en` String
) ENGINE = MergeTree
ORDER BY (url, status_code, city_name_en);
现在,我们可以使用这个简单的 Python 脚本(使用 ClickHouse Connect Python 驱动程序)向我们的 ClickHouse 实例发送 10 个小的异步插入操作。请注意,我们通过将wait_for_async_insert
设置为 1 来显式启用默认返回行为,并且我们将缓冲区刷新超时增加到 2 秒(以便更好地演示自适应异步插入)。
import clickhouse_connect
import time
client = clickhouse_connect.get_client(...)
for _ in range(10):
start_time = time.time()
client.insert(
database='default',
table='upclick_metrics',
data=[['clickhouse.com', 200, 'Amsterdam']],
column_names=['url', 'status_code', 'city_name_en'],
settings={
'async_insert':1,
'wait_for_async_insert':1,
'async_insert_busy_timeout_ms':2000,
'async_insert_use_adaptive_busy_timeout':0}
)
end_time = time.time()
print(str(round(end - start, 2)) + ' seconds')
time.sleep(1)
该脚本测量并打印每个插入操作的延迟(以秒为单位),然后休眠一秒以模拟不频繁的插入操作。
我们禁用自适应异步插入缓冲区刷新超时运行该脚本(async_insert_use_adaptive_busy_timeout
设置为 0),输出结果为
2.03 seconds
2.02 seconds
2.03 seconds
2.02 seconds
2.04 seconds
2.02 seconds
2.03 seconds
2.03 seconds
2.03 seconds
2.03 seconds
我们可以看到不频繁插入操作的延迟高达 2 秒。
但是,当我们启用自适应异步插入缓冲区刷新超时(async_insert_use_adaptive_busy_timeout
设置为 1)并使所有其他设置与上述运行相同运行该脚本时,输出结果为
0.07 seconds
0.06 seconds
0.08 seconds
0.08 seconds
0.07 seconds
0.07 seconds
0.07 seconds
0.08 seconds
0.07 seconds
0.08 seconds
不频繁插入操作的延迟大幅降低。
由于 Python 驱动程序机制、服务器端查询解析以及将查询发送到 ClickHouse 等方面存在其他延迟开销,因此我们在这里无法精确看到最小的缓冲区刷新超时 50 毫秒。