本文字数:5775;估计阅读时间:15 分钟
作者:Juan S. Carrillo
本文在公众号【ClickHouseInc】首发
我在 Embrace 工作,我们构建了唯一基于 OpenTelemetry 的用户中心移动应用可观测性解决方案,并使用 ClickHouse 为我们的时间序列分析产品提供支持。
应用版本是 Embrace 用户最重要的排序类别之一。应用版本通常采用语义版本控制(Semantic Versioning),版本格式为 <主版本>.<次版本>.<补丁版本>。按以下规则进行版本升级:
-
主版本号:进行不兼容的 API 更改时
-
次版本号:增加向后兼容的新功能时
-
补丁版本号:进行向后兼容的错误修复时
我们希望对应用版本排序时得到 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] │
└─────────┴─────────────┘
具体步骤如下:
-
splitByChar('.', version) 将版本字符串按 . 分割为字符串数组,例如将 10.0 转换为 ['10', '0']。
-
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
满足您所有的在线分析列式数据库管理需求