Натуральне сортування в MySQL


81

Чи існує елегантний спосіб ефективного, природного сортування в базі даних MySQL?

Наприклад, якщо у мене є такий набір даних:

  • Final Fantasy
  • Фінальна фантазія 4
  • Фінальна фантазія 10
  • Фінальна фантазія 12
  • Final Fantasy 12: Chains of Promathia
  • Фінальна фантастична пригода
  • Final Fantasy Origins
  • Фінальна фантастична тактика

Будь-яке інше елегантне рішення, окрім поділу назв ігор на їх компоненти

  • Назва : "Final Fantasy"
  • Номер : "12"
  • Підзаголовок : "Ланцюги Проматії"

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

Це дуже боляче, бо час від часу існує інша гра, яка порушує механізм синтаксичного аналізу назви гри (наприклад, "Warhammer 40,000", "James Bond 007")


28
Ланцюги Проматії пов'язані з 11.
Полум'я


Відповіді:


20

Думаю, саме тому багато речей відсортовано за датою випуску.

Рішенням може бути створення в таблиці ще одного стовпця для "SortKey". Це може бути дезінфікована версія заголовка, яка відповідає шаблону, який ви створюєте для зручного сортування, або лічильнику.


Я щойно написав клас саме для цього stackoverflow.com/a/47522040/935122
Крістіан

2
Це, безумовно, правильний підхід, але навряд чи це відповідь сама по собі!
Doin

90

Ось швидке рішення:

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

49
Це добре, якщо все "Final Fantasy", але це ставить "Goofy" попереду набору FF.
fortboise

4
Це рішення не працює постійно. Іноді ламається. Ви повинні краще використовувати цей: stackoverflow.com/a/12257917/384864
Борут Tomazin

6
Шпунтових кладж на ляп: SELECT alphanumeric, integer FROM sorting_test ORDER BY SOUNDEX(alphanumeric), LENGTH(alphanumeric), alphanumeric. Якщо це взагалі працює, це тому, що SOUNDEX зручно відкидає номери, таким чином гарантуючи, що, наприклад, apple1приходить раніше z1.
offby1

чудове рішення, дякую, хоча мені довелося перейти alphanmuric, length(alphanumeric)щоб уникнути "Гуфі" перед "Фінальною фантазією"
Asped

1
Пропозиція @ offby1 працює лише в тому випадку, якщо текст на 100% написаний англійською мовою, оскільки SOUNDEX()призначений для коректної роботи лише з англійськими словами.
Реймонд

56

Щойно знайшов це:

SELECT names FROM your_table ORDER BY games + 0 ASC

Чи може натуральний сорт, коли цифри знаходяться спереду, також спрацювати для середнього.


2
Я не пробував, але серйозно сумніваюся. Причина, по якій він працює з номером спереду, полягає у тому games, що використовується як у числовому контексті і таким чином перетворюється на число перед порівнянням. Якщо в середині він завжди перетвориться на 0, і сортування стане псевдовипадковим.
manixrock

1
Це не природний сорт. Швидше поглянути на цей робочий розчин: stackoverflow.com/a/12257917/384864
Борут Tomazin

@fedir Це добре працювало і для мене. Я навіть не зовсім впевнений, чому саме це працює. Будь-який шанс отримати пояснення?
BizNuge

Просто швидко провели це дослідження, і я зрозумів. Я навіть не підозрював, що MySQL зробить такий вид кастингу, просто використовуючи математичний оператор на рядку! Класна річ полягає в тому, що він просто повертає zer0 у випадку, якщо в передній частині рядка немає цілого числа, щоб "кинути". Дякую за це! ---> ВИБЕРІТЬ АДРЕСУ, (АДРЕСА * 1) як _cast ВІД приміщення, ДО КОТОРОГО ПОШТОВОГО ПОДІБУ ЯК "НЕ1%" ЗАМОВИТИ ЗА АДРЕСОМ * 1 ASC, АДРЕСА МЕЖА 100000;
BizNuge

1
Це насправді не працює, коли цифри посередині, наприклад "Final Fantasy 100" або "Final Fantasy 2". "Final Fantasy 100" покаже перший. Однак це працює, коли ціле число є першим "100 остаточних фантазій"
dwenaus

52

Та сама функція, яку опублікував @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, ".")

5
Це єдине рішення, яке справді працює. Я також тестував код drupals, але іноді він не вдається. Дякую людино!
Борут Томазін

Хтось використовує це на справді великих столах 10+ мільйонів?
Mark Steudel

3
@MarkSteudel Ми використовуємо функцію, подібну до цієї (хоч і не цієї) для природного сортування на кількох таблицях, найбільша з яких ~ 5 мільйонів рядків. Однак ми не називаємо це безпосередньо у своїх запитах, а використовуємо для встановлення значення nat_nameстовпця. Ми використовуємо тригер для запуску функції кожного разу, коли оновлюється рядок. Цей підхід дає вам природне сортування без реальних витрат на продуктивність за рахунок додаткової колонки.
Яків

це працює, сортуючи цифри перед літерами, і може бути реалізовано в Drupal за допомогою hook_views_query_alter, використовуючи щось подібне до цьогоif ($query->orderby[0]["field"] === "node_field_data.title") { $orderBySql = " udf_NaturalSortFormat(node_field_data.title, 10, '.') "; $query->orderby = []; $query->addOrderBy(NULL, $orderBySql, $query->orderby[0]["direction"], 'title_natural'); array_unshift($query->orderby, end($query->orderby)); }
realgt

16

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

@MarkSteudel Вам довелося б спробувати і перевірити це на власні очі. У гіршому випадку ви завжди можете кешувати відформатовані значення. Це, мабуть, я б зробив для великих таблиць, тому що ви також можете проіндексувати поле.
plalx

15

MySQL не дозволяє такого роду "природне сортування", тому, схоже, найкращий спосіб отримати те, що вам потрібно, - це розділити ваші дані, налаштовані, як описано вище (окреме поле ідентифікатора тощо), або невдало що, виконайте сортування на основі елемента без заголовка, індексованого елемента у вашій базі даних (дата, вставлений ідентифікатор у базі даних тощо).

Якщо db виконує сортування для вас, це майже завжди буде швидше, ніж читання великих наборів даних на вибрану вами мову програмування та сортування там, тому, якщо у вас є якийсь контроль над схемою db тут, тоді подивіться на додавання легко сортуються поля, як описано вище, це заощадить вам багато клопоту та обслуговування в довгостроковій перспективі.

Запити на додавання "природного сортування" час від часу виникають на помилках 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", боюся .


9

Отже, хоча я знаю, що ви знайшли задовільну відповідь, я деякий час боровся з цією проблемою, і ми раніше визначили, що це не можна зробити досить добре в SQL, і нам доведеться використовувати javascript у JSON масив.

Ось як я це вирішив, просто використовуючи SQL. Сподіваємось, це корисно для інших:

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

Сцена 1
Сцена 1А
Сцена 1В
Сцена 2А
Сцена 3
...
Сцена 101
Сцена XXA1
Сцена XXA2

Я насправді не "кидав" речі, хоча, гадаю, це теж могло спрацювати.

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

Мій ORDER BYпункт виглядає так:

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

Очевидно, це не допомагає з початковою проблемою, яка була не такою одноманітною, але я думаю, це, мабуть, спрацювало б для багатьох інших пов'язаних проблем, тому викладаючи це там.


LPAD()Натяк був дуже корисним. У мене є слова та цифри для сортування, LPADя міг би сортувати цифри природним чином. А використання CONCATя ігнорую нецифрові. Мій запит виглядає так (псевдонім - це стовпець для сортування): IF(CONCAT("",alias*1)=alias, LPAD(alias,5,"0"), alias) ASC;👍
Kai Noack

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

  2. Використовуйте стовпець "Дата випуску". ORDER BY release_date

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


5

Щодо найкращої відповіді від Річарда Тота 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
;;

ps Я б додав це як коментар до оригіналу, але у мене недостатньо репутації (поки)


4

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

Якщо у вас можуть бути довгі рядки цифр, іншим методом є додавання кількості цифр (фіксованої ширини, до нуля) до кожного рядка цифр. Наприклад, якщо у вас не буде більше 99 цифр поспіль, тоді для "Super Blast 10 Ultra" клавішею сортування буде "Super Blast 0210 Ultra".


4

Для замовлення:
0
1
2
10
23
101
205
1000 ААС б casdsadsa CSS





Використовуйте цей запит:

ВИБЕРІТЬ 
    ім'я_столбця 
ВІД 
    ім'я_таблиці 
СОРТУВАТИ ЗА
    ім'я_столбця REGEXP '^ \ d * [^ \ da-z & \. \' \ - \ "\! \ @ \ # \ $ \% \ ^ \ * \ (\) \; \: \\, \? \ / \ ~ \ `\ | \ _ \ -] 'DESC, 
    ім'я_столбця + 0, 
    ім'я_столбця;

На жаль , це розсипається при додаванні значення в таких , як a1, a2, a11і т.д ...
random_user_name

4

Якщо ви не хочете заново винаходити колесо або болить голова з великою кількістю коду, який не працює, просто використовуйте Drupal Natural Sort ... Просто запустіть SQL, який приходить у архіві (MySQL або Postgre), і все. Роблячи запит, просто замовляйте за допомогою:

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

Дякую за це, я пробував усілякі рішення (ха-ха-ха, подивіться, що я там зробив?), Але жодне з них насправді не працювало з усіма даними, які я мав. Друпальна функція працювала як шарм. Дякуємо за розміщення.
Бен Хічкок

це працює, але сортує числа в кінці (AZ тоді 0-9)
realgt

4

Інший варіант - зробити сортування в пам'яті після витягування даних з mysql. Хоча це не буде найкращим варіантом з точки зору продуктивності, якщо ви не сортуєте величезні списки, ви повинні бути в порядку.

Якщо ви поглянете на пост Джеффа, ви зможете знайти безліч алгоритмів для будь-якої мови, з якою ви могли б працювати. Сортування для людей: природний порядок сортування


2

Ви також можете динамічно створити "стовпець сортування":

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

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


Не знаю, наскільки це було б ефективно. Я використовую його весь час без будь-яких незручностей. Моя база даних не велика.
antoine

1

Якщо ви використовуєте 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 року, тому я не затамував дух.


Теоретично це можливо, але мені спочатку потрібно було б прочитати всі записи бази даних на своєму веб-сервері.
BlaM

Крім того, розглянемо: usort($mydata, function ($item1, $item2) { return strnatcmp($item1['key'], $item2['key']); });(У мене є асоціативний масив і сортування за ключем.) Ref: stackoverflow.com/q/12426825/1066234
Кай Ноак

1

Спрощена версія не найкращої відповіді @ 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

Я випробував кілька рішень, але насправді це дуже просто:

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

1
Дуже добре працює для сортування чисел у форматі 23-4244. Дякую :)
Pyton

1
працює лише з цими тестовими даними, оскільки рядки перед числом однакові. Спробуйте додати значення z_99там, і воно буде поставлено вгорі, але zз’явиться після v.
Семюель Нефф

@SamuelNeff, будь ласка, дивіться SQL: ЗАМОВЛЕННЯ ЗА ДОЛЖИНОЮ (test_column) DESC, test_column DESC, так що так, тому що це буде відсортоване за довжиною стовпця першим. Це добре працює, сортуючи групу префіксів таблиці, яку в іншому випадку ви не змогли б відсортувати лише за допомогою "test_column DESC"
Тарік

1

Багато інших відповідей, які я бачу тут (і в дублікатах запитань), в основному працюють лише для дуже спеціально відформатованих даних, наприклад, для рядка, який цілком є ​​числом, або для якого існує алфавітний префікс фіксованої довжини. У загальному випадку це не спрацює.

Це правда, що насправді не існує способу реалізації 100% загального nat-sort у MySQL, оскільки для того, щоб зробити те, що вам насправді потрібно, це модифікована функція порівняння , яка перемикається між лексикографічним сортуванням рядків та числовим сортуванням, якщо / коли він зустрінеться число. Такий код може реалізувати будь-який алгоритм, який ви можете побажати для розпізнавання та порівняння числових частин у двох рядках. На жаль, однак функція порівняння в MySQL є внутрішньою до свого коду і не може бути змінена користувачем.

Це залишає якийсь злом, коли ви намагаєтеся створити ключ сортування для вашого рядка, в якому числові частини переформатуються так, що стандартне лексикографічне сортування насправді сортує їх так, як ви хочете .

Для простих цілих чисел до деякої максимальної кількості цифр очевидним рішенням є просто залишити їх нулями так, щоб усі вони мали фіксовану ширину. Це підхід, застосований плагіном Drupal та рішеннями @plalx / @RichardToth. (@Christian має інше та набагато складніше рішення, але я не бачу жодних переваг).

Як зазначає @tye, ви можете покращити це, додавши до кожного числа фіксовану довжину цифр, а не просто заповнюючи його. Існує набагато, набагато більше, що ви можете вдосконалити, хоча, навіть з огляду на обмеження, що по суті є незграбним хаком. Проте, схоже, немає жодних заздалегідь побудованих рішень!

Наприклад, як щодо:

  • Знаки плюс і мінус? +10 проти 10 проти -10
  • Десяткові крапки? 8,2, 8,5, 1,006,, 75
  • Провідні нулі? 020, 030, 00000922
  • Тисячі сепараторів? "1001 Далмація" проти "1001 Далмація"
  • Номери версій? MariaDB v10.3.18 проти MariaDB v10.3.3
  • Дуже довгі цифри? 103 768 276 592 092 364 859 236 487 687 870 234 598,55

Поширюючись на метод @ tye, я створив досить компактну збережену функцію NatSortKey (), яка перетворить довільний рядок у ключ nat-sort і яка обробляє всі вищезазначені випадки, є достатньо ефективною і зберігає загальну сорту- порядок (жоден два різні рядки не мають ключів сортування, що порівнюють рівні). Другий параметр може бути використаний для обмеження кількості оброблених чисел у кожному рядку (наприклад, для перших 10 чисел, скажімо), який може бути використаний для забезпечення відповідності вихідних даних у межах заданої довжини.

ПРИМІТКА: Рядок ключа сортування, згенерований із заданим значенням цього 2-го параметра, слід сортувати лише за іншими рядками, згенерованими з однаковим значенням для параметра, інакше вони можуть неправильно сортувати!

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

SELECT myString FROM myTable ORDER BY NatSortKey(myString,0);  ### 0 means process all numbers - resulting sort key might be quite long for certain inputs

Але для ефективного сортування великих таблиць краще попередньо зберегти ключ сортування в іншому стовпці (можливо, з індексом на ньому):

INSERT INTO myTable (myString,myStringNSK) VALUES (@theStringValue,NatSortKey(@theStringValue,10)), ...
...
SELECT myString FROM myTable ORDER BY myStringNSK;

[В ідеалі, ви могли б зробити це автоматично, створивши стовпець ключа як обчислюваний збережений стовпець, використовуючи щось на зразок:

CREATE TABLE myTable (
...
myString varchar(100),
myStringNSK varchar(150) AS (NatSortKey(myString,10)) STORED,
...
KEY (myStringNSK),
...);

Але наразі ні MySQL, ні MariaDB не дозволяють зберігати функції в обчислюваних стовпцях , тому, на жаль, ви поки цього не можете зробити .]


Моя функція впливає лише на сортування чисел . Якщо ви хочете зробити іншу сортування нормування речі, такими як видалення всіх знаків пунктуації, або обрізки порожнього простору з кожним кінця, або заміною декількома пробільних послідовностей з пробілами, ви можете або розширити цю функцію, або це може бути зроблено до або після того, як NatSortKey()IS застосовано до ваших даних. (Я б рекомендував використовувати REGEXP_REPLACE()для цієї мети).

Це також дещо англоцентрично, на мою думку ''. для десяткової коми і ',' для роздільника тисяч, але це повинно бути досить легко змінити, якщо ви хочете зворотне, або якщо ви хочете, щоб це було перемикається як параметр.

Це може бути піддане подальшому вдосконаленню іншими способами; наприклад, в даний час він сортує від'ємні числа за абсолютним значенням, тому -1 приходить перед -2, а не навпаки. Також немає можливості вказати порядок сортування DESC для чисел, зберігаючи при цьому лексикографічне сортування ASC для тексту. Обидві ці проблеми можна вирішити трохи більше роботи; Я оновлю код, якщо / коли знайду час.

Існує безліч інших деталей, про які слід пам’ятати, включаючи деякі критичні залежності від набору та сортування, які ви використовуєте, але я вклав їх усі в блок коментарів у коді SQL. Будь ласка, уважно прочитайте це, перш ніж використовувати функцію для себе!

Отже, ось код. Якщо ви виявили помилку або маєте покращення, про яке я не згадував, повідомте про це в коментарях!


delimiter $$
CREATE DEFINER=CURRENT_USER FUNCTION NatSortKey (s varchar(100), n int) RETURNS varchar(350) DETERMINISTIC
BEGIN
/****
  Converts numbers in the input string s into a format such that sorting results in a nat-sort.
  Numbers of up to 359 digits (before the decimal point, if one is present) are supported.  Sort results are undefined if the input string contains numbers longer than this.
  For n>0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves).
  Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n.
  Numbers may contain ',' as a thousands separator, and '.' as a decimal point.  To reverse these (as appropriate for some European locales), the code would require modification.
  Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign.
  Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001).
  Numbers with leading zeros sort after the same number with no (or fewer) leading zeros.
  Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character.
  Numbers with thousand separators sort after the same number without them.
  Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly.
  (When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers).
  Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted.
  The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11)
  Note that The first number component in an entity like this is also permitted to contain thousand separators.

  To achieve this, numbers within the input string are prefixed and suffixed according to the following format:
  - The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros.  If there is a decimal point, this length only includes the integer part of the number.
  - A 3-character suffix is appended after the number (after the decimals if present).
    - The first character is a space, or a '+' sign if the number was preceded by '+'.  Any preceding '+' sign is also removed from the front of the number.
    - This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25)
    - The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise)
  - For version number sequences, each component number has the prefix in front of it, and the separating dots are removed.
    Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence.

  e.g. here is how some simple sample strings get converted:
  'Foo055' --> 'Foo0255 02'
  'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade'
  'The $1,000,000 prize' --> 'The $071000000 01 prize'
  '+99.74 degrees' --> '0299.74+00 degrees'
  'I have 0 apples' --> 'I have 00 02 apples'
  '.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08'
  'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004'

  The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal.
  The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix.
  A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789.
  However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length.

  The function definition does not specify the charset or collation to be used for string-type parameters or variables:  The default database charset & collation at the time the function is defined will be used.
  This is to make the function code more portable.  However, there are some important restrictions:

  - Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work.
    This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia).
    To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use.

  - For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters.
    This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4).
    If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH()

  Length of the output:

  Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert;
  This parameter is provided as a means to limit the maximum output length (to input length + 5*n).
  If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n.
  Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2))
  So for the current input length of 100, the maximum output length is 350.
  If changing the input length, the output length must be modified according to the above formula.  The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate.
****/
  DECLARE x,y varchar(100);            # need to be same length as input s
  DECLARE r varchar(350) DEFAULT '';   # return value:  needs to be same length as return type
  DECLARE suf varchar(101);   # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
  DECLARE i,j,k int UNSIGNED;
  IF n<=0 THEN SET n := -1; END IF;   # n<=0 means "process all numbers"
  LOOP
    SET i := REGEXP_INSTR(s,'\\d');   # find position of next digit
    IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF;   # no more numbers to process -> we're done
    SET n := n-1, suf := ' ';
    IF i>1 THEN
      IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF;   # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control"
      IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF;   # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together
      SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i);   # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s)
    END IF;
    SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+'));   # capture the number + following decimals (including multiple consecutive '.<digits>' sequences)
    SET s := SUBSTRING(s,LENGTH(x)+1);   # NOTE: LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8)
    SET i := INSTR(x,'.');
    IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF;   # move any following decimals into y
    SET i := LENGTH(x);
    SET x := REPLACE(x,',','');
    SET j := LENGTH(x);
    SET x := TRIM(LEADING '0' FROM x);   # strip leading zeros
    SET k := LENGTH(x);
    SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0'));   # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise)  Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters
    SET i := LOCATE('.',y,2);
    IF i=0 THEN
      SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf);   # k = count of digits in number, bounded to be <= '9Z' base-36
    ELSE   # encode a version number (like 3.12.707, etc)
      SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);   # k = count of digits in number, bounded to be <= '9Z' base-36
      WHILE LENGTH(y)>0 AND n!=0 DO
        IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF;
        SET j := LENGTH(x);
        SET x := TRIM(LEADING '0' FROM x);   # strip leading zeros
        SET k := LENGTH(x);
        SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);   # k = count of digits in number, bounded to be <= '9Z' base-36
        SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0'));   # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits
        SET n := n-1;
      END WHILE;
      SET r := CONCAT(r,y,suf);
    END IF;
  END LOOP;
END
$$
delimiter ;

Я новачок у MySQL і спробував це. З'явилась така помилка: "# 1305 - ФУНКЦІЯ mydatabase.REGEXP_INSTR не існує". Будь-яка ідея?
Джон Т

Для будь-якого іншого новачка. У мене не було встановлено MySQL 8.0. Це потрібно для REGEXP_INSTR (та інших матеріалів REGEXP).
Джон Т

Щойно виправили серйозну помилку в NatSortKey: був неправильний символ регулярного виразу. Якщо ви користувались цією функцією самостійно, оновіть свій код!
Закінчено


0

Ось простий варіант, якщо заголовки мають лише версію як номер:

ORDER BY CAST(REGEXP_REPLACE(title, "[a-zA-Z]+", "") AS INT)';

В іншому випадку ви можете використовувати простий SQL, якщо використовуєте шаблон (цей шаблон використовує # перед версією):

create table titles(title);

insert into titles (title) values 
('Final Fantasy'),
('Final Fantasy #03'),
('Final Fantasy #11'),
('Final Fantasy #10'),
('Final Fantasy #2'),
('Bond 007 ##2'),
('Final Fantasy #01'),
('Bond 007'),
('Final Fantasy #11}');

select REGEXP_REPLACE(title, "#([0-9]+)", "\\1") as title from titles
ORDER BY REGEXP_REPLACE(title, "#[0-9]+", ""),
CAST(REGEXP_REPLACE(title, ".*#([0-9]+).*", "\\1") AS INT);     
+-------------------+
| title             |
+-------------------+
| Bond 007          |
| Bond 007 #2       |
| Final Fantasy     |
| Final Fantasy 01  |
| Final Fantasy 2   |
| Final Fantasy 03  |
| Final Fantasy 10  |
| Final Fantasy 11  |
| Final Fantasy 11} |
+-------------------+
8 rows in set, 2 warnings (0.001 sec)

За потреби можна використовувати інші візерунки. Наприклад, якщо у вас є фільм "Я №1" і "Я №1, частина 2", тоді, можливо, оберніть версію, наприклад "Final Fantasy {11}"


-4

Я знаю, що ця тема давня, але я думаю, що знайшов спосіб зробити це:

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: Advent Children Final Fantasy 12 Final Fantasy 112 FF1 FF2


3
чому б не видалити цю відповідь? ви отримаєте за це значок
m47730,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.