Naturalne sortowanie w MySQL

81

Czy istnieje elegancki sposób na wydajne, naturalne sortowanie w bazie danych MySQL?

Na przykład, jeśli mam taki zestaw danych:

  • Final Fantasy
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Chains of Promathia
  • Final Fantasy Adventure
  • Final Fantasy Origins
  • Final Fantasy Tactics

Każde inne eleganckie rozwiązanie niż podzielenie nazw gier na ich składniki

  • Tytuł : „Final Fantasy”
  • Liczba : „12”
  • Podtytuł : „Chains of Promathia”

aby upewnić się, że wychodzą we właściwej kolejności? (10 po 4, nie przed 2).

Takie postępowanie jest uciążliwe dla a **, ponieważ od czasu do czasu pojawia się inna gra, która łamie ten mechanizm parsowania tytułu gry (np. „Warhammer 40 000”, „James Bond 007”)

BlaM
źródło
28
Chains of Promathia jest powiązany z 11.
Flame
Związane z: stackoverflow.com/questions/48600059/…
Paul Spiegel

Odpowiedzi:

20

Myślę, że dlatego wiele rzeczy jest posortowanych według daty wydania.

Rozwiązaniem mogłoby być utworzenie w tabeli kolejnej kolumny dla „SortKey”. Może to być oczyszczona wersja tytułu, która jest zgodna ze wzorem utworzonym w celu łatwego sortowania lub licznika.

Michael Haren
źródło
Właśnie napisałem klasę dla dokładnie tego stackoverflow.com/a/47522040/935122
Christian
2
Jest to zdecydowanie właściwe podejście, ale samo w sobie nie jest odpowiedzią!
Doin
90

Oto szybkie rozwiązanie:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
slotishtype
źródło
49
Fajnie, jeśli wszystko jest „Final Fantasy”, ale stawia „Goofy” przed pakietem FF.
fortboise
4
To rozwiązanie nie działa cały czas. Czasami pęka. Powinieneś raczej użyć tego: stackoverflow.com/a/12257917/384864
Borut Tomazin
6
Palowania kludge na kludge: SELECT alphanumeric, integer FROM sorting_test ORDER BY SOUNDEX(alphanumeric), LENGTH(alphanumeric), alphanumeric. Jeśli to w ogóle zadziała, to dlatego, że SOUNDEX wygodnie odrzuca liczby, zapewniając w ten sposób, że np . Występuje apple1wcześniej z1.
offby 1
świetne rozwiązanie, dzięki, chociaż musiałem to zrobić alphanmuric, length(alphanumeric)żeby uniknąć "Goofy" przed "Final Fantasy"
Asped
1
@ offby1 sugestia działa tylko wtedy, gdy tekst jest w 100% napisany po angielsku, ponieważ SOUNDEX()jest zaprojektowany tak, aby działał poprawnie tylko ze słowami angielskimi.
Raymond Nijland
56

Właśnie to znalazłem:

SELECT names FROM your_table ORDER BY games + 0 ASC

Wykonuje naturalne sortowanie, gdy liczby są z przodu, może również działać dla środka.


źródło
2
Nie próbowałem tego, ale poważnie w to wątpię. Powodem, dla którego działa z liczbą na początku, jest to, że gamesjest używany jako kontekst liczbowy, a zatem jest konwertowany na liczbę przed porównaniem. Jeśli w środku to zawsze będzie konwertowane na 0, a sortowanie stanie się pseudolosowe.
manixrock
1
To nie jest naturalny rodzaj. Raczej spójrz na to działające rozwiązanie: stackoverflow.com/a/12257917/384864
Borut Tomazin
@fedir To też działało dobrze dla mnie. Nie jestem nawet do końca pewien, dlaczego to działa. Jakaś szansa na wytłumaczenie?
BizNuge
Właśnie zbadałem to szybko i rozumiem. Nawet nie zdawałem sobie sprawy, że MySQL może wykonać tego rodzaju rzutowanie po prostu używając operatora matematycznego na łańcuchu! Fajne jest to, że po prostu zwraca zero w przypadku, gdy na początku łańcucha nie ma liczby całkowitej do „rzutowania”. Dzięki za to! ---> WYBIERZ ADRES, (ADRES * 1) jako _cast Z lokalu GDZIE KOD POCZTOWY JAK 'NE1%' ZAMÓWIENIE WEDŁUG ADRESU * 1 ASC, ADRES LIMIT 100000;
BizNuge
1
To faktycznie nie działa, gdy liczby są pośrodku, na przykład „Final Fantasy 100” lub „Final Fantasy 2”. Jako pierwsza pokaże się „Final Fantasy 100”. Działa jednak, gdy pierwsza liczba całkowita to „100 Final Fantasy”
dwenaus
52

Ta sama funkcja, którą opublikował @plalx, ​​ale przepisana do 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
;;

Stosowanie:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")
Richard Toth
źródło
5
To jedyne rozwiązanie, które naprawdę działa. Przetestowałem również kod Drupals, ale czasami zawodzi. Dzięki!
Borut Tomazin
Czy ktoś używa tego na naprawdę dużych stołach 10+ milionów?
Mark Steudel
3
@MarkSteudel Używamy funkcji podobnej do tej (choć nie tej dokładnej) do naturalnego sortowania na kilku tabelach, z których największa ma ~ 5 milionów wierszy. Jednak nie wywołujemy go bezpośrednio w naszych zapytaniach, ale zamiast tego używamy go do ustawiania wartości nat_namekolumny. Używamy wyzwalacza do uruchamiania funkcji za każdym razem, gdy wiersz jest aktualizowany. Takie podejście zapewnia naturalne sortowanie bez rzeczywistych kosztów wydajności kosztem dodatkowej kolumny.
Jacob
to działa, sortuje liczby przed literami i można je zaimplementować w Drupalu za pomocą hook_views_query_alter, używając czegoś podobnego do tegoif ($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

Napisałem tę funkcję dla MSSQL 2000 jakiś czas temu:

/**
 * 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
plalx
źródło
@MarkSteudel Musiałbyś spróbować i przetestować to sam. W gorszym przypadku zawsze możesz buforować sformatowane wartości. Prawdopodobnie zrobiłbym to w przypadku dużych tabel, ponieważ możesz również zindeksować pole.
plalx
15

MySQL nie pozwala na tego rodzaju „naturalne sortowanie”, więc wygląda na to, że najlepszym sposobem na uzyskanie tego, czego szukasz, jest podzielenie danych w sposób opisany powyżej (oddzielne pole identyfikatora itp.) że wykonaj sortowanie na podstawie elementu niebędącego tytułem, elementu indeksowanego w Twojej bazie danych (data, identyfikator wstawiony w bazie danych itp.).

Posortowanie bazy danych przez bazę danych prawie zawsze będzie szybsze niż wczytywanie dużych zestawów danych do wybranego języka programowania i sortowanie ich tam, więc jeśli masz jakąkolwiek kontrolę nad schematem db tutaj, to spójrz na dodanie pola, które można łatwo posortować, jak opisano powyżej, na dłuższą metę zaoszczędzi wiele kłopotów i konserwacji.

Prośby o dodanie „naturalnego sortowania” pojawiają się od czasu do czasu na błędach MySQL i na forach dyskusyjnych , a wiele rozwiązań obraca się wokół usuwania określonych części danych i rzutowania ich na ORDER BYczęść zapytania, np.

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

Tego rodzaju rozwiązanie mogłoby po prostu zadziałać na powyższym przykładzie Final Fantasy, ale nie jest szczególnie elastyczne i mało prawdopodobne, aby obejmował zestaw danych zawierający, powiedzmy, „Warhammer 40 000” i „James Bond 007”. Obawiam się, że .

Conroy P.
źródło
9

Tak więc, chociaż wiem, że znalazłeś satysfakcjonującą odpowiedź, przez jakiś czas zmagałem się z tym problemem, a wcześniej ustaliliśmy, że nie można tego zrobić wystarczająco dobrze w SQL i będziemy musieli użyć javascript na JSON szyk.

Oto, jak rozwiązałem to za pomocą SQL. Mamy nadzieję, że jest to pomocne dla innych:

Miałem takie dane jak:

Scena 1
Scena 1A
Scena 1B
Scena 2A
Scena 3
...
Scena 101
Scena XXA1
Scena XXA2

Właściwie nie „rzucałem” rzeczy, chociaż przypuszczam, że to też mogło zadziałać.

Najpierw wymieniłem części, które były niezmienne w danych, w tym przypadku „Scena”, a następnie wykonałem LPAD, aby wyrównać. Wydaje się, że umożliwia to całkiem dobre sortowanie ciągów alfa, podobnie jak ciągów numerowanych.

Moja ORDER BYklauzula wygląda następująco:

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

Oczywiście nie pomaga to w pierwotnym problemie, który nie był tak jednolity - ale wyobrażam sobie, że prawdopodobnie zadziałaby w przypadku wielu innych powiązanych problemów, więc umieść to tam.

FilmJ
źródło
LPAD()Wskazówka była bardzo pomocna. Mam słowa i liczby do sortowania, a liczby LPADmogę sortować naturalnie. Używając CONCATignoruję nieliczby. Moje zapytanie wygląda następująco (alias jest kolumną do sortowania): IF(CONCAT("",alias*1)=alias, LPAD(alias,5,"0"), alias) ASC;👍
Kai Noack
6
  1. Dodaj klucz sortowania (ranking) do swojej tabeli. ORDER BY rank

  2. Wykorzystaj kolumnę „Data wydania”. ORDER BY release_date

  3. Podczas wyodrębniania danych z SQL, spraw, aby twój obiekt przeprowadził sortowanie, np. Jeśli wyodrębniasz do zestawu, uczyń go zestawem drzewa i spraw, aby twój model danych zaimplementował porównywalną i wprowadził tutaj algorytm sortowania naturalnego (sortowanie przez wstawianie wystarczy język bez kolekcji), ponieważ będziesz czytać wiersze z SQL jeden po drugim podczas tworzenia modelu i wstawiania go do kolekcji)

JeeBee
źródło
5

Odnośnie najlepszej odpowiedzi Richarda Totha https://stackoverflow.com/a/12257917/4052357

Uważaj na ciągi zakodowane w UTF8, które zawierają 2-bajtowe (lub więcej) znaki i cyfry, np

12 南新宿

Użycie funkcji MySQL LENGTH()w udf_NaturalSortFormatfunkcji zwróci długość w bajtach łańcucha i będzie nieprawidłowe, zamiast tego użyj funkcji, CHAR_LENGTH()która zwróci prawidłową długość znaku.

W moim przypadku użycie LENGTH()spowodowanych zapytań nigdy się nie kończy i skutkuje 100% wykorzystaniem procesora przez 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 Dodałbym to jako komentarz do oryginału, ale nie mam wystarczającej reputacji (jeszcze)

Luke Hoggett
źródło
4

Dodaj pole dla „klucza sortowania”, które ma wszystkie ciągi cyfr dopełnione zerami do określonej długości, a następnie posortuj je według tego pola.

Jeśli możesz mieć długie ciągi cyfr, inną metodą jest dołączenie liczby cyfr (o stałej szerokości, uzupełnione zerami) do każdego ciągu cyfr. Na przykład, jeśli nie masz więcej niż 99 cyfr w rzędzie, to dla „Super Blast 10 Ultra” kluczem sortowania będzie „Super Blast 0210 Ultra”.

tye
źródło
4

Na zamówienie:
0
1
2
10
23
101
205
1000
a
aac
b
casdsadsa
css

Użyj tego zapytania:

WYBIERZ 
    Nazwa kolumny 
OD 
    Nazwa tabeli 
ZAMÓW PRZEZ
    nazwa_kolumny REGEXP '^ \ d * [^ \ da-z & \. \' \ - \ "\! \ @ \ # \ $ \% \ ^ \ * \ (\) \; \: \\, \? \ / \ ~ \ `\ | \ _ \ -] 'DESC, 
    nazwa_kolumny + 0, 
    Nazwa kolumny;
Guma
źródło
Niestety ten rozbija jeśli dodać wartości, takich jak a1, a2, a11, itp ...
random_user_name
4

Jeśli nie chcesz wymyślać koła na nowo lub boli cię głowa z dużą ilością kodu, który nie działa, po prostu użyj Drupal Natural Sort ... Po prostu uruchom SQL, który jest spakowany (MySQL lub Postgre) i to wszystko. Składając zapytanie, wystarczy zamówić za pomocą:

... ORDER BY natsort_canon(column_name, 'natural')
Neto Queiroz
źródło
Dzięki za to, próbowałem różnych rozwiązań (ha ha widzisz, co tam zrobiłem?), Ale żadne z nich tak naprawdę nie działało dla wszystkich danych, które miałem. Funkcja drupal działała jak urok. Dzięki za wysłanie wiadomości.
Ben Hitchcock
to działa, ale sortuje liczby na końcu (AZ, a następnie 0-9)
realgt
4

Inną opcją jest sortowanie w pamięci po ściągnięciu danych z mysql. Chociaż nie będzie to najlepsza opcja z punktu widzenia wydajności, jeśli nie sortujesz dużych list, powinno być dobrze.

Jeśli spojrzysz na post Jeffa, możesz znaleźć wiele algorytmów dla każdego języka, z którym możesz pracować. Sortowanie dla ludzi: naturalny porządek sortowania

Pion
źródło
2

Możesz także dynamicznie utworzyć „kolumnę sortowania”:

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

W ten sposób możesz tworzyć grupy do sortowania.

W moim zapytaniu chciałem, aby przed wszystkim znajdowało się „-”, potem cyfry, a potem tekst. Co może spowodować coś takiego:

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

W ten sposób nie musisz utrzymywać kolumny sortowania we właściwej kolejności podczas dodawania danych. Możesz także zmienić kolejność sortowania w zależności od potrzeb.

antoine
źródło
Nie wiem, jak wydajne byłoby to. Używam go cały czas bez żadnych niedogodności. Moja baza danych nie jest jednak duża.
antoine
1

Jeśli używasz PHP, możesz wykonać naturalne sortowanie w 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]; 
}

Mam nadzieję, że MySQL zaimplementuje naturalne sortowanie w przyszłej wersji, ale żądanie funkcji (nr 1588) jest otwarte od 2003 roku, więc nie wstrzymywałbym oddechu.

Bob Fanger
źródło
Teoretycznie jest to możliwe, ale najpierw musiałbym odczytać wszystkie rekordy bazy danych na moim serwerze internetowym.
BlaM
Alternatywnie rozważ: usort($mydata, function ($item1, $item2) { return strnatcmp($item1['key'], $item2['key']); });(Mam tablicę asocjacyjną i sortuj według klucza.) Ref: stackoverflow.com/q/12426825/1066234
Kai Noack
1

Uproszczona wersja najlepszej odpowiedzi @ plaix / Richard Toth / Luke Hoggett bez udf, która działa tylko dla pierwszej liczby całkowitej w polu, to

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
bonger
źródło
1

Wypróbowałem kilka rozwiązań, ale w rzeczywistości jest to bardzo proste:

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
...
*/
Tarik
źródło
1
Bardzo dobrze sprawdza się przy sortowaniu liczb w formacie 23-4244. Dzięki :)
Pyton
1
działa tylko z tymi danymi testowymi, ponieważ ciągi znaków przed liczbą są takie same. Spróbuj wprowadzić z_99tam wartość, a zostanie ona umieszczona na górze, ale znastąpi później v.
Samuel Neff,
@SamuelNeff zobacz SQL: ORDER BY LENGTH (kolumna_testowa) DESC, kolumna_testowa DESC, więc tak, ponieważ najpierw zostanie posortowana według długości kolumny. To działa dobrze, sortując przedrostek grupy tabeli, której w innym przypadku nie byłbyś w stanie posortować tylko za pomocą „test_column DESC”
Tarik
1

Wiele innych odpowiedzi, które widzę tutaj (i w zduplikowanych pytaniach) w zasadzie działa tylko dla bardzo specyficznie sformatowanych danych, np. Ciągu, który jest w całości liczbą lub dla którego istnieje prefiks alfabetyczny o stałej długości. W ogólnym przypadku to nie zadziała.

To prawda, że ​​tak naprawdę nie ma sposobu na zaimplementowanie 100% ogólnego sortowania nat w MySQL, ponieważ aby to zrobić, naprawdę potrzebujesz zmodyfikowanej funkcji porównania , która przełącza się między leksykograficznym sortowaniem ciągów i sortowaniem numerycznym, jeśli / kiedy napotka numer. Taki kod mógłby zaimplementować dowolny algorytm rozpoznawania i porównywania części numerycznych w dwóch ciągach. Niestety, funkcja porównawcza w MySQL jest wewnętrzna dla jego kodu i użytkownik nie może jej zmienić.

Pozostawia to pewnego rodzaju hack, w którym próbujesz utworzyć klucz sortowania dla swojego ciągu, w którym części numeryczne są ponownie formatowane, tak że standardowe sortowanie leksykograficzne faktycznie sortuje je tak, jak chcesz .

W przypadku zwykłych liczb całkowitych do pewnej maksymalnej liczby cyfr oczywistym rozwiązaniem jest po prostu wypełnienie ich zerami w lewo, aby wszystkie miały stałą szerokość. Takie jest podejście zastosowane przez wtyczkę Drupal i rozwiązania @plalx / @RichardToth. (@Christian ma inne i znacznie bardziej złożone rozwiązanie, ale nie oferuje żadnych korzyści, które widzę).

Jak wskazuje @tye, możesz to poprawić, dodając stałą liczbę cyfr do każdej liczby, zamiast po prostu dopełniać ją w lewo. Można jednak ulepszyć o wiele, wiele więcej rzeczy, nawet biorąc pod uwagę ograniczenia tego, co w zasadzie jest niezręcznym hackiem. Jednak wydaje się, że nie ma żadnych gotowych rozwiązań!

Na przykład co z:

  • Znaki plus i minus? +10 przeciwko 10 przeciwko -10
  • Ułamki dziesiętne? 8,2, 8,5, 1,006, 0,75
  • Wiodące zera? 020, 030, 00000922
  • Tysiące separatorów? „1001 dalmatów” kontra „1001 dalmatów”
  • Numery wersji? MariaDB 10.3.18 vs MariaDB 10.3.3
  • Bardzo długie liczby? 103.768.276.592.092.364.859.236.487.687.870.234.598,55

Rozszerzając metodę @ tye, stworzyłem dość zwartą funkcję przechowywaną NatSortKey (), która konwertuje dowolny ciąg na klucz nat-sort i która obsługuje wszystkie powyższe przypadki, jest dość wydajna i zachowuje całkowite sortowanie kolejność (żadne dwa różne ciągi nie mają równych kluczy sortowania). Drugi parametr może być użyty do ograniczenia liczby liczb przetwarzanych w każdym łańcuchu (np. Do pierwszych 10 liczb), co może służyć do zapewnienia, że ​​dane wyjściowe mieszczą się w zadanej długości.

UWAGA: Łańcuch klucza sortowania wygenerowany z podaną wartością tego drugiego parametru powinien być sortowany tylko względem innych ciągów wygenerowanych z tą samą wartością parametru, w przeciwnym razie mogą one nie być sortowane poprawnie!

Możesz z niego skorzystać bezpośrednio w zamówieniu np

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

Ale w celu efektywnego sortowania dużych tabel lepiej jest wstępnie przechowywać klucz sortowania w innej kolumnie (prawdopodobnie z indeksem):

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

[Idealnie byłoby, gdyby stało się to automatycznie, tworząc kolumnę kluczową jako obliczoną przechowywaną kolumnę, używając czegoś takiego jak:

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

Ale na razie ani MySQL, ani MariaDB nie pozwalają na przechowywanie funkcji w kolumnach obliczeniowych , więc niestety nie możesz jeszcze tego zrobić .]


Moja funkcja wpływa tylko na sortowanie liczb . Jeśli chcesz robić inne rzeczy sort-normalizacja, takich jak usuwanie wszystkich znaków interpunkcyjnych lub przycinanie spacje przy każdym końcu, albo zastępując sekwencje wielu białych znaków z pojedynczymi odstępami, można też rozszerzyć funkcję, czy można to zrobić przed lub po NatSortKey()Is zastosowane do Twoich danych. (Polecam użycie REGEXP_REPLACE()w tym celu).

Zakładam również, że jest nieco anglo-centryczny ”. dla przecinka dziesiętnego i „,” dla separatora tysięcy, ale modyfikacja powinna być łatwa, jeśli chcesz, aby odwrotność była odwrotna lub jeśli chcesz, aby była przełączalna jako parametr.

Może być możliwe dalsze ulepszanie w inny sposób; na przykład obecnie sortuje liczby ujemne według wartości bezwzględnej, więc -1 występuje przed -2, a nie na odwrót. Nie ma również możliwości określenia kolejności sortowania DESC dla liczb przy zachowaniu sortowania leksykograficznego ASC dla tekstu. Oba te problemy można rozwiązać przy odrobinie pracy; Zaktualizuję kod, jeśli / kiedy otrzymam czas.

Jest wiele innych szczegółów, o których należy pamiętać - w tym niektóre krytyczne zależności od używanego zestawu i sortowania - ale umieściłem je wszystkie w bloku komentarzy w kodzie SQL. Przeczytaj uważnie przed użyciem tej funkcji!

Oto kod. Jeśli znajdziesz błąd lub poprawę, o której nie wspomniałem, daj mi znać w komentarzach!


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 ;
Doin
źródło
Jestem początkującym użytkownikiem MySQL i próbowałem tego. Pojawił się ten błąd: „# 1305 - FUNKCJA moja baza danych.REGEXP_INSTR nie istnieje”. Dowolny pomysł?
John T
Dla każdego innego nowicjusza. Nie miałem zainstalowanego MySQL 8.0. Jest potrzebny do REGEXP_INSTR (i innych rzeczy związanych z REGEXP).
John T
Właśnie naprawiono poważny błąd w NatSortKey: wystąpił nieprawidłowy znak regex. Jeśli sam korzystałeś z tej funkcji, zaktualizuj swój kod!
Doin
0

Oto prosty przykład, jeśli tytuły mają wersję tylko jako numer:

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

W przeciwnym razie możesz użyć prostego SQL, jeśli używasz wzorca (ten wzorzec używa # przed wersją):

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)

W razie potrzeby możesz użyć innych wzorów. Na przykład, jeśli masz film „Jestem numerem 1” i „Jestem numerem 1, część 2”, może opakuj wersję, np. „Final Fantasy {11}”

Frank Forte
źródło
-4

Wiem, że ten temat jest starożytny, ale myślę, że znalazłem sposób, aby to zrobić:

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

Złom, że nieprawidłowo posortował następujący zestaw (to bezużyteczne lol):

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

user1467716
źródło
3
dlaczego nie usunąć tej odpowiedzi? dostaniesz za to odznakę
m47730