0
126
0
0
首页/专栏/ 版本发布/ 查看内容

ClickHouse 24.8 LTS 版本发布说明

 admin   发表于  2024-10-15 14:50
专栏 版本发布

时间飞逝,又到了新版本发布的时刻

发布概要

本次ClickHouse 24.8 版本包含了19个新功能🎁、18性能优化🛷、65个bug修复🐛


这个版本是长期支持 (LTS) 版本,发布后将获得长达 12 个月的支持。如果你想了解稳定版本和 LTS 版本的区别,可以查阅文档。

本次更新包括全新升级的 JSON 类型、针对时间序列数据的表引擎、Kafka 消息的精准一次性处理,当然也少不了对表连接操作的优化!


新贡献者

一如既往,我们热烈欢迎 24.9 版本中的所有新贡献者!ClickHouse 能有今天的流行,离不开社区的辛勤付出。看到这个社区日益壮大,真的让人倍感自豪。

以下是新贡献者名单:

Graham Campbell, Halersson Paris, Jacob Reckhard, Lennard Eijsackers, Miсhael Stetsyuk, Peter Nguyen, Ruihang Xia, Samuele Guerrini, Sasha Sheikin, Vladimir Varankin, Zhukova, Maria, heguangnan, khodyrevyurii, sakulali, shiyer7474, xc0derx, zoomxi

JSON 数据类型

由 Pavel Kruglov 贡献

本次发布中,我们实验性地引入了全新的 JSON 数据类型。这个功能开发已久,之前版本的发布中,我们也提到过它依赖的几种类型——Variable 和 Dynamic。

JSON 数据类型专为存储半结构化数据而设计,适用于每行数据结构可能不同或不希望将其拆分为单独列的情况。

我们将使用 StatsBomb 提供的足球数据集来试验这个功能。数据集包括比赛、阵容和事件信息。

对于此次实验,最有趣的数据是事件信息。事件可能包括传球、解围、封堵等比赛中发生的各种行为。虽然相同类型的事件具有相同的结构,但不同类型事件的字段却不尽相同。

如果你希望亲自操作,可以通过运行以下命令下载数据集:

wget https://github.com/statsbomb/open-data/archive/refs/heads/master.zip

接下来,快速查看我们将要处理的数据。我们会使用 JsonAsObject (对象化 JSON) 格式读取数据,这样 ClickHouse 就不会尝试推断 JSON 中各属性的类型。

SELECT    replaceRegexpAll(splitByRegexp('/', _file)[-1], '.json', '') AS matchId,    jsonFROM file('master.zip :: **/data/events/*.json', JSONAsObject)LIMIT 1FORMAT Vertical
Row 1:──────matchId: 15946json: {"duration":0,"id":"9f6e2ecf-6685-45df-a62e-c2db3090f6c1","index":"1","minute":"0","period":"1","play_pattern":{"id":"1","name":"Regular Play"},"possession":"1","possession_team":{"id":"217","name":"Barcelona"},"second":"0","tactics":{"formation":"442","lineup":[{"jersey_number":"1","player":{"id":"20055","name":"Marc-André ter Stegen"},"position":{"id":"1","name":"Goalkeeper"}},{"jersey_number":"2","player":{"id":"6374","name":"Nélson Cabral Semedo"},"position":{"id":"2","name":"Right Back"}},{"jersey_number":"3","player":{"id":"5213","name":"Gerard Piqué Bernabéu"},"position":{"id":"3","name":"Right Center Back"}},{"jersey_number":"23","player":{"id":"5492","name":"Samuel Yves Umtiti"},"position":{"id":"5","name":"Left Center Back"}},{"jersey_number":"18","player":{"id":"5211","name":"Jordi Alba Ramos"},"position":{"id":"6","name":"Left Back"}},{"jersey_number":"5","player":{"id":"5203","name":"Sergio Busquets i Burgos"},"position":{"id":"9","name":"Right Defensive Midfield"}},{"jersey_number":"4","player":{"id":"5470","name":"Ivan Rakitić"},"position":{"id":"11","name":"Left Defensive Midfield"}},{"jersey_number":"20","player":{"id":"6379","name":"Sergi Roberto Carnicer"},"position":{"id":"12","name":"Right Midfield"}},{"jersey_number":"11","player":{"id":"5477","name":"Ousmane Dembélé"},"position":{"id":"16","name":"Left Midfield"}},{"jersey_number":"9","player":{"id":"5246","name":"Luis Alberto Suárez Díaz"},"position":{"id":"22","name":"Right Center Forward"}},{"jersey_number":"10","player":{"id":"5503","name":"Lionel Andrés Messi Cuccittini"},"position":{"id":"24","name":"Left Center Forward"}}]},"team":{"id":"217","name":"Barcelona"},"timestamp":"00:00:00.000","type":{"id":"35","name":"Starting XI"}}

这行数据代表了一场比赛的首发阵容事件,但数据集中还包含数百种其他事件类型。接下来我们看看如何将这些数据加载到 ClickHouse 中。

要使用 JSON 数据类型,需要启用以下配置:

SET allow_experimental_json_type=1;

接着,我们将创建一个事件表。该表包含一个存储事件 JSON 数据的 `json` 列,以及一个 matchId 列,用于存储从文件名中提取的比赛 ID。

在创建 ClickHouse 表时,我们需要定义一个排序键,它决定数据在磁盘上的排序方式。此处我们使用 possession_team.id 作为排序键。

目前尚不支持将嵌套字段作为排序键,不过该功能已计划在未来版本中推出。暂时,我们可以创建一个 MATERIALIZED 列,从 JSON 列中提取相应的值,并将其用作排序键。以下是创建表的查询语句:

CREATE TABLE events(    matchId String,    json JSON,    possession_team_id String MATERIALIZED getSubcolumn(json, 'possession_team.id'))ENGINE = MergeTreeORDER BY possession_team_id;

接下来,我们可以复制之前的 SELECT 查询,并在其前面添加 INSERT INTO events 语句来加载数据:

INSERT INTO events SELECT    replaceRegexpAll(splitByRegexp('/', _file)[-1], '.json', '') AS matchId,    jsonFROM file('master.zip :: **/data/events/*.json', JSONAsObject)
0 rows in set. Elapsed: 72.967 sec. Processed 12.08 million rows, 10.39 GB (165.60 thousand rows/s., 142.42 MB/s.)Peak memory usage: 3.52 GiB.

加载 1200 万条事件数据大约需要一分钟时间。之后,我们可以编写一个使用 JSON 点语法的查询来找出最常见的事件类型:

SELECT    json.type.name,    count() AS countFROM eventsGROUP BY ALLORDER BY count DESCLIMIT 10
┌─json.type.name─┬───count─┐│ Pass │ 3358652│ Ball Receipt* │ 3142664│ Carry │ 2609610│ Pressure │ 1102075│ Ball Recovery363161│ Duel │ 255791│ Clearance │ 157713Block130858│ Dribble │ 121105│ Goal Keeper │ 105390└────────────────┴─────────┘

点语法在读取字面值时非常方便,但它并不适用于读取子对象。例如,下面的查询尝试统计最常见的 possession_team

SELECT    json.possession_team AS team,    count()FROM eventsGROUP BY teamORDER BY count() DESCLIMIT 10
┌─team─┬──count()─┐│ ᴺᵁᴸᴸ │ 12083338└──────┴──────────┘

结果显示,所有值都为 null!

点语法由于性能原因,不会读取嵌套对象。数据存储方式使得通过路径读取字面值非常高效,但如果要读取所有子对象,数据量会增加,速度可能变慢。

如果我们需要返回一个对象,则需要使用 .^ 语法。这种特殊语法专用于从 JSON 数据类型字段中读取嵌套对象:

SELECT    json.^possession_team AS team,    count()FROM eventsGROUP BY teamORDER BY count() DESCLIMIT 10
┌─team──────────────────────────────────────┬─count()─┐│ {"id":"217","name":"Barcelona"} │ 1326515│ {"id":"131","name":"Paris Saint-Germain"} │ 239930│ {"id":"1","name":"Arsenal"} │ 154789│ {"id":"904","name":"Bayer Leverkusen"} │ 147473│ {"id":"220","name":"Real Madrid"} │ 135421│ {"id":"968","name":"Arsenal WFC"} │ 131637│ {"id":"746","name":"Manchester City WFC"} │ 131017│ {"id":"971","name":"Chelsea FCW"} │ 115761│ {"id":"212","name":"Atlético Madrid"} │ 110893│ {"id":"169","name":"Bayern Munich"} │ 104804└───────────────────────────────────────────┴─────────┘

在读取多层嵌套的子对象时,若只想返回某个子对象,只需在路径中的第一个对象使用 .^ 语法。例如:

select json.^pass.body_part AS x, toTypeName(x)FROM events LIMIT 1;
┌─x───────────────────────────────┬─toTypeName(x)─┐│ {"id":"40","name":"Right Foot"} │ JSON└─────────────────────────────────┴───────────────┘

这种语法仅限于返回对象。如果试图使用它返回字面值,将得到一个空的 JSON 对象:

SELECT    json.^possession_team.name AS team, toTypeName(team),    count()FROM eventsGROUP BY teamORDER BY count() DESCLIMIT 10;
┌─team─┬─toTypeName(team)─┬──count()─┐ {} │ JSON │ 12083338 │└──────┴──────────────────┴──────────┘

我们计划添加一个新操作符 .^$,它可以同时返回字面值和子对象,作为单一子列。

当返回字面值时,它们的类型将为 Dynamic。可以使用 dynamicType 函数来确定每个值的实际底层类型:

SELECT    json.possession_team.name AS team,    dynamicType(team) AS teamType,    json.duration AS duration,    dynamicType(duration) AS durationTypeFROM eventsLIMIT 1
┌─team────┬─teamType─┬─duration─┬─durationType─┐│ Arsenal │ String0.657763 │ Float64 │└─────────┴──────────┴──────────┴──────────────┘

我们还可以使用 .:后缀来假定数据类型,并使用::进行类型转换。

SELECT    json.possession_team.name AS team,    toTypeName(team),    json.possession_team.name.:String AS teamAssume,    toTypeName(teamAssume) AS assumeType,    json.possession_team.name::String AS teamCast,    toTypeName(teamCast) AS castTypeFROM eventsLIMIT 1;
┌─team────┬─toTypeName(team)─┬─teamAssume─┬─assumeType───────┬─teamCast─┬─castType─┐│ Arsenal │ Dynamic │ Arsenal │ Nullable(String) │ Arsenal │ String │└─────────┴──────────────────┴────────────┴──────────────────┴──────────┴──────────┘

最后要注意的是,JSON 数据类型的列可以进一步配置。例如,如果希望排除存储中的某些 JSON 对象部分,可以通过在 JSON 路径中使用 SKIPSKIP REGEXP 跳过特定路径。

例如,以下创建表的语句跳过了 pass.body.part 路径以及任何以字母 t 开头的路径:

CREATE TABLE events2(    matchId String,    json JSON(      SKIP pass.body_part,       SKIP REGEXP 't.*'    ),    possession_team_id String MATERIALIZED getSubcolumn(json, 'possession_team.id'))ENGINE = MergeTreeORDER BY possession_team_id;

在将数据导入带有额外设置的 JSON 列的表时,ClickHouse 不会自动将传入的数据转换为正确的类型,不过这一问题将在未来的版本中得到修复。目前,我们需要在导入查询中显式定义 JSON 列的类型:

INSERT INTO events2SELECT    replaceRegexpAll(splitByRegexp('/', _file)[-1], '.json', '') AS matchId,    jsonFROM file(  'master.zip :: **/data/events/*.json',  JSONAsObject,  '`json` JSON(SKIP `pass.body_part`, SKIP REGEXP \'t.*\')');
0 rows in set. Elapsed: 75.122 sec. Processed 12.08 million rows, 10.39 GB (160.85 thousand rows/s., 138.33 MB/s.)Peak memory usage: 3.52 GiB.

如果我们查询 events2 表,会发现这些子路径已经不再存在:

SELECT json.^pass AS passFROM events3WHERE empty(pass) != trueLIMIT 3FORMAT Vertical;
Row 1:──────pass: {"angle":-3.1127546,"end_location":[49.6,39.7],"height":{"id":"1","name":"Ground Pass"},"length":10.404326,"recipient":{"id":"401732","name":"Jaclyn Katrina Demis Sawicki"},"type":{"id":"65","name":"Kick Off"}}
Row 2:──────pass: {"angle":2.9699645,"end_location":[28,44.2],"height":{"id":"1","name":"Ground Pass"},"length":22.835499,"recipient":{"id":"401737","name":"Hali Moriah Candido Long"}}
Row 3:──────pass: {"angle":-1.7185218,"end_location":[27.1,27.1],"height":{"id":"1","name":"Ground Pass"},"length":16.984993,"recipient":{"id":"389446","name":"Jessika Rebecca Macayan Cowart"}}

我们还可以为路径提供类型注释:

CREATE TABLE events3(    matchId String,    json JSON(      pass.height.name String,       pass.height.id Int64     ),    possession_team_id String MATERIALIZED getSubcolumn(json, 'possession_team.id'))ENGINE = MergeTreeORDER BY possession_team_id;

此外,还有一些额外设置,如 max_dynamic_pathsmax_dynamic_types。这些设置用于控制数据在磁盘上的存储方式。你可以在 JSON 数据类型的文档中了解更多详细信息【https://clickhouse.com/docs/en/sql-reference/data-types/newjson】


路过

雷人

握手

鲜花

鸡蛋

版权声明:本文为 clickhouse 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论