又一个月过去了,这意味着又到了发布新版本的时候了!
ClickHouse 24.7 版本包含 **18 项新功能** 🎁 **12 项性能优化** 🛷 **76 项错误修复** 🐛
新贡献者
像往常一样,我们向所有在 24.7 版本中做出贡献的新贡献者表示热烈欢迎!ClickHouse 的受欢迎程度在很大程度上归功于社区的努力。看到社区不断壮大,我们总是感到谦卑。
以下是新贡献者的姓名
0x01f, AntiTopQuark, Daniel Anugerah, Elena Torró Martínez, Filipp Bakanov, Gosha Letov, Guspan Tanadi, Haydn, Kevin Song, Linh Giang, Maksim Galkin, Max K., Nathan Clevenger, Rodolphe Dugé de Bernonville, Tobias Florek, Yinzuo Jiang, Your Name, Zawa-II, cw5121, gabrielmcg44, gun9nir, jiaosenvip, jwoodhead, max-vostrikov, maxvostrikov, nauu, 忒休斯~Theseus
提示:如果您好奇我们如何生成此列表…… 点击此处.
您也可以 查看演示文稿的幻灯片.
按顺序读取的优化
由 Anton Popov 贡献
从表中读取数据时,ClickHouse 默认会应用一些优化。其中一项优化是 optimize_read_in_order:如果查询的 ORDER BY 列构成表的 primary key 的前缀,或者在 完全排序合并连接 中,一个或两个连接表的物理行顺序与连接键排序顺序匹配,则可以按磁盘顺序读取数据,并可以跳过排序操作。这通常也有利于内存使用。由于没有进行完整的内存排序,因此需要更少的内存。此外,当查询使用 LIMIT 子句时,短路 是可能的。
optimize_read_in_order
优化可以防止数据重新排序,但会降低读取表数据的并行度。 通常,表数据会被分成不重叠的范围,并由 N
个线程并行读取 (流式传输) 到查询引擎中,以进行进一步处理 (N 由 max_threads 设置控制)。
下图显示了为什么这种方法不适用于 optimize_read_in_order
优化
在上图中,我们描绘了一些属于表的 data part 的数据部分,这些 data part 的行在磁盘上按 CounterID
列排序(按 data part)。我们展示了 查询管道(物理执行计划)执行一个包含与表中物理磁盘行顺序匹配的 ORDER BY
子句的查询。因此,无需对数据进行重新排序。相反,来自表的 data part 的已排序(局部)行通过在 data part 之间以及 data part 内进行交错线性扫描来合并。这意味着数据不是并发流式传输的,而是顺序流式传输的。
ClickHouse 24.7 现在引入了在合并步骤之前对表 part 的数据进行缓冲,由 read_in_order_use_buffering
设置控制(默认启用)。它会增加内存使用量,但也会增加查询执行的并行度,因为它允许在合并数据之前并发地将数据流式传输到缓冲区,然后将数据合并到最终结果中
如果查询使用高选择性过滤器(这会极大地减少流式传输和缓冲的数据量),这会将应用了 optimize_read_in_order
优化的查询性能提高多达 10 倍。
让我们借助 匿名网页分析数据数据集 来实际操作,该数据集包含 1 亿条匿名页面点击量。 ClickBench 也使用此数据集,我们可以使用 提供的 DDL 语句 创建表。
请注意,该表具有以下 primary key
PRIMARY KEY (CounterID, EventDate, UserID, EventTime, WatchID)
接下来,我们插入数据
INSERT INTO hits SELECT * FROM s3('s3://clickhouse-public-datasets/hits_compatible/hits.parquet');
我们将在运行在 AWS c6a.8xlarge 实例上的 ClickHouse 实例上加载此数据集。
让我们首先编写一个不使用新缓冲方法的查询(请注意,我们禁用了文件系统缓存,以便更好地比较此查询和下一个查询的运行结果)
SELECT
CounterID,
UserID
FROM hits_100m_obfuscated
WHERE RegionID = 2
ORDER BY CounterID ASC
FORMAT `Null`
SETTINGS enable_filesystem_cache = 0,
read_in_order_use_buffering = 0
0 rows in set. Elapsed: 0.590 sec. Processed 100.00 million rows, 1.58 GB (169.48 million rows/s., 2.67 GB/s.)
Peak memory usage: 17.82 MiB.
此查询大约需要 0.6 秒,并使用 18 MB 内存。现在,让我们启用缓冲
SELECT
CounterID,
UserID
FROM hits_100m_obfuscated
WHERE RegionID = 2
ORDER BY CounterID ASC
FORMAT `Null`
SETTINGS enable_filesystem_cache = 0,
read_in_order_use_buffering = 1
0 rows in set. Elapsed: 0.097 sec. Processed 100.00 million rows, 1.58 GB (1.04 billion rows/s., 16.35 GB/s.)
Peak memory usage: 48.37 MiB.
查询时间缩短到 0.1 秒以下,内存使用量为 48 MB。因此,缓冲功能将此查询的运行速度提高了 5 倍,同时使用了 3 倍的内存。
更快的并行哈希连接
由 Nikita Taranov 贡献
每个 ClickHouse 版本都包含 JOIN 方面的改进。
在本版本中,我们改进了 哈希表 的分配,用于并行哈希连接算法。
提醒一下,并行哈希连接 算法是哈希连接的一种变体,它会将输入数据分成多个部分以并发构建多个哈希表,从而以更高的内存开销为代价加快构建阶段。
默认哈希连接算法需要 更少 的内存,因为它使用单个哈希表来 填充 来自 JOIN 右侧表的 data。为此,右侧表的 data 可以被分割并 由多个线程并行读取,但只有一个线程可以将此 data 填充到哈希表中,因为哈希表对于插入操作不是 线程安全的。如果右侧表很大,这可能会成为 JOIN 运行时的瓶颈。
下图显示了并行哈希连接算法如何克服此瓶颈
在算法的 ① 构建阶段,来自右表的 data 被分割并由 N
个流并发流式传输(N 由 max_threads 设置控制)以并发填充 N
个哈希表。每个流中的行通过对每行的连接键应用哈希函数来路由到 N
个哈希表中的一个。用于将行分割到哈希表中的哈希函数与哈希表内部使用的哈希函数不同。
在算法的 ② 探测阶段,来自左表的 data 被分割并由 N
个流并发流式传输(同样,N 由 max_threads
设置控制)。步骤 ① 中的相同“桶哈希函数”被应用于每行的连接键以确定相应的哈希表,并通过对相应哈希表进行查找来连接行。
为了避免浪费内存,哈希表会预先分配一个有限的初始大小。如果一个表已满,则会分配一个新的更大的哈希表,将之前表中的 data 复制到新表中,然后释放旧表。新哈希表的大小始终通过使用内部乘数来增加之前表的大小来确定。
在 ClickHouse 24.7 中,当并行哈希连接完成时,现在会收集和缓存哈希表的最终大小(使用 JOIN 的右表名称和用于查找键的连接列名称作为输入)。在随后的查询执行中,哈希表会根据之前查询运行中记住的大小进行预分配。通过避免冗余的中间调整大小步骤,这可以节省时间。下图可视化了这一点
让我们用一个具体的例子来演示这一点。
我们使用 ClickBench 表,它包含 1 亿个匿名页面点击。
首先,我们通过运行 提供的 DDL 语句 来创建该表。接下来,我们插入数据
INSERT INTO hits SELECT * FROM s3('s3://clickhouse-public-datasets/hits_compatible/hits.parquet');
数据加载完成后,该表应包含约 1 亿行
SELECT count()
FROM hits;
┌──count()─┐
1. │ 99997497 │ -- 100.00 million
└──────────┘
我们运行一个执行自连接的 JOIN 查询。请注意,此 JOIN 使用 默认 的哈希连接算法
SELECT count()
FROM hits AS t1
INNER JOIN hits AS t2 ON t1.ClientIP = t2.RemoteIP
WHERE t1.ClientIP != 0;
┌─count()─┐
1. │ 3395861 │ -- 3.40 million
└─────────┘
1 row in set. Elapsed: 5.112 sec. Processed 199.99 million rows, 799.98 MB (39.12 million rows/s., 156.49 MB/s.)
Peak memory usage: 3.25 GiB.
现在,我们使用并行哈希连接算法运行相同的 JOIN 查询
SELECT count()
FROM hits AS t1
INNER JOIN hits AS t2 ON t1.ClientIP = t2.RemoteIP
WHERE t1.ClientIP != 0
SETTINGS join_algorithm = 'parallel_hash';
┌─count()─┐
1. │ 3395861 │ -- 3.40 million
└─────────┘
1 row in set. Elapsed: 0.517 sec. Processed 199.99 million rows, 799.98 MB (387.03 million rows/s., 1.55 GB/s.)
Peak memory usage: 3.44 GiB.
请注意,此查询在同一硬件上运行速度快了 10 倍。
当我们首次(自 ClickHouse 进程启动以来)使用并行哈希连接算法和启用的跟踪级别日志记录运行查询时,我们可以观察到 ClickHouse 正在收集和缓存哈希表大小统计信息的日志消息
SELECT count()
FROM hits AS t1
INNER JOIN hits AS t2 ON t1.ClientIP = t2.RemoteIP
WHERE t1.ClientIP != 0
SETTINGS
join_algorithm = 'parallel_hash',
send_logs_level = 'trace';
...
<Trace> HashTablesStatistics: Statistics updated for key=18113390195926062714: new sum_of_sizes=9594872, median_size=149909
...
当我们重新运行查询(期间没有重启 ClickHouse 进程)且启用了跟踪级别日志记录时,我们可以观察到 ClickHouse 在预分配之前如何访问缓存的哈希表统计信息。
SELECT count()
FROM hits AS t1
INNER JOIN hits AS t2 ON t1.ClientIP = t2.RemoteIP
WHERE t1.ClientIP != 0
SETTINGS
join_algorithm = 'parallel_hash',
send_logs_level = 'trace';
...
<Trace> HashTablesStatistics: An entry for key=18113390195926062714 found in cache: sum_of_sizes=9594872, median_size=149909
...
ASOF JOIN 的合并连接算法
由 Vladimir Cherkasov 贡献
ClickHouse 是第一个在 2019 年 4 月引入 ASOF JOIN 的 SQL DBMS。
提醒一下,ASOF JOIN 提供非精确匹配功能。如果左表中的某一行在右表中没有精确匹配项,则会使用右表中最接近的匹配行作为匹配项。
这对时间序列分析特别有用,可以大幅降低查询复杂度。
此外,当 ClickHouse 用作机器学习的特征存储 时,ASOF JOIN 很方便。它允许 轻松组合特征 以产生一组特征向量。
到目前为止,ASOF JOIN 仅支持内存绑定的 哈希 和 并行哈希 连接算法。
自 ClickHouse 24.7 以来,ASOF JOIN 也适用于非内存绑定的 完全排序合并 连接算法。提醒一下,该算法的连接策略要求先按连接键对连接数据进行排序,然后才能通过交错的线性扫描和对来自两个表的行块的排序流进行合并来识别连接匹配项
完全排序合并连接可以利用一个或两个表的物理行顺序,从而允许跳过排序(以及从 按顺序读取的新优化 中获益,如上面某个部分所述)。在这种情况下,连接性能可能与哈希连接算法相媲美,但通常需要的内存要少得多。否则,完全排序合并连接需要在识别连接匹配项之前完全对表的行进行排序。排序可以在内存中进行,内存使用量通常 与连接表的的大小无关。
让我们看一个具体的例子。
假设我们正在 hits
表中跟踪互联网用户的网站点击事件,在 sessions
表中跟踪用户会话。然后,我们可以使用 ASOF JOIN 简洁地制定一个查询,以查找每个会话的第一个用户点击事件
SELECT ...
FROM hits h ASOF JOIN sessions s
ON h.UserID = s.UserID AND h.EventTime > s.StartTime;
我们使用 hits
表模拟 sessions
表,并将上面的查询重新表述为自 ASOF JOIN
SELECT ...
FROM hits t1 ASOF JOIN hits t2
ON t1.UserID = t2.UserID AND t1.EventTime < t2.EventTime;
要运行此查询,我们使用 由 ClickBench 提供的 DDL 语句 创建 hits
表。
接下来,我们插入数据(1 亿行)
INSERT INTO hits SELECT * FROM s3('s3://clickhouse-public-datasets/hits_compatible/hits.parquet');
现在我们已准备好使用默认的哈希连接算法运行上面的 ASOF 连接查询
SELECT count()
FROM hits t1 ASOF JOIN hits t2
ON t1.UserID = t2.UserID AND t1.EventTime < t2.EventTime;
┌──count()─┐
1. │ 81878322 │ -- 81.88 million
└──────────┘
1 row in set. Elapsed: 11.849 sec. Processed 199.99 million rows, 2.40 GB (16.88 million rows/s., 202.55 MB/s.)
Peak memory usage: 6.49 GiB.
让我们使用 完全排序合并连接
算法运行相同的查询
SELECT count()
FROM hits t1 ASOF JOIN hits t2
ON t1.UserID = t2.UserID AND t1.EventTime < t2.EventTime
SETTINGS
join_algorithm = 'full_sorting_merge';
┌──count()─┐
1. │ 81878322 │ -- 81.88 million
└──────────┘
1 row in set. Elapsed: 5.041 sec. Processed 199.99 million rows, 2.40 GB (39.68 million rows/s., 476.11 MB/s.)
Peak memory usage: 2.41 GiB.
如您所见,对于我们的特定数据集,由 完全排序合并连接
算法实现的 ASOF 连接运行速度快了两倍以上,并且消耗的峰值主内存比使用 哈希连接
算法运行的 ASOF 连接少了两倍以上。但是,这不是一个普遍的规则,最好使用您的特定数据集进行测试。
percent_rank
由 lgbo-ustc 贡献
percent_rank
返回窗口分区内行的相对排名(即百分位数)。
让我们借助足球运动员工资的合成数据集来看看它是如何工作的。我们将创建一个名为 salaries
的表
CREATE TABLE salaries
ORDER BY team AS
SELECT *
FROM url(
'https://raw.githubusercontent.com/ClickHouse/examples/main/LearnClickHouseWithMark/WindowFunctions-Ranking/data/salaries.csv'
)
SETTINGS schema_inference_make_columns_nullable=0;
然后,我们可以计算这些工资的 rank
和 percent_rank
SELECT
team, player, weeklySalary AS salary, position AS pos,
rank() OVER (ORDER BY salary DESC) AS rank,
round(percent_rank() OVER (ORDER BY salary DESC), 6) AS percentRank
FROM salaries
ORDER BY salary DESC
LIMIT 10
┌─team─────────────────────────┬─player──────────┬─salary─┬─pos─┬─rank─┬─percentRank─┐
│ North Pamela Trojans │ Robert Griffin │ 399999 │ GK │ 1 │ 0 │
│ Jimmyville Legionnaires │ Nathan Thompson │ 399998 │ D │ 2 │ 0.000004 │
│ Stephaniemouth Trojans │ Benjamin Cline │ 399998 │ D │ 2 │ 0.000004 │
│ Maryhaven Generals │ Scott Chavez │ 399998 │ M │ 2 │ 0.000004 │
│ Michaelborough Rogues │ Dan Conner │ 399998 │ M │ 2 │ 0.000004 │
│ Nobleview Sages │ William Rubio │ 399997 │ M │ 6 │ 0.00002 │
│ North Christinaview Archers │ Robert Cook │ 399991 │ M │ 7 │ 0.000024 │
│ North Krystal Knights-Errant │ Juan Bird │ 399986 │ GK │ 8 │ 0.000028 │
│ Claireberg Vikings │ Benjamin Taylor │ 399985 │ M │ 9 │ 0.000032 │
│ Andreaberg Necromancers │ John Lewis │ 399985 │ D │ 9 │ 0.000032 │
└──────────────────────────────┴─────────────────┴────────┴─────┴──────┴─────────────┘
自动命名元组
由 Amos Bird 贡献
如果您为元组元素指定别名(使用 AS
),则会创建一个命名元组。让我们看看它在 24.6 中是如何工作的
docker run --rm clickhouse/clickhouse-server:24.6 \
clickhouse-local \
--query \
"SELECT ('Hello' AS a, 123 AS b) AS x,
toTypeName(x) AS type,
toJSONString(x) AS json
FORMAT Vertical"
Row 1:
──────
x: ('Hello',123)
type: Tuple(String, UInt8)
json: ["Hello",123]
现在,24.7
docker run --rm clickhouse/clickhouse-server:24.7 \
clickhouse-local \
--query \
"SELECT ('Hello' AS a, 123 AS b) AS x,
toTypeName(x) AS type,
toJSONString(x) AS json
FORMAT Vertical"
Row 1:
──────
x: ('Hello',123)
type: Tuple(
a String,
b UInt8)
json: {"a":"Hello","b":123}