Domanda

Esiste un modo elegante per avere un ordinamento naturale e performante in un database MySQL?

Ad esempio se ho questo set di dati:

  • 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

Qualsiasi altra soluzione elegante che dividere i nomi dei giochi nei loro componenti

  • Titolo : " Final Fantasy "
  • Numero : " 12 "
  • Sottotitoli : " Chains of Promathia "

per assicurarsi che escano nell'ordine giusto? (10 dopo 4, non prima di 2).

Farlo è un dolore in a ** perché di tanto in tanto c'è un altro gioco che rompe quel meccanismo di analisi del titolo del gioco (ad es. " Warhammer 40.000 " ;, " James Bond 007 ")

È stato utile?

Soluzione

Penso che questo sia il motivo per cui molte cose sono ordinate per data di uscita.

Una soluzione potrebbe essere quella di creare un'altra colonna nella tabella per " SortKey " ;. Questa potrebbe essere una versione sterilizzata del titolo conforme a un modello creato per un ordinamento semplice o un contatore.

Altri suggerimenti

Ecco una soluzione rapida:

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

Ho appena trovato questo:

SELECT names FROM your_table ORDER BY games + 0 ASC

Fa un ordinamento naturale quando i numeri sono in primo piano, potrebbe funzionare anche per il mezzo.

Stessa funzione pubblicata da @plalx, ??ma riscritta su 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
;;

Utilizzo:

SELECT name FROM products ORDER BY udf_NaturalSortFormat(name, 10, ".")

MySQL non consente questo tipo di "ordinamento naturale", quindi sembra che il modo migliore per ottenere ciò che stai cercando sia quello di dividere i tuoi dati come descritto sopra (campo ID separato, ecc. ) o, in mancanza, esegui un ordinamento basato su un elemento senza titolo, elemento indicizzato nel tuo db (data, ID inserito nel db, ecc.).

Avere il db a fare l'ordinamento per te sarà quasi sempre più veloce della lettura di grandi set di dati nel tuo linguaggio di programmazione prescelto e l'ordinamento lì, quindi se hai qualche controllo su tutto lo schema db qui, allora guarda l'aggiunta di campi facilmente ordinabili come descritto sopra, ti farà risparmiare molta seccatura e manutenzione a lungo termine.

Richieste di aggiungere un " ordinamento naturale " di tanto in tanto vengono pubblicati sui bug MySQL e forum di discussione e molte soluzioni ruotano attorno alla rimozione di parti specifiche dei dati e al cast per la parte ORDER BY della query, ad es.

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

Questo tipo di soluzione potrebbe essere quasi realizzato per funzionare sul tuo esempio di Final Fantasy sopra, ma non è particolarmente flessibile e difficilmente si estenderà in modo pulito a un set di dati che, per esempio, "Warhammer 40.000" e "James Bond 007" Ho paura.

Ho scritto questa funzione per MSSQL 2000 qualche tempo fa:

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

Quindi, mentre so che hai trovato una risposta soddisfacente, stavo lottando con questo problema per un po ', e in precedenza avevamo stabilito che non poteva essere fatto ragionevolmente bene in SQL e che avremmo dovuto usare javascript su un array JSON.

Ecco come l'ho risolto usando solo SQL. Spero che questo sia utile per gli altri:

Ho avuto dati come:

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

In realtà non ho " cast " anche se suppongo che potrebbe anche aver funzionato.

Ho prima sostituito le parti che erano immutabili nei dati, in questo caso "Scene", e poi ho fatto un LPAD per allineare le cose. Ciò sembra consentire abbastanza bene che le stringhe alfa vengano ordinate correttamente e anche quelle numerate.

La mia clausola ORDER BY è simile a:

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

Ovviamente questo non aiuta con il problema originale che non era così uniforme - ma immagino che probabilmente funzionerebbe per molti altri problemi correlati, quindi mettilo in campo.

  1. Aggiungi una chiave di ordinamento (classifica) nella tabella. ORDER BY rank

  2. Utilizza la " Data di rilascio " colonna. ORDER BY release_date

  3. Quando si estraggono i dati da SQL, fare in modo che il proprio oggetto esegua l'ordinamento, ad es. se si estrae in un set, si fa un TreeSet e si rende implementabile il modello di dati comparabile e si applica l'algoritmo di ordinamento naturale qui (ordinamento per inserzione basterà se stai usando una lingua senza raccolte) poiché leggerai le righe da SQL una ad una mentre crei il tuo modello e lo inserisci nella raccolta)

Riguardo alla migliore risposta di Richard Toth https://stackoverflow.com/a/12257917/4052357

Fai attenzione alle stringhe codificate UTF8 che contengono caratteri e numeri da 2byte (o più) ad es.

12 南新宿

L'uso della funzione LENGTH () di MySQL nella funzione udf_NaturalSortFormat restituirà la lunghezza in byte della stringa e sarà errato, invece utilizzare CHAR_LENGTH () che restituirà la lunghezza corretta del carattere.

Nel mio caso l'uso di LENGTH () ha impedito il completamento delle query e il 100% di utilizzo della CPU per 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
;;

P.S. Avrei aggiunto questo come commento all'originale, ma non ho abbastanza reputazione (ancora)

Per ordinare:
0
1
2
10
23
101
205
1000
un
aac
b
casdsadsa
css

Usa questa query:

SELECT 
    column_name 
FROM 
    table_name 
ORDER BY
    column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#\$\%\^\*\(\)\;\:\\,\?\/\~\`\|\_\-]' DESC, 
    column_name + 0, 
    column_name;

Se non vuoi reinventare la ruota o avere mal di testa con molto codice che non funziona, usa Drupal Natural Sort ... Basta eseguire l'SQL che viene compresso (MySQL o Postgre), e il gioco è fatto. Quando fai una query, ordina semplicemente usando:

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

Un'altra opzione è quella di fare l'ordinamento in memoria dopo aver estratto i dati da mysql. Anche se non sarà l'opzione migliore dal punto di vista delle prestazioni, se non stai ordinando elenchi di grandi dimensioni, dovresti andare bene.

Se dai un'occhiata al post di Jeff, puoi trovare molti algoritmi per qualunque linguaggio tu stia lavorando. Ordinamento per esseri umani: ordinamento naturale

Aggiungi un campo per " chiave di ordinamento " che ha tutte le stringhe di cifre con spaziatura zero fino a una lunghezza fissa, quindi ordina invece su quel campo.

Se si possono avere lunghe stringhe di cifre, un altro metodo è anteporre il numero di cifre (larghezza fissa, riempimento zero) a ciascuna stringa di cifre. Ad esempio, se non avrai più di 99 cifre di fila, allora per "Super Blast 10 Ultra" la chiave di ordinamento sarebbe " Super Blast 0210 Ultra " ;.

Puoi anche creare in modo dinamico la colonna " ordina colonna " :

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

In questo modo, puoi creare gruppi da ordinare.

Nella mia domanda, volevo il '-' davanti a tutto, quindi i numeri, quindi il testo. Il che potrebbe tradursi in qualcosa del genere:

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

In questo modo non è necessario mantenere la colonna di ordinamento nell'ordine corretto quando si aggiungono i dati. Puoi anche modificare il tuo ordinamento in base a ciò di cui hai bisogno.

Ho provato diverse soluzioni, ma in realtà è molto semplice:

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

Se stai usando PHP puoi fare l'ordinamento naturale in 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]; 
}

Spero che MySQL implementerà l'ordinamento naturale in una versione futura, ma la richiesta di funzionalità (# 1588) è aperto dal 2003, quindi non trattengo il respiro.

Una versione non udf semplificata della migliore risposta di @ plaix / Richard Toth / Luke Hoggett, che funziona solo per il primo numero intero nel campo, è

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

Inoltre c'è natsort . È destinato a far parte di un plugin di drupal , ma funziona perfettamente autonomo.

So che questo argomento è antico ma penso di aver trovato un modo per farlo:

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

Scartalo, ha ordinato il seguente set in modo errato (It's lol inutile):

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top