it-swarm.com.ru

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

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

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

  • Последняя фантазия
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Цепи Проматии
  • Final Fantasy Adventure
  • Final Fantasy Origins
  • Final Fantasy Tactics

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

  • Title: "Final Fantasy"
  • Number: "12"
  • Подзаголовок: «Цепи Проматии»

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

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

71
BlaM

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

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

22
Michael Haren

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

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

Просто нашел это:

SELECT names FROM your_table ORDER BY games + 0 ASC

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

51
markletp

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

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

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

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

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

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

15
ConroyP

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

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

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

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

 Сцена 1 
 Сцена 1A 
 Сцена 1B 
 Сцена 2A 
 Сцена 3 
...
 Сцена 101 
 Сцена XXA1 
 Сцена XXA2 

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

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

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

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

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

9
FilmJ
  1. Добавьте ключ сортировки (ранг) в вашей таблице. ORDER BY rank

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

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

5
JeeBee

Относительно лучшего ответа от Ричарда Тота 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
;;

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

5
Luke Hoggett

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

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

4
Bob

Заказать:
0
1
2
10
23
101
205
1000

ААС
Б
Casdsadsa
CSS

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

ВЫБРАТЬ 
 имя_ столбца 
 ОТ 
 table_name 
 ORDER BY 
 имя_ столбца REGEXP '^\d * [^\da-z & \. \'\-\"\!\@\#\$ \%\^\*\(\) \; \: \\, \? \/\ ~\`\ |\_\-] 'DESC, 
 Column_name + 0, 
 Column_name; 
4
Guma

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

... ORDER BY natsort_Canon(column_name, 'natural')
4
Neto Queiroz

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

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
...
*/
3
Tarik

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

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

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

3
antoine

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

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

3
tye

Упрощенная не-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
1
bonger

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

1
Bob Fanger

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

0
Peter V. Mørch