我在 Embrace 工作,我们基于 OpenTelemetry (OTel) 构建了唯一以用户为中心的移动应用可观测性解决方案。我们使用 ClickHouse 来支持我们的时间序列分析产品。
对于 Embrace 用户来说,最重要的排序类别之一是应用版本。应用版本通常使用语义版本控制,其中版本将以 <MAJOR>.<MINOR>.<PATCH>
格式描述。您根据以下规则递增它们
- 当您进行不兼容的 API 更改时,主版本 (MAJOR)
- 当您以向后兼容的方式添加功能时,次版本 (MINOR)
- 当您进行向后兼容的错误修复时,补丁版本 (PATCH)
我们希望能够对应用版本进行排序,以便 2.1.0、2.1.2 和 2.1.10 按照该顺序出现,而不是 2.1.0、2.1.10 和 2.1.2,当您按字典顺序排序时会发生这种情况。
ClickHouse 没有提供开箱即用的语义版本控制排序方法。但是,您可以使用用户定义函数 (UDF),ClickHouse v21.10 中引入了 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)
我们可以将 toUInt32
替换为 toUInt32OrZero
,对于非数字字符串,它将默认为 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 │
└─────────────────────────────────────────────────────────┘
让我们进一步更改它,以允许包含 2 个或更多子部分的语义版本。
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,以最适合您的需求。对于我们的目的,我们发现这已经足够好了。