web-dev-qa-db-fra.com

Tri naturel dans MySQL

Existe-t-il un moyen élégant d’effectuer un tri naturel et performant dans une base de données MySQL?

Par exemple si j'ai cet ensemble de données:

  • Final Fantasy
  • Final Fantasy 4
  • Final Fantasy 10
  • Final Fantasy 12
  • Final Fantasy 12: Les chaînes de Promathia
  • Final Fantasy Adventure
  • Final Fantasy Origins
  • Tactiques Final Fantasy

Toute autre solution élégante que de scinder les noms des jeux en leurs composants 

  • Titre: "Final Fantasy"
  • Nombre: "12"
  • Sous-titres: "Chaînes de Promathia"

pour s'assurer qu'ils sortent dans le bon ordre? (10 après 4, pas avant 2).

Cela est pénible a ** car de temps en temps, un autre jeu rompt le mécanisme d'analyse du titre (par exemple, "Warhammer 40,000", "James Bond 007")

71
BlaM

Je pense que c'est pourquoi beaucoup de choses sont triées par date de sortie.

Une solution pourrait être de créer une autre colonne dans votre table pour la "Clé de tri". Il peut s'agir d'une version assainie du titre, conforme à un modèle que vous créez pour faciliter le tri ou à un compteur.

22
Michael Haren

Voici une solution rapide:

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

Je viens de trouver ceci:

SELECT names FROM your_table ORDER BY games + 0 ASC

Est-ce qu’une sorte naturelle quand les chiffres sont à l’avant, pourrait également fonctionner pour le milieu.

51
markletp

Même fonction que celle publiée par @plalx, ​​mais réécrite dans 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
;;

Usage:

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

MySQL n'autorisant pas ce type de "tri naturel", il semble donc que le meilleur moyen d'obtenir ce que vous recherchez est de scinder la configuration de vos données comme vous l'avez décrit plus haut (champ id séparé, etc.) ou d'échouer. que, effectuez un tri basé sur un élément non-title, un élément indexé dans votre base de données (date, identifiant inséré dans la base de données, etc.).

Demander à la base de données de faire le tri à votre place sera presque toujours plus rapide que de lire de grands ensembles de données dans le langage de programmation de votre choix et de les trier là-bas; Comme il est décrit ci-dessus, les champs faciles à trier vous épargneront beaucoup de tracas et de maintenance à long terme.

Des requêtes pour ajouter une "sorte naturelle" apparaissent de temps en temps sur les bogues MySQL et les forums de discussion , et de nombreuses solutions consistent à supprimer certaines parties de vos données et à les convertir pour la partie ORDER BY de la requête, par exemple.

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

Ce type de solution pourrait très bien fonctionner pour votre exemple Final Fantasy ci-dessus, mais il n'est ni particulièrement flexible ni susceptible de s'étendre proprement à un ensemble de données comprenant, par exemple, "Warhammer 40,000" et "James Bond 007". .

15
ConroyP

J'ai écrit cette fonction pour MSSQL 2000 il y a quelque temps:

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

Ainsi, bien que je sache que vous avez trouvé une réponse satisfaisante, je me débattais avec ce problème depuis un moment et nous avions précédemment déterminé que cela ne pouvait pas être fait raisonnablement bien avec SQL et que nous allions devoir utiliser javascript sur un JSON. tableau.

Voici comment je l'ai résolu en utilisant simplement SQL. J'espère que cela est utile pour les autres:

J'ai eu des données telles que:

 Scène 1 
 Scène 1A 
 Scène 1B 
 Scène 2A 
 Scène 3 
....__ Scène 101 
 Scène XXA1 
 Scène XXA2 

En fait, je n'ai pas "jeté" de choses, mais je suppose que cela a peut-être également fonctionné.

J'ai d'abord remplacé les parties qui ne changeaient pas dans les données, dans ce cas "Scène", puis j'ai créé un LPAD pour aligner les choses. Cela semble permettre assez bien aux chaînes alpha de bien trier, ainsi qu'aux chaînes numérotées.

Ma clause ORDER BY ressemble à:

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

Évidemment, cela n’aide en rien le problème initial, qui n’était pas aussi uniforme, mais j’imagine que cela fonctionnerait probablement pour de nombreux autres problèmes connexes, alors présentez-le.

9
FilmJ
  1. Ajoutez une clé de tri (rang) dans votre tableau. ORDER BY rank

  2. Utilisez la colonne "Date de publication". ORDER BY release_date

  3. Lors de l'extraction des données à partir de SQL, faites en sorte que votre objet effectue le tri. Par exemple, si vous effectuez l'extraction dans un ensemble, faites-en un TreeSet et faites en sorte que votre modèle de données implémente Comparable et appliquez l'algorithme de tri naturel ici (le tri par insertion suffira si vous utilisez une langue sans collections) car vous lirez les lignes de SQL une à une au fur et à mesure que vous créez votre modèle et que vous l'insérez dans la collection)

5
JeeBee

Concernant la meilleure réponse de Richard Toth https://stackoverflow.com/a/12257917/4052357

Méfiez-vous des chaînes codées en UTF8 contenant des caractères et des nombres d'au moins 2 octets (par exemple).

12 南新宿

L'utilisation de la fonction LENGTH() dans udf_NaturalSortFormat de MySQL renverra la longueur en octets de la chaîne et sera incorrecte; utilisez plutôt CHAR_LENGTH() qui renverra la longueur de caractère correcte.

Dans mon cas, en utilisant LENGTH(), les requêtes ne se terminent jamais et généraient une utilisation du processeur à 100% pour 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. J'aurais ajouté ceci comme commentaire à l'original mais je n'ai pas encore assez de réputation 

5
Luke Hoggett

Une autre option est de faire le tri en mémoire après avoir extrait les données de mysql. Bien que ce ne soit pas la meilleure option du point de vue de la performance, si vous ne triez pas de grandes listes, tout devrait bien se passer. 

Si vous jetez un coup d'oeil au message de Jeff, vous trouverez une multitude d'algorithmes pour la langue avec laquelle vous travaillerez . Tri pour les humains: Ordre de tri naturel

4
Bob

Commander:
0
1
2
dix
23
101
205
1000
une
aac
b
casdsadsa
css

Utilisez cette requête:

 SELECT 
 nom_colonne 
 FROM 
 nom_table 
 ORDER BY 
 nom_colonne REGEXP '^\d * [^\da-z & \. \'\-\"\!\#\$ \%\^\* (\) \; \: \\, \?\~\`\ |\_\-] 'DESC, 
 Nom_colonne + 0, 
 Nom_colonne; 
4
Guma

Si vous ne voulez pas réinventer la roue ou si vous avez mal à la tête avec beaucoup de code qui ne fonctionne pas, utilisez simplement Drupal Natural Sort ... Il suffit d’exécuter le code SQL fourni avec MySQL ou Postgre, . Lorsque vous effectuez une requête, commandez simplement en utilisant:

... ORDER BY natsort_Canon(column_name, 'natural')
4
Neto Queiroz

J'ai essayé plusieurs solutions mais en réalité c'est très simple:

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

Vous pouvez également créer de manière dynamique la "colonne de tri":

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

De cette façon, vous pouvez créer des groupes à trier. 

Dans ma requête, je voulais le '-' devant tout, puis les chiffres, puis le texte. Ce qui pourrait entraîner quelque chose comme:

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

De cette façon, vous ne devez pas conserver la colonne de tri dans le bon ordre lorsque vous ajoutez des données. Vous pouvez également modifier votre ordre de tri en fonction de vos besoins.

3
antoine

Ajoutez un champ pour "clé de tri" contenant toutes les chaînes de chiffres remplies de zéros à une longueur fixe, puis triez sur ce champ à la place.

Si vous avez de longues chaînes de chiffres, une autre méthode consiste à ajouter le nombre de chiffres (largeur fixe, remplissage à zéro) à chaque chaîne de chiffres. Par exemple, si vous n'avez pas plus de 99 chiffres à la suite, pour "Super Blast 10 Ultra", la clé de tri sera "Super Blast 0210 Ultra".

3
tye

Une version simplifiée non udf de la meilleure réponse de @ plaix/Richard Toth/Luke Hoggett, qui ne fonctionne que pour le premier entier du champ, est

SELECT name,
LEAST(
    IFNULL(NULLIF(LOCATE('0', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('1', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('2', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('3', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('4', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('5', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('6', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('7', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('8', name), 0), ~0),
    IFNULL(NULLIF(LOCATE('9', name), 0), ~0)
) AS first_int
FROM table
ORDER BY IF(first_int = ~0, name, CONCAT(
    SUBSTR(name, 1, first_int - 1),
    LPAD(CAST(SUBSTR(name, first_int) AS UNSIGNED), LENGTH(~0), '0'),
    SUBSTR(name, first_int + LENGTH(CAST(SUBSTR(name, first_int) AS UNSIGNED)))
)) ASC
1
bonger

Si vous utilisez PHP, vous pouvez faire le tri naturel en 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]; 
}

J'espère que MySQL implémentera le tri naturel dans une future version, mais la requête feature (# 1588) est ouverte depuis 2003, je ne pouvais donc pas retenir mon souffle.

1
Bob Fanger

Il y a aussi natsort . Il est destiné à faire partie d'un drupal plugin , mais il fonctionne parfaitement de manière autonome.

0
Peter V. Mørch