Вопрос

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

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

  • Последняя фантазия
  • Финальная фантазия 4
  • Финальная фантазия 10
  • Финальная фантазия 12
  • Финальная фантазия 12:Цепи из Проматии
  • Финальное Фантастическое Приключение
  • Происхождение Final Fantasy
  • Тактика Финальной фантазии

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

  • Название:"Финальная фантазия"
  • Число: "12"
  • Подзаголовок:"Цепи Проматии"

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

Делать это непросто, потому что время от времени появляется другая игра, которая нарушает этот механизм синтаксического анализа названия игры (например"Warhammer 40,000", "Джеймс Бонд 007")

Это было полезно?

Решение

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

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

Другие советы

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

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

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

SELECT names FROM your_table ORDER BY games + 0 ASC

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

Та же функция, что опубликована @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 не допускает такого рода "естественную сортировку", поэтому, похоже, лучший способ получить то, что вам нужно, - это разделить ваши настройки данных, как вы описали выше (отдельное поле 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".

Я написал эту функцию для 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. Добавьте ключ сортировки (Rank) в свою таблицу. ORDER BY rank

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

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

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

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

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

На заказ:
0
1
2
10
23
101
205
1000
a
aac
b
касдсадса
css

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

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

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

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

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

Если вы взглянете на пост Джеффа, то сможете найти множество алгоритмов для любого языка, с которым вы могли бы работать.Сортировка для людей :Естественный Порядок сортировки

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

Если у вас могут быть длинные строки цифр, другой метод заключается в добавлении количества цифр (фиксированной ширины, с добавлением нуля) к каждой строке цифр.Например, если у вас будет не более 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

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

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

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 года, Так что я бы не стал задерживать дыхание.

Упрощенная версия наилучшего ответа @plaix / Ричарда Тота / Люка Хоггетта, не использующая udf, которая работает только для первого целого числа в поле, является

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

Также есть нацорт.Он предназначен для того, чтобы быть частью плагин для 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

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

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7:Адвент Детей Final Fantasy 12 Final Fantasy 112 ФФ1 ФФ2

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top