Естественная сортировка в MySQL - PullRequest
72 голосов
/ 30 сентября 2008

Существует ли элегантный способ иметь эффективную, естественную сортировку в базе данных MySQL?

Например, если у меня есть этот набор данных:

  • Final Fantasy
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Цепи Проматии
  • Final Fantasy Adventure
  • Final Fantasy Origins
  • Final Fantasy Tactics

Любое другое элегантное решение, кроме деления названий игр на составляющие

  • Заголовок : "Final Fantasy"
  • Число : "12"
  • Подзаголовок : "Цепи Проматии"

чтобы убедиться, что они выходят в правильном порядке? (10 после 4, а не до 2).

Это - боль в a **, потому что время от времени появляется другая игра, которая нарушает этот механизм анализа названия игры (например, «Warhammer 40,000», «James Bond 007»)

Ответы [ 19 ]

85 голосов
/ 14 июня 2011

Вот быстрое решение:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
52 голосов
/ 19 января 2009

Только что нашел это:

SELECT names FROM your_table ORDER BY games + 0 ASC

Делает естественную сортировку, когда числа находятся спереди, может также работать для середины.

45 голосов
/ 04 сентября 2012

Та же функция, что и @plalx, ​​но переписанная в MySQL:

DROP FUNCTION IF EXISTS `udf_FirstNumberPos`;
DELIMITER ;;
CREATE FUNCTION `udf_FirstNumberPos` (`instring` varchar(4000)) 
RETURNS int
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE position int;
    DECLARE tmp_position int;
    SET position = 5000;
    SET tmp_position = LOCATE('0', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF; 
    SET tmp_position = LOCATE('1', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('2', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('3', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('4', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('5', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('6', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('7', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('8', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;
    SET tmp_position = LOCATE('9', instring); IF (tmp_position > 0 AND tmp_position < position) THEN SET position = tmp_position; END IF;

    IF (position = 5000) THEN RETURN 0; END IF;
    RETURN position;
END
;;

DROP FUNCTION IF EXISTS `udf_NaturalSortFormat`;
DELIMITER ;;
CREATE FUNCTION `udf_NaturalSortFormat` (`instring` varchar(4000), `numberLength` int, `sameOrderChars` char(50)) 
RETURNS varchar(4000)
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE sortString varchar(4000);
    DECLARE numStartIndex int;
    DECLARE numEndIndex int;
    DECLARE padLength int;
    DECLARE totalPadLength int;
    DECLARE i int;
    DECLARE sameOrderCharsLen int;

    SET totalPadLength = 0;
    SET instring = TRIM(instring);
    SET sortString = instring;
    SET numStartIndex = udf_FirstNumberPos(instring);
    SET numEndIndex = 0;
    SET i = 1;
    SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars);

    WHILE (i <= sameOrderCharsLen) DO
        SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' ');
        SET i = i + 1;
    END WHILE;

    WHILE (numStartIndex <> 0) DO
        SET numStartIndex = numStartIndex + numEndIndex;
        SET numEndIndex = numStartIndex;

        WHILE (udf_FirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO
            SET numEndIndex = numEndIndex + 1;
        END WHILE;

        SET numEndIndex = numEndIndex - 1;

        SET padLength = numberLength - (numEndIndex + 1 - numStartIndex);

        IF padLength < 0 THEN
            SET padLength = 0;
        END IF;

        SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength));

        SET totalPadLength = totalPadLength + padLength;
        SET numStartIndex = udf_FirstNumberPos(RIGHT(instring, CHAR_LENGTH(instring) - numEndIndex));
    END WHILE;

    RETURN sortString;
END
;;

Использование:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")
22 голосов
/ 30 сентября 2008

Я думаю, именно поэтому многие вещи отсортированы по дате выпуска.

Решением может быть создание еще одного столбца в вашей таблице для "SortKey". Это может быть очищенная версия заголовка, которая соответствует шаблону, который вы создаете для легкой сортировки или счетчика.

15 голосов
/ 30 сентября 2008

MySQL не допускает такого рода «естественную сортировку», поэтому похоже, что лучший способ получить то, что вам нужно, - это разделить ваши данные, как вы описали выше (отдельное поле id и т. Д.) или, если это не удалось, выполните сортировку на основе элемента без заголовка, индексированного элемента в вашей базе данных (дата, вставленный идентификатор в базе данных и т. д.).

Выполнение сортировки БД для вас почти всегда будет быстрее, чем считывание больших наборов данных на выбранный вами язык программирования и их сортировка там, так что если у вас есть какой-либо контроль над схемой БД здесь, то Посмотрите на добавление легко сортируемых полей, как описано выше, это сэкономит вам много хлопот и затрат на обслуживание в долгосрочной перспективе.

Время от времени на дискуссионных форумах MySQL и периодически появляются запросы на добавление "естественной сортировки", и многие решения вращаются вокруг удаления определенных частей ваших данных и приведение их к ORDER BY части запроса, например

SELECT * FROM table ORDER BY CAST(mid(name, 6, LENGTH(c) -5) AS unsigned) 

Такого рода решение может быть почти готово для работы с вашим примером Final Fantasy, описанным выше, но оно не очень гибкое и вряд ли будет чисто распространяться на набор данных, включающий, скажем, «Warhammer 40 000» и «James Bond 007» I боюсь.

15 голосов
/ 07 апреля 2011

Я написал эту функцию для MSSQL 2000 некоторое время назад:

/**
 * Returns a string formatted for natural sorting. This function is very useful when having to sort alpha-numeric strings.
 *
 * @author Alexandre Potvin Latreille (plalx)
 * @param {nvarchar(4000)} string The formatted string.
 * @param {int} numberLength The length each number should have (including padding). This should be the length of the longest number. Defaults to 10.
 * @param {char(50)} sameOrderChars A list of characters that should have the same order. Ex: '.-/'. Defaults to empty string.
 *
 * @return {nvarchar(4000)} A string for natural sorting.
 * Example of use: 
 * 
 *      SELECT Name FROM TableA ORDER BY Name
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                    ID  Name
 *  1.  A1.                 1.  A1-1.       
 *  2.  A1-1.                   2.  A1.
 *  3.  R1      -->         3.  R1
 *  4.  R11                 4.  R11
 *  5.  R2                  5.  R2
 *
 *  
 *  As we can see, humans would expect A1., A1-1., R1, R2, R11 but that's not how SQL is sorting it.
 *  We can use this function to fix this.
 *
 *      SELECT Name FROM TableA ORDER BY dbo.udf_NaturalSortFormat(Name, default, '.-')
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                    ID  Name
 *  1.  A1.                 1.  A1.     
 *  2.  A1-1.                   2.  A1-1.
 *  3.  R1      -->         3.  R1
 *  4.  R11                 4.  R2
 *  5.  R2                  5.  R11
 */
CREATE FUNCTION dbo.udf_NaturalSortFormat(
    @string nvarchar(4000),
    @numberLength int = 10,
    @sameOrderChars char(50) = ''
)
RETURNS varchar(4000)
AS
BEGIN
    DECLARE @sortString varchar(4000),
        @numStartIndex int,
        @numEndIndex int,
        @padLength int,
        @totalPadLength int,
        @i int,
        @sameOrderCharsLen int;

    SELECT 
        @totalPadLength = 0,
        @string = RTRIM(LTRIM(@string)),
        @sortString = @string,
        @numStartIndex = PATINDEX('%[0-9]%', @string),
        @numEndIndex = 0,
        @i = 1,
        @sameOrderCharsLen = LEN(@sameOrderChars);

    -- Replace all char that has to have the same order by a space.
    WHILE (@i <= @sameOrderCharsLen)
    BEGIN
        SET @sortString = REPLACE(@sortString, SUBSTRING(@sameOrderChars, @i, 1), ' ');
        SET @i = @i + 1;
    END

    -- Pad numbers with zeros.
    WHILE (@numStartIndex <> 0)
    BEGIN
        SET @numStartIndex = @numStartIndex + @numEndIndex;
        SET @numEndIndex = @numStartIndex;

        WHILE(PATINDEX('[0-9]', SUBSTRING(@string, @numEndIndex, 1)) = 1)
        BEGIN
            SET @numEndIndex = @numEndIndex + 1;
        END

        SET @numEndIndex = @numEndIndex - 1;

        SET @padLength = @numberLength - (@numEndIndex + 1 - @numStartIndex);

        IF @padLength < 0
        BEGIN
            SET @padLength = 0;
        END

        SET @sortString = STUFF(
            @sortString,
            @numStartIndex + @totalPadLength,
            0,
            REPLICATE('0', @padLength)
        );

        SET @totalPadLength = @totalPadLength + @padLength;
        SET @numStartIndex = PATINDEX('%[0-9]%', RIGHT(@string, LEN(@string) - @numEndIndex));
    END

    RETURN @sortString;
END

GO
9 голосов
/ 15 июня 2011

Итак, хотя я знаю, что вы нашли удовлетворительный ответ, я некоторое время боролся с этой проблемой, и мы ранее определили, что это не может быть сделано достаточно хорошо в SQL, и мы собирались использовать javascript в массиве JSON.

Вот как я решил это, просто используя SQL. Надеюсь, это полезно для других:

У меня были такие данные как:

Scene 1
Scene 1A
Scene 1B
Scene 2A
Scene 3
...
Scene 101
Scene XXA1
Scene XXA2

Я на самом деле не "кастовал" вещи, хотя, полагаю, это тоже сработало.

Сначала я заменил части, которые не менялись в данных, в данном случае «Сцена», а затем сделал LPAD, чтобы выстроить все в ряд. Похоже, что это позволяет довольно хорошо сортировать альфа-строки так же, как и пронумерованные.

Мое предложение ORDER BY выглядит так:

ORDER BY LPAD(REPLACE(`table`.`column`,'Scene ',''),10,'0')

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

5 голосов
/ 05 декабря 2014

Относительно лучшего ответа от Ричарда Тота https://stackoverflow.com/a/12257917/4052357

Остерегайтесь строк в кодировке UTF8, которые содержат 2-байтовые (или более) символы и цифры, например,

12 南新宿

Использование функции MySQL LENGTH() in udf_NaturalSortFormat вернет длину строки в байтах и ​​будет неправильным, вместо этого используйте CHAR_LENGTH(), который вернет правильную длину символа.

В моем случае использование LENGTH() приводило к тому, что запросы никогда не выполнялись и приводило к 100% загрузке ЦП для MySQL

DROP FUNCTION IF EXISTS `udf_NaturalSortFormat`;
DELIMITER ;;
CREATE FUNCTION `udf_NaturalSortFormat` (`instring` varchar(4000), `numberLength` int, `sameOrderChars` char(50)) 
RETURNS varchar(4000)
LANGUAGE SQL
DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
BEGIN
    DECLARE sortString varchar(4000);
    DECLARE numStartIndex int;
    DECLARE numEndIndex int;
    DECLARE padLength int;
    DECLARE totalPadLength int;
    DECLARE i int;
    DECLARE sameOrderCharsLen int;

    SET totalPadLength = 0;
    SET instring = TRIM(instring);
    SET sortString = instring;
    SET numStartIndex = udf_FirstNumberPos(instring);
    SET numEndIndex = 0;
    SET i = 1;
    SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars);

    WHILE (i <= sameOrderCharsLen) DO
        SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' ');
        SET i = i + 1;
    END WHILE;

    WHILE (numStartIndex <> 0) DO
        SET numStartIndex = numStartIndex + numEndIndex;
        SET numEndIndex = numStartIndex;

        WHILE (udf_FirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO
            SET numEndIndex = numEndIndex + 1;
        END WHILE;

        SET numEndIndex = numEndIndex - 1;

        SET padLength = numberLength - (numEndIndex + 1 - numStartIndex);

        IF padLength < 0 THEN
            SET padLength = 0;
        END IF;

        SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength));

        SET totalPadLength = totalPadLength + padLength;
        SET numStartIndex = udf_FirstNumberPos(RIGHT(instring, CHAR_LENGTH(instring) - numEndIndex));
    END WHILE;

    RETURN sortString;
END
;;

p.s. Я бы добавил это как комментарий к оригиналу, но у меня недостаточно репутации (пока)

5 голосов
/ 30 сентября 2008
  1. Добавьте ключ сортировки (ранг) в вашу таблицу. ORDER BY rank

  2. Использовать столбец «Дата выпуска». ORDER BY release_date

  3. При извлечении данных из SQL заставьте ваш объект выполнить сортировку, например, если вы извлекаете в Set, сделайте его TreeSet и сделайте вашу модель данных реализуемой Comparable и примените алгоритм естественной сортировки здесь (вставка будет достаточно, если вы используете язык без коллекций), так как вы будете читать строки из SQL одну за другой при создании модели и вставке ее в коллекцию)

4 голосов
/ 04 мая 2017

Если вы не хотите изобретать велосипед или испытывать головную боль с большим количеством кода, который не работает, просто используйте Drupal Natural Sort ... Просто запустите SQL, который поставляется в заархивированном виде (MySQL или Postgre ), и это все. Делая запрос, просто закажите с помощью:

... ORDER BY natsort_canon(column_name, 'natural')
...