使用 ClickHouse 的 UDF 解决语义版本的需求

图片

本文字数:5775;估计阅读时间:15 分钟

作者:Juan S. Carrillo

本文在公众号【ClickHouseInc】首发

图片

我在 Embrace 工作,我们构建了唯一基于 OpenTelemetry 的用户中心移动应用可观测性解决方案,并使用 ClickHouse 为我们的时间序列分析产品提供支持。

应用版本是 Embrace 用户最重要的排序类别之一。应用版本通常采用语义版本控制(Semantic Versioning),版本格式为 <主版本>.<次版本>.<补丁版本>。按以下规则进行版本升级:

  1. 主版本号:进行不兼容的 API 更改时

  2. 次版本号:增加向后兼容的新功能时

  3. 补丁版本号:进行向后兼容的错误修复时

我们希望对应用版本排序时得到 2.1.0、2.1.2 和 2.1.10 这样的顺序,而不是按字典顺序排列得到的 2.1.0、2.1.10 和 2.1.2。

ClickHouse 并未直接提供语义版本排序支持。不过,从 ClickHouse v21.10 开始,我们可以通过用户定义函数(UDF)来解决这个问题。

我们最终实现的 UDF 如下。如果您有兴趣了解 UDF 的构建过程以及查询和逻辑改进,请继续阅读。

CREATE FUNCTION sortableSemVer AS version -> 
  arrayMap(
    x -> toUInt32OrZero(x), 
    splitByChar('.', extract(version, '(\\d+(\\.\\d+)+)'))
  )

将版本号转换为整数数组

数据库中版本通常以字符串形式存储,但按字典顺序排序无法得到预期的顺序。

SELECT *
FROM
(
    SELECT ['1.0', '2.0', '3.0.0', '10.0'] AS versions
)
ARRAY JOIN versions
ORDER BY versions DESC

┌─versions─┐
│ 3.0.0    │
│ 2.0      │
│ 10.0     │ << ???
│ 1.0      │
└──────────┘

我们可以将版本号转换为整数数组来解决此问题。将语义版本重写为整数数组后,排序将符合预期,且不同长度的版本也可以正常排序!

SELECT *
FROM
(
    SELECT [[1, 0], [2, 0], [3, 0, 0], [10, 0]] AS versions
)
ARRAY JOIN versions
ORDER BY versions DESC

┌─versions─┐
│ [10,0]   │
│ [3,0,0]  │
│ [2,0]    │
│ [1,0]    │
└──────────┘

可以用一个 lambda 函数,将版本字符串转换为整数数组。

SELECT
    version,
    arrayMap(x -> toUInt32(x), splitByChar('.', version)) AS sem_ver_arr
FROM
(
    SELECT ['1.0', '2.0', '3.0.0', '10.0'] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

┌─version─┬─sem_ver_arr─┐
│ 10.0    │ [10,0]      │
│ 3.0.0   │ [3,0,0]     │
│ 2.0     │ [2,0]       │
│ 1.0     │ [1,0]       │
└─────────┴─────────────┘

具体步骤如下:

  1. splitByChar('.', version) 将版本字符串按 . 分割为字符串数组,例如将 10.0 转换为 ['10', '0']。

  2. arrayMap(x -> toUInt32(x), arr) 将每个数字字符串转换为 int32 类型。

为了简化代码编写,可以通过定义一个 UDF 来实现。

CREATE FUNCTION sortableSemVer AS version -> 
  arrayMap(x -> toUInt32(x), splitByChar('.', version));

让我们试试看!

SELECT
    version,
    sortableSemVer(version) AS sem_ver_arr
FROM
(
    SELECT ['1.0', '2.0', '3.0.0', '10.0'] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

┌─version─┬─sem_ver_arr─┐
│ 10.0    │ [10,0]      │
│ 3.0.0   │ [3,0,0]     │
│ 2.0     │ [2,0]       │
│ 1.0     │ [1,0]       │
└─────────┴─────────────┘

实际上,您甚至可以完全省略 sem_ver_arr 列,仅在 ORDER BY 子句中使用 sortableSemVer。

SELECT version
FROM
(
    SELECT ['1.0', '2.0', '3.0.0', '10.0'] AS version
)
ARRAY JOIN version
ORDER BY sortableSemVer(version) DESC

┌─version─┐
│ 10.0    │
│ 3.0.0   │
│ 2.0     │
│ 1.0     │
└─────────┘

如果您的语义版本格式规范,可以直接使用这个函数。如果版本字符串是类似 my-app-1.2.3(456)-alpha-45dbbdf9ab 的格式,请继续阅读。

复杂格式的版本号

我们以一个简单的例子继续说明:1.2.3.production。由于 production 不是有效数字,之前的函数会在此处失效。

select arrayMap(x -> toUInt32(x), splitByChar('.', '1.2.3.production'));

Received exception from server (version 23.8.15):
Code: 6. DB::Exception: Received from localhost:9000. DB::Exception: Cannot parse string 'production' as UInt32: syntax error at begin of string. Note: there are toUInt32OrZero and toUInt32OrNull functions, which returns zero/NULL instead of throwing exception.: while executing 'FUNCTION toUInt32(x :: 0) -> toUInt32(x) UInt32 : 1': while executing 'FUNCTION arrayMap(__lambda :: 1, splitByChar('.', '1.2.3.production') :: 0) -> arrayMap(lambda(tuple(x), toUInt32(x)), splitByChar('.', '1.2.3.production')) Array(UInt32) : 2'. (CANNOT_PARSE_TEXT)

为了解决这个问题,可以用 toUInt32OrZero 替换 toUInt32,将非数字字符串默认转换为 0。这样,即使版本字符串完全不包含数字也能正常处理。

SELECT
    version,
    arrayMap(x -> toUInt32OrZero(x), splitByChar('.', version)) AS sem_ver_arr
FROM
(
    SELECT [
        '1.0', '2.0', '3.0.0', 
        '10.0', 'production', '1.2.3.production'
        ] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

┌─version──────────┬─sem_ver_arr─┐
│ 10.0             │ [10,0]      │
│ 3.0.0            │ [3,0,0]     │
│ 2.0              │ [2,0]       │
│ 1.2.3.production │ [1,2,3,0]   │
│ 1.0              │ [1,0]       │
│ production       │ [0]         │
└──────────────────┴─────────────┘

如果版本号为 1.2.3-production,使用 . 分割会导致丢失补丁版本。我们可以利用正则表达式和 extract 函数来提取任何符合语义版本格式的内容,这样可以从字符串开头获取语义版本。

SELECT extract('1.2.3-production', '^\\d+\\.\\d+\\.\\d+')

┌─extract('1.2.3-production', '^\\d+\\.\\d+\\.\\d+')─┐
│ 1.2.3                                              │
└────────────────────────────────────────────────────┘

我们还可以进一步调整正则表达式,以支持字符串中其他位置的语义版本。

SELECT extract('my-app1.2.3-production', '\\d+\\.\\d+\\.\\d+')

┌─extract('my-app1.2.3-production', '\\d+\\.\\d+\\.\\d+')─┐
│ 1.2.3                                                   │
└─────────────────────────────────────────────────────────┘

再进一步修改,让正则表达式支持包含两个或更多子部分的语义版本。

SELECT extract('1.2.3.4.5.6.7-production', '(\\d+(\\.\\d+)+)')

┌─extract('1.2.3.4.5.6.7-production', '(\\d+(\\.\\d+)+)')─┐
│ 1.2.3.4.5.6.7                                           │
└─────────────────────────────────────────────────────────┘

我们将整个正则表达式用括号包裹,以捕获完整版本号而不是仅重复的某个部分,否则只能捕获正则表达式的最后一段。

SELECT extract('1.2.3.4.5.6.7-production', '\\d+(\\.\\d+)+')

┌─extract('1.2.3.4.5.6.7-production', '\\d+(\\.\\d+)+')─┐
│ .7                                                    │
└───────────────────────────────────────────────────────┘

^--- Where did the rest of it go?!?

让我们更新原有的 UDF,增加正则表达式支持!

--- Drop the previous definition
DROP FUNCTION IF EXISTS sortableSemVer;

--- Create the new definition
CREATE FUNCTION sortableSemVer AS version -> 
  arrayMap(
    x -> toUInt32OrZero(x), 
    splitByChar('.', extract(version, '(\\d+(\\.\\d+)+)'))
  );

添加更多版本字符串测试,以查看该函数的行为表现。

SELECT
    version,
    sortableSemVer(version) AS sem_ver_arr
FROM
(
    SELECT [
        '1.0', '2.0', '3.0.0', '10.0', 'production', '1.2.3.production', 
        'my-app-1.2.3-prod', '3.5.0(ac22da)-test', '1456', '1.2.3.45', ''
        ] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

图片

当然,这种方法并非适用于所有情况。例如,以下格式的版本字符串无法被正确解析:

SELECT sortableSemVer('100.731a9bd8-5edbc015-SNAPSHOT') AS sem_ver_arr

┌─sem_ver_arr─┐
│ [100,731]   │
└─────────────┘

由于后缀被移除,以下版本也无法正确排序:

SELECT
    version,
    sortableSemVer(version) AS sem_ver_arr
FROM
(
    SELECT ['1.2.3-prod', '1.2.3', '1.2.3-stg'] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

┌─version────┬─sem_ver_arr─┐
│ 1.2.3-prod │ [1,2,3]     │
│ 1.2.3      │ [1,2,3]     │
│ 1.2.3-stg  │ [1,2,3]     │
└────────────┴─────────────┘

此外,不同的版本控制方案可能会产生冲突:

SELECT
    version,
    sortableSemVer(version) AS sem_ver_arr
FROM
(
    SELECT [
        'my-app-1.2.3-prod', 
        '1.2.3', 
        '1.2.3(af012342)-ALPHA'
        ] AS version
)
ARRAY JOIN version
ORDER BY sem_ver_arr DESC

Query id: 5bce759d-8ddb-4327-8e84-6f682b71b022

┌─version───────────────┬─sem_ver_arr─┐
│ my-app-1.2.3-prod     │ [1,2,3]     │
│ 1.2.3                 │ [1,2,3]     │
│ 1.2.3(af012342)-ALPHA │ [1,2,3]     │
└───────────────────────┴─────────────┘

不过,这通常不成问题,因为用户往往会使用相同的版本控制方案。ClickHouse 的 UDF 为使用 lambda 处理数据提供了强大支持。根据本指南中的方法调整 UDF,以更好地满足您的需求。对于我们的使用场景来说,这种方法已经足够有效。

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]

 

​​联系我们

手机号:13910395701

邮箱:[email protected]

满足您所有的在线分析列式数据库管理需求

猜你喜欢

转载自blog.csdn.net/ClickHouseDB/article/details/143352671