北半球的春天到了,也是 ClickHouse 发布新版本的时候了。
ClickHouse 24.3 版本包含 **12 个新特性** 🎁 **18 项性能优化** 🛷 **60 个 bug 修复** 🐛
新贡献者
一如既往,我们对 24.3 版本的所有新贡献者表示热烈的欢迎!ClickHouse 的受欢迎程度在很大程度上归功于社区的贡献。看到社区不断壮大,我们深感荣幸。
以下是新贡献者的姓名列表:
johnnymatthews, AlexeyGrezz, Aliaksei Khatskevich, Aris Tritas, Artem Alperin, Blacksmith, Blargian, Brokenice0415, Charlie, Dan Wu, Daniil Ivanik, Eyal Halpern Shalev, Fille, HowePa, Jayme Bird, Joshua Hildred, Juan Madurga, Kirill Nikiforov, Lino Uruñuela, LiuYuan, Maksim Alekseev, Marina Fathouat, Mark Needham, Mathieu Rey, MochiXu, Nataly Merezhuk, Nickolaj Jepsen, Nikita Fomichev, Nikolai Fedorovskikh, Nikolay Edigaryev, Nikolay Monkov, Nikolay Yankin, Oxide Computer Company, Pablo Musa, PapaToemmsn, Peter, Pham Anh Tuan, Roman Glinskikh, Ronald Bradford, Shanfeng Pang, Shaun Struwig, Shuai li, Shubham Ranjan, Tim Liou, Waterkin, William Schoeffel, YenchangChan, Zheng Miao, avinzhang, beetelbrox, bluikko, chenwei, conicliu, danila-ermakov, divanik, edpyt, jktng, josh-hildred, mikhnenko, mochi, nemonlou, qaziqarta, rogeryk, shabroo, shuai-xu, sunny, sunny19930321, tomershafir, una, unashi, Кирилл Гарбар, 豪肥肥
提示:如果您好奇我们是如何生成此列表的……点击此处。
您还可以查看演示文稿的幻灯片。
默认启用分析器
由 Maksim Kita、Nikolai Kochetov、Dmitriy Novik、Vladimir Cherkasov、Igor Nikonov、Yakov Olkhovskiy 等贡献
分析器是 ClickHouse 中一个全新的查询分析和优化基础设施,我们已经开发了数年。它提供了更好的兼容性和功能完整性,并支持复杂的查询优化。
我们已经有一段时间提供了分析器的实验性支持,但从 24.3 版本开始,此功能处于测试阶段并默认启用。
如果您不想使用它,仍然可以通过配置以下设置来禁用它
SET allow_experimental_analyzer = 0;
在 24.4 或 24.5 版本中,我们计划将分析器提升到生产环境并移除旧的查询分析实现。
旧的查询运行时无法很好地处理使用嵌套 CTE 和联接的查询。例如,以下查询应该返回数字 1
WITH example AS (
SELECT '2021-01-01' AS date, 1 AS node, 1 AS user
)
SELECT extra_data
FROM (
SELECT join1.*
FROM example
LEFT JOIN (
SELECT '2021-01-01' AS date, 1 AS extra_data
) AS join1
ON example.date = join1.date
LEFT JOIN (
SELECT '2021-01-01' AS date
) AS join2
ON example.date = join2.date);
但是,如果我们使用旧的查询运行时(我们可以在 24.3 中通过设置 allow_experimental_analyzer = 0
来模拟),我们将收到以下错误
Received exception:
Code: 47. DB::Exception: Missing columns: 'extra_data' while processing query: 'WITH example AS (SELECT '2021-01-01' AS date, 1 AS node, 1 AS user) SELECT extra_data FROM (SELECT join1.* FROM example LEFT JOIN (SELECT '2021-01-01' AS date, 1 AS extra_data) AS join1 ON example.date = join1.date LEFT JOIN (SELECT '2021-01-01' AS date) AS join2 ON example.date = join2.date) SETTINGS allow_experimental_analyzer = 0', required columns: 'extra_data' 'extra_data'. (UNKNOWN_IDENTIFIER)
分析器还支持在同一个查询中使用多个 ARRAY JOIN
子句,这在之前是不可能的。
假设我们有以下包含订单、产品和评论的 JSON 文件。
[
{
"order_id": 1,
"products": [
{
"product_id": 101,
"name": "Laptop",
"reviews": [
{"review_id": 1001, "rating": 5, "comment": "Excellent product!"},
{"review_id": 1002, "rating": 4, "comment": "Very good, but could be cheaper."}
]
},
{
"product_id": 102,
"name": "Smartphone",
"reviews": [
{"review_id": 2001, "rating": 5, "comment": "Best phone I've ever had."},
{"review_id": 2002, "rating": 3, "comment": "Battery life could be better."}
]
}
]
},
{
"order_id": 2,
"products": [
{
"product_id": 103,
"name": "Headphones",
"reviews": [
{"review_id": 3001, "rating": 5, "comment": "Great sound quality!"},
{"review_id": 3002, "rating": 2, "comment": "Stopped working after a month."}
]
},
{
"product_id": 104,
"name": "E-book Reader",
"reviews": [
{"review_id": 4001, "rating": 4, "comment": "Makes reading so convenient!"},
{"review_id": 4002, "rating": 5, "comment": "A must-have for book lovers."}
]
}
]
}
]
我们希望整理数据,以便能够逐行查看产品信息以及对应的评论。我们可以使用以下查询来实现
SELECT
review.rating AS rating,
review.comment,
product.product_id AS id,
product.name AS name
FROM `products.json`
ARRAY JOIN products AS product
ARRAY JOIN product.reviews AS review
ORDER BY rating DESC
┌─rating─┬─review.comment───────────────────┬──id─┬─name──────────┐
1. │ 5 │ Excellent product! │ 101 │ Laptop │
2. │ 5 │ Best phone I've ever had. │ 102 │ Smartphone │
3. │ 5 │ Great sound quality! │ 103 │ Headphones │
4. │ 5 │ A must-have for book lovers. │ 104 │ E-book Reader │
5. │ 4 │ Very good, but could be cheaper. │ 101 │ Laptop │
6. │ 4 │ Makes reading so convenient! │ 104 │ E-book Reader │
7. │ 3 │ Battery life could be better. │ 102 │ Smartphone │
8. │ 2 │ Stopped working after a month. │ 103 │ Headphones │
└────────┴──────────────────────────────────┴─────┴───────────────┘
现在我们可以像对待列一样对待元组元素,这意味着我们可以传入 review
元组中的所有元素并格式化输出
SELECT format('{}: {} [{}]', review.*) AS r
FROM `products.json`
ARRAY JOIN products AS product
ARRAY JOIN product.reviews AS review;
┌─r──────────────────────────────────────────┐
1. │ Excellent product!: 5 [1001] │
2. │ Very good, but could be cheaper.: 4 [1002] │
3. │ Best phone I've ever had.: 5 [2001] │
4. │ Battery life could be better.: 3 [2002] │
5. │ Great sound quality!: 5 [3001] │
6. │ Stopped working after a month.: 2 [3002] │
7. │ Makes reading so convenient!: 4 [4001] │
8. │ A must-have for book lovers.: 5 [4002] │
└────────────────────────────────────────────┘
我们还可以为 lambda 函数创建别名
WITH x -> round(x * 1.2, 2) AS addTax
SELECT
round(randUniform(8, 20), 2) AS amount,
addTax(amount)
FROM numbers(5)
┌─amount─┬─addTax(amount)─┐
1. │ 15.76 │ 18.91 │
2. │ 19.27 │ 23.12 │
3. │ 8.45 │ 10.14 │
4. │ 9.46 │ 11.35 │
5. │ 13.02 │ 15.62 │
└────────┴────────────────┘
还有其他一些改进,您可以在演示文稿幻灯片中了解。
从远程磁盘附加分区
由 Unalian 贡献
从另一个磁盘附加表 是 ClickHouse 中已存在一段时间的功能,但在查看下一个功能之前,让我们快速回顾一下它的工作原理。
ClickHouse/web-tables-demo 存储库包含英国房价数据集的数据库文件。我们可以通过运行以下命令来附加该表
ATTACH TABLE uk_price_paid_web UUID 'cf712b4f-2ca8-435c-ac23-c4393efe52f7'
(
price UInt32,
date Date,
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
type Enum8('other' = 0, 'terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4),
is_new UInt8,
duration Enum8('unknown' = 0, 'freehold' = 1, 'leasehold' = 2),
addr1 String,
addr2 String,
street LowCardinality(String),
locality LowCardinality(String),
town LowCardinality(String),
district LowCardinality(String),
county LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2)
SETTINGS disk = disk(type = web, endpoint = 'https://raw.githubusercontent.com/ClickHouse/web-tables-demo/main/web/');
UUID 需要保持不变,否则无法正确附加。运行 ATTACH TABLE
命令时不会看到错误,但结果查询将不会返回任何结果;
然后,我们可以运行以下查询来查找过去 10 年中按年份分组的平均房价
SELECT
toYear(date) AS year,
round(avg(price)) AS price,
bar(price, 0, 1000000, 80)
FROM uk_price_paid_web
GROUP BY year
ORDER BY year ASC;
┌─year─┬──price─┬─bar(price, 0, 1000000, 80)──────┐
1. │ 2022 │ 387415 │ ██████████████████████████████▉ │
2. │ 2021 │ 382166 │ ██████████████████████████████▌ │
3. │ 2020 │ 376855 │ ██████████████████████████████▏ │
4. │ 2019 │ 352562 │ ████████████████████████████▏ │
5. │ 2018 │ 350913 │ ████████████████████████████ │
6. │ 2017 │ 346486 │ ███████████████████████████▋ │
7. │ 2016 │ 313543 │ █████████████████████████ │
8. │ 2015 │ 297282 │ ███████████████████████▊ │
9. │ 2014 │ 280029 │ ██████████████████████▍ │
10. │ 2013 │ 256928 │ ████████████████████▌ │
└──────┴────────┴─────────────────────────────────┘
10 rows in set. Elapsed: 3.249 sec. Processed 27.40 million rows, 164.42 MB (8.43 million rows/s., 50.60 MB/s.)
Peak memory usage: 209.20 MiB.
此查询花费的时间是运行它的机器的互联网带宽速度的函数 - 数据未存储在本地,因此需要先下载数据,然后才能对其运行查询。
24.3 版本中更新了 ATTACH PARTITION
命令,允许您从不同的/远程磁盘附加数据,这使得将数据库从 GitHub 复制到我们的机器变得很容易。首先,让我们创建一个具有相同架构的另一个表
CREATE TABLE uk_price_paid_local
(
price UInt32,
date Date,
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
type Enum8('other' = 0, 'terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4),
is_new UInt8,
duration Enum8('unknown' = 0, 'freehold' = 1, 'leasehold' = 2),
addr1 String,
addr2 String,
street LowCardinality(String),
locality LowCardinality(String),
town LowCardinality(String),
district LowCardinality(String),
county LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2);
然后,我们可以将 uk_price_paid_web
中的数据附加到 uk_price_paid_local
中
ALTER TABLE uk_price_paid_local
ATTACH PARTITION () FROM uk_price_paid_web;
0 rows in set. Elapsed: 4.669 sec.
此命令将数据库文件从 GitHub 存储库复制到我们的机器上。这比执行 INSERT INTO...SELECT AS
查询快得多,后者需要先将数据从 GitHub 反序列化到内存数据结构中,然后再将其序列化回我们机器上的数据库文件。
现在,让我们对本地表运行查询以查找平均销售价格
SELECT
toYear(date) AS year,
round(avg(price)) AS price,
bar(price, 0, 1000000, 80)
FROM uk_price_paid_local
GROUP BY year
ORDER BY year DESC
LIMIT 10;
┌─year─┬──price─┬─bar(price, 0, 1000000, 80)──────┐
1. │ 2022 │ 387415 │ ██████████████████████████████▉ │
2. │ 2021 │ 382166 │ ██████████████████████████████▌ │
3. │ 2020 │ 376855 │ ██████████████████████████████▏ │
4. │ 2019 │ 352562 │ ████████████████████████████▏ │
5. │ 2018 │ 350913 │ ████████████████████████████ │
6. │ 2017 │ 346486 │ ███████████████████████████▋ │
7. │ 2016 │ 313543 │ █████████████████████████ │
8. │ 2015 │ 297282 │ ███████████████████████▊ │
9. │ 2014 │ 280029 │ ██████████████████████▍ │
10. │ 2013 │ 256928 │ ████████████████████▌ │
└──────┴────────┴─────────────────────────────────┘
10 rows in set. Elapsed: 0.045 sec.
快多了!
S3 Express 单区域支持
由 Nikita Taranov 贡献
2023 年 11 月,亚马逊宣布支持S3 Express 单区域存储类,旨在提供更低的延迟和更高的每秒读取次数,但成本高得多 (7 倍),并且可用性较低。
对这种新的存储类的支持需要更新 AWS/S3 库,从 24.3 版本开始,ClickHouse 支持读取和写入这些存储桶。您可以在S3 Express 文档中了解更多信息。
让我们看看如何查询 S3 Express 存储桶中的数据以及潜在的性能优势。
我们最近参加了万亿行挑战赛,我们需要查询分布在 S3 存储桶中 10 万个 Parquet 文件中的 1 万亿行数据。我们将所有这些文件复制到 S3 Express 存储桶中。
接下来,我们在与 S3 Express 存储桶相同的区域和可用区创建了一个 EC2 实例(这很重要,因为 Express 是特定于可用区的)。我们使用的是 c7gn.16xlarge 实例类型,其网络带宽为每秒 125 千兆字节 - 这应该足以满足我们的需求。
然后我们下载并安装了 ClickHouse,并在配置文件中添加了我们 S3 Express 存储桶的条目。
<s3>
<perf-bucket-url>
<endpoint>https://super-fast-clickhouse--use1-az4--x-s3.s3express-use1-az4.us-east-1.amazonaws.com</endpoint>
<region>us-east-1</region>
</perf-bucket-url>
</s3>
region
属性用于查询存储桶,如果您未指定此配置,则会收到以下错误。
Region should be explicitly specified for directory buckets
现在我们已经完成了设置,是时候看看查询了。首先,我们将统计所有文件中的记录数。由于我们查询的是 Parquet 文件,因此我们将能够从每个文件的 Parquet 元数据中计算结果,因此无需下载每个文件。
让我们在普通的 S3 存储桶上运行它。
SELECT count()
FROM s3('https://clickhouse-1trc.s3.amazonaws.com/1trc/measurements-*.parquet', '<key>', '<secret>') SETTINGS schema_inference_use_cache_for_s3=0;
1 row in set. Elapsed: 219.933 sec. Processed 1.00 trillion rows, 11.30 MB (4.55 billion rows/s., 51.36 KB/s.)
现在是 S3 Express。S3 Express API 不支持文件路径中的通配符表达式,因此我们将返回存储桶中的所有文件。这还会返回根目录,我们将通过编写 WHERE
子句将其删除(我们需要解决的问题)。
SELECT count()
FROM s3('https://super-fast-clickhouse--use1-az4--x-s3.s3express-use1-az4.us-east-1.amazonaws.com/1trc/*', '<key>', '<secret>', 'Parquet')
WHERE _file LIKE '%.parquet'
SETTINGS schema_inference_use_cache_for_s3 = 0
┌───────count()─┐
1. │ 1000000000000 │ -- 1.00 trillion
└───────────────┘
1 row in set. Elapsed: 29.544 sec. Processed 1.00 trillion rows, 0.00 B (33.85 billion rows/s., 0.00 B/s.)
Peak memory usage: 99.29 GiB.
我们运行了这些查询几次,并得到了类似的结果。对于此查询,我们看到查询性能提高了近 7 倍。
在 S3 Express 中存储数据的成本为每 GB 0.16 美元,而在普通存储桶中为每 GB 0.023 美元。因此,7 倍的性能提升伴随着 7 倍的存储成本增加。
让我们看看如果运行来自 1 万亿行挑战的实际查询会发生什么,该查询按站点对最小值、最大值和平均测量值进行分组。此查询要求我们下载所有 100,000 个文件,ClickHouse 将并行执行此操作。延迟对于此查询的重要性较低,因此我们不应该获得与计数查询一样多的性能提升。
从普通存储桶开始。
SELECT
station,
min(measure),
max(measure),
round(avg(measure), 2)
FROM s3('https://clickhouse-1trc.s3.amazonaws.com/1trc/measurements-*.parquet', '', '')
GROUP BY station
ORDER BY station ASC
FORMAT `Null`
0 rows in set. Elapsed: 3087.855 sec. Processed 1.00 trillion rows, 2.51 TB (323.85 million rows/s., 813.71 MB/s.)
Peak memory usage: 98.87 GiB.
这仅仅超过了 51 分钟。那么 S3 Express 呢?
SELECT
station,
min(measure),
max(measure),
round(avg(measure), 2)
FROM s3('https://super-fast-clickhouse--use1-az4--x-s3.s3express-use1-az4.us-east-1.amazonaws.com/1trc/*', '', '', 'Parquet')
WHERE _file LIKE '%.parquet'
GROUP BY station
ORDER BY station ASC
FORMAT `Null`
SETTINGS schema_inference_use_cache_for_s3 = 0
0 rows in set. Elapsed: 1227.979 sec. Processed 1.00 trillion rows, 2.51 TB (814.35 million rows/s., 2.05 GB/s.)
Peak memory usage: 98.90 GiB.
这仅仅超过了 20 分钟,因此这次的改进大约是 2.5 倍。
总之,S3 Express 以等量的价格提升提供了显着的查询延迟降低。这在存在许多小文件并且延迟主导查询时间的情况下往往很重要。在必须下载较大的文件并且查询中存在足够的并行性的情况下,Express 层的好处可能不那么明显。