Багато інших відповідей, які я бачу тут (і в дублікатах запитань), в основному працюють лише для дуже спеціально відформатованих даних, наприклад, для рядка, який цілком є числом, або для якого існує алфавітний префікс фіксованої довжини. У загальному випадку це не спрацює.
Це правда, що насправді не існує способу реалізації 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);
Але для ефективного сортування великих таблиць краще попередньо зберегти ключ сортування в іншому стовпці (можливо, з індексом на ньому):
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
DECLARE x,y varchar(100);
DECLARE r varchar(350) DEFAULT '';
DECLARE suf varchar(101);
DECLARE i,j,k int UNSIGNED;
IF n<=0 THEN SET n := -1; END IF;
LOOP
SET i := REGEXP_INSTR(s,'\\d');
IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF;
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;
IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF;
SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i);
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++)*+'));
SET s := SUBSTRING(s,LENGTH(x)+1);
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;
SET i := LENGTH(x);
SET x := REPLACE(x,',','');
SET j := LENGTH(x);
SET x := TRIM(LEADING '0' FROM x);
SET k := LENGTH(x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0'));
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);
ELSE
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
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);
SET k := LENGTH(x);
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x);
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0'));
SET n := n-1;
END WHILE;
SET r := CONCAT(r,y,suf);
END IF;
END LOOP;
END
$$
delimiter ;