Естественная сортировка в MySQL

есть ли элегантный способ иметь performant, естественную сортировку в базе данных 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 ответов


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

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


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

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric

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

SELECT names FROM your_table ORDER BY games + 0 ASC

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


та же функция, что и @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, ".")

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

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

запросы на добавление "естественной сортировки" приходят время от времени на ошибок 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", я боюсь.


Я написал эту функцию для 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

Итак, хотя я знаю, что вы нашли удовлетворительный ответ, я некоторое время боролся с этой проблемой, и мы ранее определили, что это не может быть сделано достаточно хорошо в 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')

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


  1. добавьте ключ сортировки (ранг) в таблицу. ORDER BY rank

  2. используйте столбец "Дата выпуска". ORDER BY release_date

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


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

следите за UTF8 закодированных строк, которые содержат 2byte (или более) символов и чисел, например

12 南新宿

использование MySQL LENGTH() на 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. Я бы добавил Это в качестве комментария к оригиналу, но у меня недостаточно репутации (пока)


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

Если вы посмотрите на сообщение Джеффа, вы можете найти множество алгоритмов для того, с каким языком вы можете работать. http://www.codinghorror.com/blog/archives/001018.html


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

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

добавьте поле для "ключа сортировки", которое имеет все строки цифр с нулевым заполнением до фиксированной длины, а затем сортируйте по этому полю.

Если у вас могут быть длинные строки цифр, другим методом является добавление количества цифр (фиксированной ширины, с нулевым заполнением) к каждой строке цифр. Например, если у вас не будет более 99 цифр подряд, то для" Super Blast 10 Ultra "ключ сортировки будет"Super Blast 0210 Ultra".


вы также можете создать динамическим способом "столбец сортировки":

SELECT name, (name = '-') boolDash, (name = '0') boolZero, (name+0 > 0) boolNum 
FROM table 
ORDER BY boolDash DESC, boolZero DESC, boolNum DESC, (name+0), name

таким образом, вы можете создавать группы для сортировки.

в моем запросе я хотел, чтобы " - " перед всем, затем цифры, затем текст. Что может привести к чему-то вроде :

-
0    
1
2
3
4
5
10
13
19
99
102
Chair
Dog
Table
Windows

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


по порядку:
0
1
2
10
23
101
205
1000
а
aac
б
casdsadsa
в CSS

используйте этот запрос:

SELECT 
    column_name 
FROM 
    table_name 
ORDER BY
    column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#$\%\^\*\(\)\;\:\,\?\/\~\`\|\_\-]' DESC, 
    column_name + 0, 
    column_name;

Я пробовал несколько решений, но на самом деле это очень просто:

SELECT test_column FROM test_table ORDER BY LENGTH(test_column) DESC, test_column DESC

/* 
Result 
--------
value_1
value_2
value_3
value_4
value_5
value_6
value_7
value_8
value_9
value_10
value_11
value_12
value_13
value_14
value_15
...
*/

Если вы используете PHP, вы можете сделать естественную сортировку в php.

$keys = array();
$values = array();
foreach ($results as $index => $row) {
   $key = $row['name'].'__'.$index; // Add the index to create an unique key.
   $keys[] = $key;
   $values[$key] = $row; 
}
natsort($keys);
$sortedValues = array(); 
foreach($keys as $index) {
  $sortedValues[] = $values[$index]; 
}

Я надеюсь, что MySQL будет реализовывать естественную сортировку в будущей версии, но запрос (#1588) открыт с 2003 года, поэтому я бы не задерживал дыхание.


упрощенная версия без udf лучшего ответа @plaix / Richard Toth / Luke Hoggett, которая работает только для первого целого числа в поле, является

SELECT name,
LEAST(
    IFNULL(NULLIF(LOCATE('0', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('1', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('2', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('3', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('4', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('5', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('6', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('7', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('8', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('9', name), 0), ~0)
) AS first_int
FROM table
ORDER BY IF(first_int = ~0, name, CONCAT(
    SUBSTR(name, 1, first_int - 1),
    LPAD(CAST(SUBSTR(name, first_int) AS UNSIGNED), LENGTH(~0), '0'),
    SUBSTR(name, first_int + LENGTH(CAST(SUBSTR(name, first_int) AS UNSIGNED)))
)) ASC

есть natsort. Он предназначен для того, чтобы быть частью плагин drupal, но он отлично работает автономно.


Я знаю, что эта тема древняя, но я думаю, что я нашел способ сделать это:

SELECT * FROM `table` ORDER BY 
CONCAT(
  GREATEST(
    LOCATE('1', name),
    LOCATE('2', name),
    LOCATE('3', name),
    LOCATE('4', name),
    LOCATE('5', name),
    LOCATE('6', name),
    LOCATE('7', name),
    LOCATE('8', name),
    LOCATE('9', name)
   ),
   name
) ASC

Scrap, что, он неправильно отсортировал следующий набор (это бесполезно lol):

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7: Дети Адвента Final Fantasy 12 Последняя Фантазия 112 FF1 ФФ2