Natürliche Sortierung in MySQL

80

Gibt es eine elegante Möglichkeit, eine performante, natürliche Sortierung in einer MySQL-Datenbank durchzuführen?

Zum Beispiel, wenn ich diesen Datensatz habe:

  • Final Fantasy
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Ketten der Promathie
  • Final Fantasy Abenteuer
  • Final Fantasy Origins
  • Final Fantasy Tactics

Jede andere elegante Lösung, als die Namen der Spiele in ihre Komponenten aufzuteilen

  • Titel : "Final Fantasy"
  • Nummer : "12"
  • Untertitel : "Ketten der Promathie"

um sicherzustellen, dass sie in der richtigen Reihenfolge herauskommen? (10 nach 4, nicht vor 2).

Dies zu tun ist ein Schmerz im a **, denn hin und wieder gibt es ein anderes Spiel, das diesen Mechanismus zum Parsen des Spieltitels durchbricht (z. B. "Warhammer 40.000", "James Bond 007").

BlaM
quelle
27
Chains of Promathia ist verwandt mit 11.
Flame
Mögliches Duplikat von MySQL 'Order By' - alphanumerische Sortierung korrekt
Christian
Siehe auch
Paul Spiegel

Antworten:

20

Ich denke, aus diesem Grund sind viele Dinge nach Veröffentlichungsdatum sortiert.

Eine Lösung könnte darin bestehen, eine weitere Spalte in Ihrer Tabelle für den "SortKey" zu erstellen. Dies kann eine bereinigte Version des Titels sein, die einem Muster entspricht, das Sie zum einfachen Sortieren oder als Zähler erstellen.

Michael Haren
quelle
Ich habe gerade eine Klasse für genau diesen Stackoverflow.com/a/47522040/935122
Christian
2
Dies ist definitiv der richtige Ansatz, aber kaum eine Antwort für sich!
Doin
90

Hier ist eine schnelle Lösung:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
slotishtype
quelle
48
Das ist schön, wenn alles "Final Fantasy" ist, aber "Goofy" ist der FF-Suite voraus.
Fortboise
4
Diese Lösung funktioniert nicht immer. Es bricht manchmal. Sie sollten lieber dieses verwenden: stackoverflow.com/a/12257917/384864
Borut Tomazin
6
Kludge auf Kludge stapeln : SELECT alphanumeric, integer FROM sorting_test ORDER BY SOUNDEX(alphanumeric), LENGTH(alphanumeric), alphanumeric. Wenn dies überhaupt funktioniert, liegt es daran, dass SOUNDEX die Zahlen bequem verwirft und so sicherstellt, dass z . B. apple1vorher kommt z1.
offby1
Tolle Lösung, danke, obwohl ich wechseln alphanmuricmusste length(alphanumeric), um "Goofy" vor "Final Fantasy" zu vermeiden
Asped
1
@ offby1 Vorschlag funktioniert nur, wenn der Text zu 100% in Englisch geschrieben SOUNDEX()ist, da er nur für englische Wörter korrekt funktioniert.
Raymond Nijland
56

Ich hab das hier gerade gefunden:

SELECT names FROM your_table ORDER BY games + 0 ASC

Funktioniert eine natürliche Sortierung, wenn die Zahlen vorne stehen, möglicherweise auch für die Mitte.


quelle
2
Ich habe es nicht versucht, aber ich bezweifle es ernsthaft. Der Grund, warum es mit der Zahl an der Vorderseite funktioniert, ist, dass gameses wie in einem numerischen Kontext verwendet und daher vor dem Vergleich in eine Zahl umgewandelt wird. In der Mitte wird immer in 0 konvertiert und die Sortierung wird pseudozufällig.
Manixrock
1
Dies ist keine natürliche Art. Schauen Sie sich lieber diese funktionierende Lösung an: stackoverflow.com/a/12257917/384864
Borut Tomazin
@fedir Das hat auch bei mir gut funktioniert. Ich bin mir nicht einmal ganz sicher, warum das so ist. Gibt es eine Chance auf eine Erklärung Markletp?
BizNuge
Hatte gerade eine kurze Untersuchung und ich verstehe. Ich wusste nicht einmal, dass MySQL diese Art von Casting nur mit einem mathematischen Operator für eine Zeichenfolge durchführen würde! Das Coole ist, dass es nur null zurückgibt, wenn an der Vorderseite des Strings keine Ganzzahl zum "Umwandeln" vorhanden ist. Danke dafür! ---> ADRESSE AUSWÄHLEN, (ADRESSE * 1) als _cast AUS Räumlichkeiten, IN DENEN POSTCODE WIE 'NE1%' BESTELLEN NACH ADRESSE * 1 ASC, ADRESSBEGRENZUNG 100000;
BizNuge
1
Dies funktioniert nicht, wenn sich die Zahlen in der Mitte befinden, z. B. "Final Fantasy 100" oder "Final Fantasy 2". "Final Fantasy 100" wird zuerst angezeigt. Es funktioniert jedoch, wenn die Ganzzahl zum ersten Mal "100 Final Fantasy" ist
dwenaus
51

Gleiche Funktion wie von @plalx gepostet, aber in MySQL umgeschrieben:

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
;;

Verwendung:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")
Richard Toth
quelle
4
Dies ist die einzige Lösung, die wirklich funktioniert. Ich habe auch Drupals-Code getestet, aber er schlägt manchmal fehl. Danke, Mann!
Borut Tomazin
Verwendet jemand dies an wirklich großen Tischen über 10 Millionen?
Mark Steudel
2
@MarkSteudel Wir verwenden eine ähnliche Funktion (wenn auch nicht genau diese) für die natürliche Sortierung in mehreren Tabellen, von denen die größte ~ 5 Millionen Zeilen umfasst. Wir rufen es jedoch nicht direkt in unseren Abfragen auf, sondern verwenden es stattdessen, um den Wert einer nat_nameSpalte festzulegen. Wir verwenden einen Trigger, um die Funktion jedes Mal auszuführen, wenn eine Zeile aktualisiert wird. Mit diesem Ansatz erhalten Sie eine natürliche Sortierung ohne echte Leistungskosten auf Kosten einer zusätzlichen Spalte.
Jacob
Dies funktioniert, indem Zahlen vor Buchstaben sortiert werden, und kann in Drupal mithilfe von hook_views_query_alter implementiert werden, wobei etwas Ähnliches verwendet wirdif ($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

Ich habe diese Funktion vor einiger Zeit für MSSQL 2000 geschrieben:

/**
 * 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
quelle
@MarkSteudel Sie müssten es ausprobieren und selbst testen. Schlimmer noch, Sie könnten die formatierten Werte immer zwischenspeichern. Das würde ich wahrscheinlich für große Tabellen tun, da Sie das Feld auch indizieren könnten.
Plalx
15

MySQL erlaubt diese Art der "natürlichen Sortierung" nicht. Daher scheint es der beste Weg zu sein, das zu erreichen, wonach Sie suchen, Ihre Daten wie oben beschrieben aufzuteilen (separates ID-Feld usw.) oder fehlzuschlagen Führen Sie dazu eine Sortierung basierend auf einem Nicht-Titel-Element, einem indizierten Element in Ihrer Datenbank (Datum, eingefügte ID in der Datenbank usw.) durch.

Wenn die Datenbank die Sortierung für Sie durchführt, ist dies fast immer schneller, als große Datenmengen in die Programmiersprache Ihrer Wahl einzulesen und dort zu sortieren. Wenn Sie also das Datenbankschema hier überhaupt steuern können, sollten Sie das Hinzufügen in Betracht ziehen Einfach zu sortierende Felder, wie oben beschrieben, ersparen Ihnen auf lange Sicht viel Aufwand und Wartung.

In den MySQL-Fehlern und Diskussionsforen werden von Zeit zu Zeit Anfragen zum Hinzufügen einer "natürlichen Sorte" gestellt. Bei vielen Lösungen geht es darum, bestimmte Teile Ihrer Daten zu entfernen und sie für den ORDER BYTeil der Abfrage zu übertragen, z

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

Diese Art von Lösung könnte fast dazu gebracht werden, an Ihrem obigen Final Fantasy-Beispiel zu arbeiten, ist jedoch nicht besonders flexibel und wird wahrscheinlich nicht sauber auf einen Datensatz ausgedehnt, der beispielsweise "Warhammer 40.000" und "James Bond 007" enthält, fürchte ich .

ConroyP
quelle
9

Obwohl ich weiß, dass Sie eine zufriedenstellende Antwort gefunden haben, hatte ich eine Weile mit diesem Problem zu kämpfen, und wir hatten zuvor festgestellt, dass es in SQL nicht angemessen funktioniert, und wir mussten Javascript auf einem JSON verwenden Array.

Hier ist, wie ich es nur mit SQL gelöst habe. Hoffentlich ist dies für andere hilfreich:

Ich hatte Daten wie:

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

Ich habe tatsächlich keine Dinge "besetzt", obwohl ich denke, dass das auch funktioniert hat.

Ich habe zuerst die Teile ersetzt, die sich in den Daten nicht geändert haben, in diesem Fall "Szene", und dann ein LPAD erstellt, um die Dinge auszurichten. Dies scheint es ziemlich gut zu ermöglichen, dass die Alpha-Zeichenfolgen sowohl richtig als auch nummeriert sortiert werden.

Meine ORDER BYKlausel sieht aus wie:

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

Offensichtlich hilft dies nicht bei dem ursprünglichen Problem, das nicht so einheitlich war - aber ich kann mir vorstellen, dass dies wahrscheinlich bei vielen anderen verwandten Problemen funktionieren würde.

FilmJ
quelle
Der LPAD()Hinweis war sehr hilfreich. Ich habe Wörter und Zahlen zu sortieren, mit denen LPADich die Zahlen natürlich sortieren könnte. Und mit CONCATignoriere ich Nicht-Zahlen. Meine Abfrage sieht folgendermaßen aus (Alias ​​ist die zu sortierende Spalte): IF(CONCAT("",alias*1)=alias, LPAD(alias,5,"0"), alias) ASC;👍
Kai Noack
6
  1. Fügen Sie Ihrer Tabelle einen Sortierschlüssel (Rang) hinzu. ORDER BY rank

  2. Verwenden Sie die Spalte "Veröffentlichungsdatum". ORDER BY release_date

  3. Lassen Sie Ihr Objekt beim Extrahieren der Daten aus SQL die Sortierung durchführen, z. B. wenn Sie in ein Set extrahieren, machen Sie es zu einem TreeSet und lassen Sie Ihr Datenmodell Comparable implementieren und setzen Sie hier den natürlichen Sortieralgorithmus ein (Einfügesortierung reicht aus, wenn Sie verwenden eine Sprache ohne Sammlungen), da Sie die Zeilen nacheinander aus SQL lesen, während Sie Ihr Modell erstellen und in die Sammlung einfügen)

JeeBee
quelle
5

In Bezug auf die beste Antwort von Richard Toth https://stackoverflow.com/a/12257917/4052357

Achten Sie auf UTF8-codierte Zeichenfolgen, die 2 Byte (oder mehr) Zeichen und Zahlen enthalten, z

12 南新宿

Mit Hilfe von MySQL LENGTH()in udf_NaturalSortFormatFunktion wird die Byte - Länge der Zeichenfolge zurückgeben und falsch sein, stattdessen verwenden , CHAR_LENGTH()welche die korrekte Zeichenlänge zurück.

In meinem Fall führte die Verwendung dazu, LENGTH()dass Abfragen nie abgeschlossen wurden und 100% der CPU für MySQL ausgelastet waren

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 Ich hätte dies als Kommentar zum Original hinzugefügt, aber ich habe (noch) nicht genug Ruf.

Luke Hoggett
quelle
4

Fügen Sie ein Feld für "Sortierschlüssel" hinzu, in dem alle Ziffernfolgen auf eine feste Länge mit Nullen aufgefüllt sind, und sortieren Sie stattdessen nach diesem Feld.

Wenn Sie möglicherweise lange Ziffernfolgen haben, können Sie jeder Ziffernfolge auch die Anzahl der Ziffern (feste Breite, mit Nullen aufgefüllt) voranstellen. Wenn Sie beispielsweise nicht mehr als 99 Stellen hintereinander haben, lautet der Sortierschlüssel für "Super Blast 10 Ultra" "Super Blast 0210 Ultra".

tye
quelle
4

Bestellung:
0
1
2
10
23
101
205
1000
a
aac
b
casdsadsa
css

Verwenden Sie diese Abfrage:

WÄHLEN 
    Spaltenname 
VON 
    Tabellenname 
SORTIEREN NACH
    Spaltenname REGEXP '^ \ d * [^ \ da-z & \. \' \ - \ "\! \ @ \ # \ $ \% \ ^ \ * \ (\) \; \: \\, \? \ / \ ~ \ `\ | \ _ \ -] 'DESC, 
    Spaltenname + 0, 
    Spaltenname;
Guma
quelle
Leider ist diese bricht , wenn Sie Werte in einem solchen hinzufügen , wie a1, a2, a11, etc ...
random_user_name
4

Wenn Sie das Rad nicht neu erfinden möchten oder Kopfschmerzen mit viel Code haben, der nicht funktioniert, verwenden Sie einfach Drupal Natural Sort ... Führen Sie einfach das komprimierte SQL aus (MySQL oder Postgre), und fertig. Wenn Sie eine Anfrage stellen, bestellen Sie einfach mit:

... ORDER BY natsort_canon(column_name, 'natural')
Neto Queiroz
quelle
Vielen Dank dafür, ich habe alle möglichen Lösungen ausprobiert (ha ha sehen, was ich dort gemacht habe?), Aber keine davon hat wirklich für alle Daten funktioniert, die ich hatte. Die Drupal-Funktion wirkte wie ein Zauber. Danke fürs Schreiben.
Ben Hitchcock
Dies funktioniert, sortiert aber die Zahlen am Ende (AZ dann 0-9)
Realgt
4

Eine andere Möglichkeit besteht darin, die Sortierung im Speicher durchzuführen, nachdem die Daten aus MySQL abgerufen wurden. Unter Leistungsgesichtspunkten ist dies zwar nicht die beste Option, aber wenn Sie keine großen Listen sortieren, sollten Sie in Ordnung sein.

Wenn Sie sich Jeffs Beitrag ansehen, finden Sie zahlreiche Algorithmen für jede Sprache, mit der Sie möglicherweise arbeiten. Sortieren für Menschen: Natürliche Sortierreihenfolge

Bob
quelle
2

Sie können auch dynamisch die "Sortierspalte" erstellen:

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

Auf diese Weise können Sie Gruppen zum Sortieren erstellen.

In meiner Anfrage wollte ich das '-' vor allem, dann die Zahlen, dann den Text. Was zu etwas führen könnte wie:

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

Auf diese Weise müssen Sie die Sortierspalte beim Hinzufügen von Daten nicht in der richtigen Reihenfolge pflegen. Sie können Ihre Sortierreihenfolge auch nach Bedarf ändern.

Antoine
quelle
Ich weiß nicht, wie performant das sein würde. Ich benutze es die ganze Zeit ohne irgendwelche Unannehmlichkeiten. Meine Datenbank ist nicht groß.
Antoine
1

Wenn Sie PHP verwenden, können Sie die natürliche Sortierung in PHP durchführen.

$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]; 
}

Ich hoffe, MySQL wird in einer zukünftigen Version die natürliche Sortierung implementieren, aber die Feature-Anfrage (# 1588) ist seit 2003 geöffnet. Ich würde also nicht den Atem anhalten.

Bob Fanger
quelle
Theoretisch ist das möglich, aber ich müsste zuerst alle Datenbankeinträge auf meinem Webserver lesen.
BlaM
Alternativ können Sie Folgendes berücksichtigen: usort($mydata, function ($item1, $item2) { return strnatcmp($item1['key'], $item2['key']); });(Ich habe ein assoziatives Array und sortiere nach Schlüssel.) Ref: stackoverflow.com/q/12426825/1066234
Kai Noack
1

Eine vereinfachte Nicht-UDF-Version der besten Antwort von @ plaix / Richard Toth / Luke Hoggett, die nur für die erste Ganzzahl im Feld funktioniert, ist

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
quelle
1

Ich habe verschiedene Lösungen ausprobiert, aber das ist eigentlich sehr einfach:

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
quelle
1
Funktioniert sehr gut zum Sortieren von Zahlen im Format 23-4244. Danke :)
Pyton
1
funktioniert nur mit diesen Testdaten, da die Zeichenfolgen vor der Nummer alle gleich sind. Versuchen Sie, dort einen Wert z_99einzugeben, der an die Spitze gesetzt wird, aber danach zfolgt v.
Samuel Neff
@SamuelNeff siehe SQL: ORDER BY LENGTH (test_column) DESC, test_column DESC also ja, da es zuerst nach der Länge der Spalte sortiert wird. Dies funktioniert gut beim Sortieren einer Präfix-Tabellengruppe, die Sie sonst nicht nur mit "test_column DESC" sortieren könnten
Tarik
1

Viele andere Antworten, die ich hier (und in den doppelten Fragen) sehe, funktionieren grundsätzlich nur für sehr spezifisch formatierte Daten, z. B. eine Zeichenfolge, die vollständig eine Zahl ist oder für die es ein alphabetisches Präfix fester Länge gibt. Dies wird im allgemeinen Fall nicht funktionieren.

Es ist wahr , dass es nicht wirklich eine Möglichkeit , eine 100% allgemeine nat-Art in MySQL zu implementieren, weil es zu tun , was Sie wirklich brauchen , ist ein modifizierter Vergleichsfunktion , die ob / wann es Begegnungen zwischen lexicographic Sortierung der Strings und numerische Sortierung Schalter eine Zahl. Ein solcher Code könnte jeden Algorithmus implementieren, den Sie zum Erkennen und Vergleichen der numerischen Teile innerhalb von zwei Zeichenfolgen wünschen könnten. Leider ist die Vergleichsfunktion in MySQL intern im Code und kann vom Benutzer nicht geändert werden.

Dies hinterlässt einen Hack, bei dem Sie versuchen, einen Sortierschlüssel für Ihre Zeichenfolge zu erstellen, in dem die numerischen Teile neu formatiert werden, sodass die lexikografische Standardsortierung sie tatsächlich nach Ihren Wünschen sortiert .

Für einfache Ganzzahlen bis zu einer maximalen Anzahl von Ziffern besteht die offensichtliche Lösung darin, sie einfach mit Nullen links aufzufüllen, damit sie alle eine feste Breite haben. Dies ist der Ansatz des Drupal-Plugins und der Lösungen von @plalx / @RichardToth. (@Christian hat eine andere und viel komplexere Lösung, bietet aber keine Vorteile, die ich sehen kann).

Wie @tye hervorhebt, können Sie dies verbessern, indem Sie jeder Zahl eine feste Ziffernlänge voranstellen, anstatt sie einfach links aufzufüllen. Es gibt jedoch noch viel mehr Verbesserungen, selbst angesichts der Einschränkungen eines im Wesentlichen umständlichen Hacks. Es scheint jedoch keine vorgefertigten Lösungen zu geben!

Was ist zum Beispiel mit:

  • Plus- und Minuszeichen? +10 vs 10 vs -10
  • Dezimalstellen? 8,2, 8,5, 1,006, 0,75
  • Führende Nullen? 020, 030, 00000922
  • Tausend Separatoren? "1.001 Dalmations" gegen "1001 Dalmations"
  • Versionsnummern? MariaDB v10.3.18 vs MariaDB v10.3.3
  • Sehr lange Zahlen? 103.768.276.592.092.364.859.236.487.687.870.234.598.55

Als Erweiterung der Methode von @ tye habe ich eine ziemlich kompakte gespeicherte NatSortKey () -Funktion erstellt, die eine beliebige Zeichenfolge in einen Nat-Sort-Schlüssel konvertiert, alle oben genannten Fälle behandelt, einigermaßen effizient ist und eine vollständige Sortierung beibehält. Reihenfolge (keine zwei verschiedenen Zeichenfolgen haben Sortierschlüssel, die gleich sind). Ein zweiter Parameter kann verwendet werden, um die Anzahl der in jeder Zeichenfolge verarbeiteten Zahlen zu begrenzen (z. B. auf die ersten 10 Zahlen), mit denen sichergestellt werden kann, dass die Ausgabe in eine bestimmte Länge passt.

HINWEIS: Sortierschlüsselzeichenfolgen, die mit einem bestimmten Wert dieses zweiten Parameters generiert wurden, sollten nur nach anderen Zeichenfolgen sortiert werden, die mit demselben Wert für den Parameter generiert wurden , da sie sonst möglicherweise nicht richtig sortiert werden.

Sie können es direkt bei der Bestellung verwenden, z

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

Für eine effiziente Sortierung großer Tabellen ist es jedoch besser, den Sortierschlüssel in einer anderen Spalte vorab zu speichern (möglicherweise mit einem Index darauf):

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

[Idealerweise würden Sie dies automatisch erreichen, indem Sie die Schlüsselspalte als berechnete gespeicherte Spalte erstellen und dabei Folgendes verwenden:

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

Aber jetzt weder MySQL noch MariaDB erlauben Funktionen in berechneten Spalten gespeichert , so leider kann man noch nicht das tun .]


Meine Funktion betrifft nur das Sortieren von Zahlen . Wenn Sie andere Sortierungsnormalisierungsaufgaben ausführen möchten, z. B. das Entfernen aller Satzzeichen oder das Trimmen von Leerzeichen an jedem Ende oder das Ersetzen von Sequenzen mit mehreren Leerzeichen durch einzelne Leerzeichen, können Sie die Funktion entweder erweitern oder vorher oder nachher NatSortKey()ausführen auf Ihre Daten angewendet. (Ich würde empfehlen, REGEXP_REPLACE()für diesen Zweck zu verwenden).

Es ist auch etwas anglozentrisch, da ich davon ausgehe, dass '.' für einen Dezimalpunkt und ',' für das Tausendertrennzeichen, aber es sollte leicht genug sein, Änderungen vorzunehmen, wenn Sie das Gegenteil wünschen oder wenn Sie möchten, dass dies als Parameter umschaltbar ist.

Es könnte auf andere Weise einer weiteren Verbesserung zugänglich sein; Beispielsweise werden derzeit negative Zahlen nach dem absoluten Wert sortiert, sodass -1 vor -2 steht und nicht umgekehrt. Es gibt auch keine Möglichkeit, eine DESC-Sortierreihenfolge für Zahlen anzugeben, während die lexikografische ASC-Sortierung für Text beibehalten wird. Beide Probleme können mit etwas mehr Arbeit behoben werden. Ich werde den Code aktualisieren, wenn ich Zeit habe.

Es gibt viele andere Details zu beachten - einschließlich einiger kritischer Abhängigkeiten von der von Ihnen verwendeten Chaset und Sortierung -, aber ich habe sie alle in einen Kommentarblock innerhalb des SQL-Codes eingefügt. Bitte lesen Sie dies sorgfältig durch, bevor Sie die Funktion selbst nutzen!

Also, hier ist der Code. Wenn Sie einen Fehler finden oder eine Verbesserung haben, die ich nicht erwähnt habe, lassen Sie es mich bitte in den Kommentaren wissen!


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
quelle
Ich bin ein Anfänger in MySQL und habe es versucht. Erhalten Sie diesen Fehler: "# 1305 - FUNCTION mydatabase.REGEXP_INSTR existiert nicht". Irgendeine Idee?
John T
Für jeden anderen Neuling da draußen. Ich hatte MySQL 8.0 nicht installiert. Es wird für REGEXP_INSTR (und andere REGEXP-Sachen) benötigt.
John T
Ich habe gerade einen schwerwiegenden Fehler in NatSortKey behoben: Es gab ein falsches Regex-Zeichen. Wenn Sie diese Funktion selbst verwendet haben, aktualisieren Sie bitte Ihren Code!
Doin
-4

Ich weiß, dass dieses Thema uralt ist, aber ich denke, ich habe einen Weg gefunden, dies zu tun:

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

Verschrottet das, es hat den folgenden Satz falsch sortiert (Es ist nutzlos lol):

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7: Adventskinder Final Fantasy 12 Final Fantasy 112 FF1 FF2

user1467716
quelle
3
Warum nicht diese Antwort entfernen? Sie erhalten ein Abzeichen dafür
m47730