北半球正值春天,又到了 ClickHouse 发布新版本的时候了。
ClickHouse 24.3 版本包含 12 个新功能 🎁 18 项性能优化 🛷 60 个错误修复 🐛
新贡献者
像往常一样,我们特别欢迎 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 版本开始,此功能为 Beta 版并默认启用。
如果您不想使用它,您仍然可以通过配置以下设置来禁用它
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 │
└────────┴────────────────┘
还有许多其他改进,您可以在演示文稿幻灯片中阅读相关内容。
从远程磁盘 ATTACH PARTITION
贡献者: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.
此查询所需的时间取决于您运行它的机器的互联网带宽速度 - 数据未存储在本地,因此需要先拉取数据,然后才能对其运行查询。
`ATTACH PARTITION` 命令已在 24.3 版本中更新,允许您从不同的/远程磁盘附加数据,这使得从 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 One Zone 存储类,该存储类旨在提供更低的延迟和更高的每秒读取次数,但 成本更高(7 倍),可用性更低。
对这种新存储类的支持需要更新 AWS/S3 库,并且从 24.3 版本开始,ClickHouse 支持从这些存储桶读取和写入数据。您可以在 S3 Express 文档中了解更多信息。
让我们看一下如何在 S3 Express 存储桶中查询数据以及潜在的性能优势。
我们最近参加了 1 万亿行挑战赛,在其中我们必须查询分布在 S3 存储桶中 10 万个 Parquet 文件中的 1 万亿行数据。我们将所有这些文件复制到了 S3 Express 存储桶中。
接下来,我们在与 S3 Express 存储桶相同的区域和可用区中创建了一个 EC2 实例(这一点很重要,因为 express 是 AZ 特定的)。我们使用 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 不支持文件路径中的 glob 表达式,因此我们将返回存储桶中的所有文件。这也返回了根目录,我们将通过编写 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 万亿行挑战赛中的实际查询会怎么样,该查询计算按站点分组的最小、最大和平均测量值。此查询要求我们下载所有 10 万个文件,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 层的好处可能不太明显。