Как сравнить строку версии ("xyz") в MySQL? - PullRequest
13 голосов
/ 03 января 2012

У меня в таблице есть строки с версиями прошивки (например, "4.2.2" или "4.2.16")

Как их сравнить, выбрать или отсортировать?

Я не могу использовать стандартное сравнение строк: «4.2.2» является видимым SQL больше, чем «4.2.16»

Как строки версии, я бы хотел, чтобы 4.2.16 была больше, чем 4.2.2

Я хотел бы учесть, что в версии прошивки могут быть символы: 4.24a1, 4.25b3 ... для этого обычно подполе с символами имеет фиксированную длину.

как поступить?

Ответы [ 9 ]

15 голосов
/ 03 января 2012

Если все номера ваших версий выглядят так:

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.Таким образом, он, в свою очередь, будет передан функции CONCAT() вместе с ver.И результат этого CONCAT() теперь может быть напрямую передан в INET_ATON().Итак, вот что мы в итоге получаем:

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

И это только для одного значения!:) Аналогичное выражение должно быть построено для другой строки, после чего вы можете сравнить результаты.

Ссылки:

4 голосов
/ 03 января 2012

Предполагая, что количество групп равно 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

Примечания:

  1. Вы можете расширить этот пример до 4 или более групп, но строковые функции будут усложняться.
  2. Преобразование типа данных DECIMAL(6,3) используется для иллюстрации. Если в младших номерах версий ожидается более 3 цифр, измените их соответствующим образом.
3 голосов
/ 04 января 2012

Наконец, я нашел другой способ сортировки строк версий.

Я просто оправдываю строку перед сохранением в базе данных таким образом, чтобы ее можно было сортировать.Поскольку я использую инфраструктуру 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(' ',''))
2 голосов
/ 03 января 2012

Это довольно сложный вопрос, поскольку SQL не предназначен для разделения нескольких значений из одного поля - это нарушение Первая нормальная форма . Предполагая, что у вас не будет более трех групп чисел, каждая из которых будет длиной не более трех цифр, попробуйте:

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)
1 голос
/ 16 января 2017

Здесь много хороших решений, но я хотел, чтобы сохраненная функция работала с 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)

И позволяет сравнивать практически любой набор версий, даже с буквами.

1 голос
/ 23 августа 2014

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
    >>> 
0 голосов
/ 12 января 2019
/**
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 ;
0 голосов
/ 17 марта 2016

Это моё решение.Это не зависит от номера подрывной деятельности.

Например:

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

возвращает 'HIGH'

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

возвращает 'EQUAL'

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 //
0 голосов
/ 10 февраля 2014

Я искал то же самое и вместо этого закончил делать это - но остался в mysql:

  • Установка этой библиотеки udf в 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, вы можете просто вызвать его от любого пользователя или пространства баз данных, подобных этому
  • ^".,\\/_ () сейчас я рассматриваю все эти символы как разделители или традиционные "точки" в версии
  • preg_replace('/(?<=[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 в качестве отступа, чтобы ошибки продавца, такие как переход от фиксированного трехзначного блока к переменному, меня не кусали.

Вы можете легко выбрать больше отступов, если хотите обрабатывать глупые версии, такие как "2.11.0 В разработке (нестабильный) (2010-03-09)" - строка development составляет 11 байтов.

Вы можете легко запросить больше блоков в окончательной замене.

Я мог бы сделать больше, но я пытался сделать как можно меньше шагов с высокой точностью, поскольку у меня есть несколько миллионов записей для регулярного сканирования. Если кто-то видит оптимизацию, пожалуйста, ответьте.

Я решил оставить его в виде строки, а не в виде числа, потому что приведение имеет стоимость, а также буквы важны, как мы видели. Одна вещь, о которой я думал, это выполнить тест на строке и вернуть опцию, которая не так много проходов или менее дорогая функция для более аккуратных случаев. как 11.1.1.3 очень распространенный формат

...