如何比较 MySQL 中的版本字符串(“x.y.z”)?

Posted

技术标签:

【中文标题】如何比较 MySQL 中的版本字符串(“x.y.z”)?【英文标题】:How to compare version string ("x.y.z") in MySQL? 【发布时间】:2012-02-01 07:50:13 【问题描述】:

我的表中有固件版本字符串(如“4.2.2”或“4.2.16”)

如何比较、选择或排序它们?

我不能使用标准字符串比较:“4.2.2”是 SQL 看到的大于“4.2.16”

作为版本字符串,我希望 4.2.16 大于 4.2.2

我想考虑固件版本中可以包含字符:4.24a1、4.25b3 ...为此,通常带有字符的子字段具有固定长度。

如何进行?

【问题讨论】:

这就是为什么你应该将字符串存储为字符串,将数字存储为数字 版本号总是包含3组数字吗? @Salman :不,我可能需要比较 4.2 和 4.2.1 @Eric:你会拥有超过组超过3组数字吗? @Mark Ba​​nnister:如您所见 - 我不建议将其存储在 single 字段中。 PS:很酷的声誉值;-) 【参考方案1】:

如果您的所有版本号都类似于以下任何一个:

X
X.X
X.X.X
X.X.X.X

其中 X 是 0 到 255(含)之间的整数,那么您可以使用 INET_ATON() 函数将字符串转换为适合比较的整数。

不过,在应用函数之前,您需要确保函数的参数是 X.X.X.X 形式,方法是向其附加必要数量的 '.0'。为此,您首先需要找出字符串中已经包含多少个.,可以这样完成:

CHAR_LENGTH(ver) - CHAR_LENGTH(REPLACE(ver, '.', '')

即字符串中的句点个数等于字符串的长度减去去掉句点后的长度。

然后应该从3 中减去获得的结果,并与'.0' 一起传递给REPEAT() 函数:

REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', ''))

这将为我们提供必须附加到原始ver 值的子字符串,以符合X.X.X.X 格式。因此,它将依次与ver 一起传递给CONCAT() 函数。 CONCAT() 的结果现在可以直接传递给 INET_ATON()。所以这就是我们最终得到的结果:

INET_ATON(
  CONCAT(
    ver,
    REPEAT(
      '.0',
      3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', ''))
    )
  )
)

这只是一个价值! :) 应该为另一个字符串构造一个类似的表达式,然后你可以比较结果。

参考资料:

INET_ATON()

CHAR_LENGTH()

CONCAT()

REPEAT()

REPLACE()

【讨论】:

非常感谢。我遇到了这个问题,我必须比较数据库中的版本值。所以我需要一种方法在传递给 inet_aton 之前清理 mysql 中的版本信息。 +1 给你 注意:此解决方案在以下三种情况下会失败: (a) 版本超过 3 个点,如 2.2.0.1.1 (2) 版本包含非数字值,如 @987654349 @ (3) 版本段高于255,如2020.11.18。如果您能保证这些情况永远不会发生,请仅使用此代码 @Philipp:我完全同意。尽管措辞不同,但我想我已经在回答的开头传达了所有这些观点。不过,如果您认为我的措辞在某些方面有所欠缺,我真的很想知道这一点并修正我的答案,因为我不是以英语为母语的人。干杯。【参考方案2】:

假设组数为3个或更少,您可以将版本号视为两个十进制数并进行相应的排序。方法如下:

SELECT 
ver,
CAST(
    SUBSTRING_INDEX(ver, '.', 2)
    AS DECIMAL(6,3)
) AS ver1, -- ver1 = the string before 2nd dot
CAST(
    CASE
        WHEN LOCATE('.', ver) = 0 THEN NULL
        WHEN LOCATE('.', ver, LOCATE('.', ver)+1) = 0 THEN SUBSTRING_INDEX(ver, '.', -1)
        ELSE SUBSTRING_INDEX(ver, '.', -2)
    END
    AS DECIMAL(6,3)
) AS ver2  -- ver2 = if there is no dot then 0.0
           --        else if there is no 2nd dot then the string after 1st dot
           --        else the string after 1st dot
FROM
(
SELECT '1' AS ver UNION
SELECT '1.1' UNION
SELECT '1.01' UNION
SELECT '1.01.03' UNION
SELECT '1.01.04' UNION
SELECT '1.01.1' UNION
SELECT '1.11' UNION
SELECT '1.2' UNION
SELECT '1.2.0' UNION
SELECT '1.2.1' UNION
SELECT '1.2.11' UNION
SELECT '1.2.2' UNION
SELECT '2.0' UNION
SELECT '2.0.1' UNION
SELECT '11.1.1' 
) AS sample
ORDER BY ver1, ver2

输出:

ver     ver1    ver2
======= ======  ======
1        1.000  (NULL)
1.01     1.010   1.000
1.01.03  1.010   1.030
1.01.04  1.010   1.040
1.01.1   1.010   1.100
1.1      1.100   1.000
1.11     1.110  11.000
1.2.0    1.200   2.000
1.2      1.200   2.000
1.2.1    1.200   2.100
1.2.11   1.200   2.110
1.2.2    1.200   2.200
2.0      2.000   0.000
2.0.1    2.000   0.100
11.1.1  11.100   1.100

注意事项:

    您可以将此示例扩展到最多 4 个或更多组,但字符串函数会变得越来越复杂。 数据类型转换DECIMAL(6,3) 用于说明。如果您希望次要版本号超过 3 位,请进行相应修改。

【讨论】:

【参考方案3】:

最后,我找到了另一种对版本字符串进行排序的方法。

我只是在以可排序的方式存储到数据库之前证明字符串的合理性。 当我使用 python Django 框架时,我刚刚创建了一个 VersionField,它在存储时“编码”版本字符串并在读取时“解码”它,因此它对应用程序完全透明:

这是我的代码:

The justify function :

def vjust(str,level=5,delim='.',bitsize=6,fillchar=' '):
    """
    1.12 becomes : 1.    12
    1.1  becomes : 1.     1
    """
    nb = str.count(delim)
    if nb < level:
        str += (level-nb) * delim
    return delim.join([ v.rjust(bitsize,fillchar) for v in str.split(delim)[:level+1] ])

The django VersionField :

class VersionField(models.CharField) :

    description = 'Field to store version strings ("a.b.c.d") in a way it is sortable'

    __metaclass__ = models.SubfieldBase

    def get_prep_value(self, value):
        return vjust(value,fillchar=' ')

    def to_python(self, value):
        return re.sub('\.+$','',value.replace(' ',''))

【讨论】:

【参考方案4】:

这是一个相当复杂的问题,因为 SQL 并非旨在从单个字段中拆分多个值 - 这违反了First Normal Form。假设您不会拥有超过三组数字,每组数字的长度不会超过三位数,请尝试:

cast(substring_index(concat(X,'.0.0.'), '.', 1) as float) * 1000000 +
cast(substring_index(substring_index(concat(X,'.0.0.'), '.', 2), '.', -1) as float) * 1000 +
cast(substring_index(substring_index(concat(X,'.0.0.'), '.', 3), '.', -1) as float)

【讨论】:

此解决方案有效。但是强制转换为浮点数会导致 mysql(?) 中出现 sql 语法错误。所以我做了一点修改: select CONCAT(LPAD(substring_index(concat("1.2.3",'.0.0.'), '.', 1), 9, '0'), LPAD(substring_index(substring_index(concat ("1.2.3",'.0.0.'), '.', 2), '.', -1), 9, '0'), LPAD(substring_index(substring_index(concat("1.2.3", '.0.0.'), '.', 3), '.', -1), 9, '0'));【参考方案5】:

Python 可以按照您希望比较版本的方式逐个元素地比较列表,因此您可以简单地在“.”上拆分,在每个元素上调用 int(x)(使用列表理解)来将字符串转成int,然后比较

    >>> v1_3 = [ int(x) for x in "1.3".split(".") ]
    >>> v1_2 = [ int(x) for x in "1.2".split(".") ]
    >>> v1_12 = [ int(x) for x in "1.12".split(".") ]
    >>> v1_3_0 = [ int(x) for x in "1.3.0".split(".") ]
    >>> v1_3_1 = [ int(x) for x in "1.3.1".split(".") ]
    >>> v1_3
    [1, 3]
    >>> v1_2
    [1, 2]
    >>> v1_12
    [1, 12]
    >>> v1_3_0
    [1, 3, 0]
    >>> v1_3_1
    [1, 3, 1]
    >>> v1_2 < v1_3
    True
    >>> v1_12 > v1_3
    True
    >>> v1_12 > v1_3_0
    True
    >>> v1_12 > v1_3_1
    True
    >>> v1_3_1 < v1_3
    False
    >>> v1_3_1 < v1_3_0
    False
    >>> v1_3_1 > v1_3_0
    True
    >>> v1_3_1 > v1_12
    False
    >>> v1_3_1 < v1_12
    True
    >>> 

【讨论】:

【参考方案6】:

我一直在寻找同样的东西,结果却这样做了——但留在了 mysql 中:

将这个udf library 安装到 mysql 中,因为我想要 PCRE 的强大功能。

使用这个语句

case when version is null then null
when '' then 0
else
preg_replace( '/[^.]*([^.]10)[.]+/', '$1', 
    preg_replace('/([^".,\\/_ ()-]+)([".,\\/_ ()-]*)/','000000000$1.',
        preg_replace('/(?<=[0-9])([^".,\\/_ ()0-9-]+)/','.!$1',version
))) 
end

我将分解这意味着什么:

preg_replace 是 UDF 库创建的函数。因为它是一个 UDF,所以您可以像这样从任何用户或 dbspace 调用它 ^".,\\/_ () 现在我正在考虑将所有这些字符作为分隔符或版本中的传统“点” preg_replace('/(?&lt;=[0-9])([^".,\\/_ ()0-9-]+)/','.!$1',version) 表示将所有前面有数字的非“点”和非数字替换为“点”和感叹号。 preg_replace('/([^".,\\/_ ()-]+)([".,\\/_ ()-]*)/','000000000$1.', ...) 表示另外用实际点替换所有“点”,并用 9 个零填充所有数字。任何相邻的点也会减少到 1。 preg_replace( '/0*([^.]10)[.]+/', '$1', ... ) 意味着将所有数字块额外剥离到只有 10 位长,并根据需要保留尽可能多的块。我想强制 6 个块将其保持在 64 字节以下,但需要 7 个块非常普遍,因此对于我的准确性来说是必要的。还需要 10 块,所以 7 块 9 不是一个选择。但是可变长度对我来说效果很好。 -- 记住字符串是从左到右比较的

所以现在我可以处理如下版本:

1.2 < 1.10
1.2b < 1.2.0
1.2a < 1.2b
1.2 = 1.2.0
1.020 = 1.20
11.1.1.3.0.100806.0408.000  < 11.1.1.3.0.100806.0408.001
5.03.2600.2180 (xpsp_sp2_rtm.040803-2158)
A.B.C.D = a.B.C.D
A.A  <  A.B

我选择感叹号是因为它在 0 之前的排序规则序列中排序(无论如何我都在使用)。它与 0 的相对排序允许像 b 和 a 这样的字母在与上面的数字紧邻使用时被视为新的部分并在 0 之前排序——这是我正在使用的填充。

我使用 0 作为填充,这样供应商的错误(例如从固定的 3 位数块移动到变量块)就不会咬我。

如果您想处理诸如“2.11.0 开发中(不稳定)(2010-03-09)”之类的愚蠢版本,您可以轻松选择更多填充 - 字符串 development 为 11 个字节。

您可以在最终替换中轻松请求更多块。

我本可以做得更多,但由于我有数百万条记录需要定期扫描,因此我试图以尽可能少的步数保持高精度。如果有人看到优化,请回复。

我选择将其保留为字符串而不是转换为数字,因为转换是有代价的,而且字母也很重要,正如我们所见。我正在考虑的一件事是对字符串进行测试并返回一个选项,该选项对于更整洁的情况来说不是那么多通过或更便宜的功能。比如11.1.1.3是一种很常见的格式

【讨论】:

【参考方案7】:

这里有很多好的解决方案,但我想要一个可以与 ORDER BY 一起使用的存储函数

CREATE FUNCTION standardize_version(version VARCHAR(255)) RETURNS varchar(255) CHARSET latin1 DETERMINISTIC NO SQL
BEGIN
  DECLARE tail VARCHAR(255) DEFAULT version;
  DECLARE head, ret VARCHAR(255) DEFAULT NULL;

  WHILE tail IS NOT NULL DO 
    SET head = SUBSTRING_INDEX(tail, '.', 1);
    SET tail = NULLIF(SUBSTRING(tail, LOCATE('.', tail) + 1), tail);
    SET ret = CONCAT_WS('.', ret, CONCAT(REPEAT('0', 3 - LENGTH(CAST(head AS UNSIGNED))), head));
  END WHILE;

  RETURN ret;
END|

测试:

SELECT standardize_version(version) FROM (SELECT '1.2.33.444.5b' AS version UNION SELECT '1' UNION SELECT NULL) AS t;

渲染:

00001.00002.00033.00444.00005b
00001
(null)

并且允许比较几乎任何一组版本,甚至是带有字母的版本。

【讨论】:

它唯一不处理的是某些版本编号方案末尾的哈希值,但无论如何这些都不是可排序的。【参考方案8】:
/**
function version_compare(version1, version2)

parameters
version1 first version number.
version2 second version number.

return values
-1: if version1 is less than version2;
1: if version1 is greater than version2,
0: if version1 equal version2.

example:
select version_compare('4.2.2','4.2.16') from dual;
version_compare('4.2.2','4.2.16')  
-----------------------------------
    -1 

*/
drop function if exists version_compare;
delimiter @@

create function version_compare(version1 varchar(100), version2 varchar(100))
  returns tinyint
  begin
    declare v_result tinyint;
    declare version1_sub_string varchar(100);
    declare version2_sub_string varchar(100);
    declare version1_sub_int int;
    declare version2_sub_int int;

    declare version1_sub_end tinyint;
    declare version2_sub_end tinyint;


    if version1 = version2 then
      set v_result = 0;
    else

      set version1_sub_string = version1;
      set version2_sub_string = version2;

      lp1 : loop
        set version1_sub_end = locate('.', version1_sub_string);
        set version2_sub_end = locate('.', version2_sub_string);

        if version1_sub_end <> 0 then
          set version1_sub_int = cast(substring(version1_sub_string, 1, version1_sub_end - 1) as signed);
          set version1_sub_string = substring(version1_sub_string, version1_sub_end +1 );
        else
          set version1_sub_int = cast(version1_sub_string as signed);
        end if;

        if version2_sub_end <> 0 then
          set version2_sub_int = cast(substring(version2_sub_string, 1, version2_sub_end - 1) as signed);

          set version2_sub_string = substring(version2_sub_string, version2_sub_end + 1);
        else
          set version2_sub_int = cast(version2_sub_string as signed);
        end if;


        if version1_sub_int > version2_sub_int then
          set v_result = 1;
          leave lp1;

        elseif version1_sub_int < version2_sub_int then
            set v_result = -1;
            leave lp1;
        else
          if version1_sub_end = 0 and version2_sub_end = 0 then
            set v_result = 0;
            leave lp1;

          elseif version1_sub_end = 0 then
              set v_result = -1;
              leave lp1;

          elseif version2_sub_end = 0 then
              set v_result = 1;
              leave lp1;
          end if;      
        end if;

      end loop;
    end if;

    return v_result;

 end@@
delimiter ;

【讨论】:

请补充说明【参考方案9】:

这是我的解决方案。它不取决于颠覆的数量。

例如:

select SF_OS_VERSION_COMPARE('2016.10.1712.58','2016.9.1712.58');

返回“高”

select SF_OS_VERSION_COMPARE('2016.10.1712.58','2016.10.1712.58');

返回“相等”

delimiter //

DROP FUNCTION IF EXISTS SF_OS_VERSION_COMPARE //

CREATE FUNCTION SF_OS_VERSION_COMPARE(ver_1 VARCHAR(50), ver_2 VARCHAR(50)) RETURNS VARCHAR(5)
    DETERMINISTIC
    COMMENT 'Return "HIGH", "LOW" OR "EQUAL" comparing VER_1 with VER_2'
BEGIN
    DECLARE v_ver1 VARCHAR(50);
    DECLARE v_ver2 VARCHAR(50);
    DECLARE v_ver1_num INT;
    DECLARE v_ver2_num INT;

    SET v_ver1 = ver_1;
    SET v_ver2 = ver_2;

    WHILE ( v_ver1 <> v_ver2 AND ( v_ver1 IS NOT NULL OR v_ver2 IS NOT NULL )) DO

    SET v_ver1_num = CAST(SUBSTRING_INDEX(v_ver1, '.', 1) AS UNSIGNED INTEGER);
    SET v_ver2_num = CAST(SUBSTRING_INDEX(v_ver2, '.', 1) AS UNSIGNED INTEGER);

    IF ( v_ver1_num > v_ver2_num )
    THEN
        return 'HIGH';
    ELSEIF ( v_ver1_num < v_ver2_num )
    THEN
        RETURN 'LOW';
    ELSE
        SET v_ver1 = SUBSTRING(v_ver1,LOCATE('.', v_ver1)+1);
        SET v_ver2 = SUBSTRING(v_ver2,LOCATE('.', v_ver2)+1);
    END IF;

    END WHILE;

    RETURN 'EQUAL';

END //

【讨论】:

【参考方案10】:

我已经基于上面的excellent answer of Salman A 创建了一个灵活的纯 SQL 解决方案:

在这个逻辑中,我比较了前 4 个版本段。当版本字符串有更多段时,忽略尾段。

代码从表中获取 idver 列,然后“清理”ver 值以始终包含 3 个点 - 此清理后的版本由 sane_ver 字段返回。

该净化后的版本随后被拆分为 4 个整数值,每个值代表一个版本段。您可以根据这 4 个整数对结果进行比较或排序。

代码

SELECT
    id,
    ver,
    SUBSTRING_INDEX(sane_ver, '.', 1) + 0 AS ver1,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 2), '.', -1) + 0 AS ver2,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 3), '.', -1) + 0 AS ver3,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 4), '.', -1) + 0 AS ver4
FROM (
    SELECT
        id,
        ver,
        CONCAT(
            ver,
            REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', '')))
        ) AS sane_ver
    FROM (
        SELECT id, ver FROM some_table
    ) AS raw_data 
) AS sane_data

示例

这是一个完整的查询,其中包含一些示例数据和一个仅返回低于 1.2.3.4 的版本的过滤器

SELECT
    id,
    ver,
    SUBSTRING_INDEX(sane_ver, '.', 1) + 0 AS ver1,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 2), '.', -1) + 0 AS ver2,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 3), '.', -1) + 0 AS ver3,
    SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 4), '.', -1) + 0 AS ver4
FROM (
    SELECT
        id,
        ver,
        CONCAT(
            ver,
            REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', '')))
        ) AS sane_ver
    FROM (
        SELECT 1 AS id, '1' AS ver UNION
        SELECT 2,  '1.1' UNION
        SELECT 3,  '1.2.3.4.5' UNION
        SELECT 4,  '1.01' UNION
        SELECT 5,  '1.01.03' UNION
        SELECT 6,  '1.01.04a' UNION
        SELECT 7,  '1.01.04' UNION
        SELECT 8,  '1.01.04b' UNION
        SELECT 9,  '1.01.1.9.2.1.0' UNION
        SELECT 10, '1.11' UNION
        SELECT 11, '1.2' UNION
        SELECT 12, '1.2.0' UNION
        SELECT 13, '1.2.1' UNION
        SELECT 14, '1.2.11' UNION
        SELECT 15, '1.2.2' UNION
        SELECT 16, '2.0' UNION
        SELECT 17, '2.0.1' UNION
        SELECT 18, '11.1.1' UNION
        SELECT 19, '2020.11.18.11'
    ) AS raw_data 
) AS sane_data
HAVING 
    ver1 <= 1
    AND (ver2 <= 2 OR ver1 < 1) 
    AND (ver3 <= 3 OR ver2 < 2 OR ver1 < 1) 
    AND (ver4 <  4 OR ver3 < 3 OR ver2 < 2 OR ver1 < 1)

注意事项

请注意此逻辑与 Salman A 的原始代码有何不同:

original answer 使用CAST AS DECIMAL()1.02 转换为1.020,并将1.1.0 转换为1.100 → 那比较 1.02.0 低于 1.1.0 (在我的理解中这是错误的)

此答案中的代码将1.02 转换为整数1, 2,并将1.1 转换为整数1, 1 → 那比较 1.1.0 低于 1.02.0

此外,我们的两种解决方案都完全忽略任何非数字字符,将1.2-alpha 视为等于1.2.0

【讨论】:

【参考方案11】:

我只使用以下适用于最高 255 的所有版本号:

比较示例:

SELECT * FROM versions
WHERE INET_ATON(SUBSTRING_INDEX(CONCAT(version, '.0.0.0'), '.', 4)) > INET_ATON(SUBSTRING_INDEX(CONCAT('2.1.27', '.0.0.0'), '.', 4));

按示例排序:

SELECT * FROM versions
ORDER BY INET_ATON(SUBSTRING_INDEX(CONCAT(version, '.0.0.0'), '.', 4));

也许您可以使用 INET6_ATON 来覆盖具有十六进制字符 (a-f) 的版本?

【讨论】:

以上是关于如何比较 MySQL 中的版本字符串(“x.y.z”)?的主要内容,如果未能解决你的问题,请参考以下文章

r 检查包版本是不是大于 x.y.z

Grafana:使用字符串类型的 MySQL 表列作为图形的 X 轴

Mysql - 如何将字符串中的日期与查询中的unix时间进行比较

你如何比较Java中的两个版本字符串?

MySQL数据库中的中文乱码如何解决

动态比较 MySQL 中的版本号