Question

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 ce jeu 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
  • Tactics de Final Fantasy

Toute autre solution élégante qui consiste à diviser les noms des jeux en composants

  • Titre : "Final Fantasy"
  • Numéro : "12"
  • Sous-titre : "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 pour a ** car de temps en temps, un autre jeu casse le mécanisme d'analyse du titre du jeu (par exemple, "Warhammer 40 000", "James Bond 007")

.
Était-ce utile?

La solution

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

Une solution pourrait consister à créer une autre colonne dans votre table pour le "SortKey". Il peut s’agir d’une version assainie du titre, conforme à un modèle créé pour faciliter le tri ou à un compteur.

Autres conseils

Voici une solution rapide:

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

Je viens de trouver ceci:

SELECT names FROM your_table ORDER BY games + 0 ASC

Effectue-t-il un tri naturel lorsque les chiffres sont à l'avant, pourrait également fonctionner pour le milieu.

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

Utilisation:

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

MySQL n'autorise pas ce type de "tri naturel", il semble donc que le meilleur moyen d'obtenir ce que vous recherchez est de fractionner la configuration de vos données comme décrit précédemment (champ id séparé, etc.). ), ou à défaut, effectuez un tri basé sur un élément non-title, é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 va presque toujours être plus rapide que de lire des ensembles de données volumineux dans le langage de programmation de votre choix et de les trier là-bas. Si vous avez le moindre contrôle sur le schéma de base de données, Si vous envisagez d'ajouter des champs faciles à trier comme décrit ci-dessus, cela vous évitera beaucoup de tracas et de maintenance à long terme.

Demande l'ajout d'un " type naturel " apparaître de temps en temps sur les bugs MySQL et forums de discussion , et de nombreuses solutions consistent à supprimer certaines parties de vos données et à les diffuser. les 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 s’appliquer à votre exemple Final Fantasy ci-dessus, mais elle n’est ni particulièrement flexible ni susceptible de s’étendre proprement à un jeu de données comprenant, par exemple, "Warhammer 40,000". et "James Bond 007" J'ai peur.

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

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

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

J'avais des données telles que:

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

En fait, je n'ai pas "diffusé". Je suppose que les choses ont peut-être également fonctionné.

J’ai d’abord remplacé les éléments qui ne changeaient pas dans les données, dans ce cas-ci "Scène", puis j’ai réalisé 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 se présente comme suit:

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.

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

  2. Utilisez la " date de publication " colonne. ORDER BY version_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 applique l'algorithme de tri naturel ici (tri par insertion suffira si vous utilisez un langage sans collections) car vous lirez les lignes SQL une par une au fur et à mesure que vous créez votre modèle et que vous l'insérez dans la collection)

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 chiffres d'au moins 2 octets (p. ex.

12 南新宿

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

Dans mon cas, l'utilisation de LENGTH () a empêché la complétude des requêtes et généré 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

Pour commander:
0
1
2
10
23
101
205
1000
un
aac
b
casdsadsa
css

Utilisez cette requête:

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

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 compressé (MySQL ou Postgre), et c'est tout. Lorsque vous effectuez une requête, commandez simplement en utilisant:

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

Une autre option consiste à 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'œil au message de Jeff, vous trouverez de nombreux algorithmes pour toutes les langues avec lesquelles vous travaillerez. Tri pour les humains: ordre de tri naturel

Ajouter un champ pour la " clé de tri " qui a toutes les chaînes de chiffres complétées de zéros à une longueur fixe, puis trie 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, sélectionnez "Super Blast 10 Ultra". la clé de tri serait "Super Blast 0210 Ultra".

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.

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

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 version ultérieure, mais la demande de fonctionnalité (# 1588) est ouvert depuis 2003, je ne pouvais donc pas retenir mon souffle.

Une version non-udf simplifiée de la meilleure réponse de @ plaix / Richard Toth / Luke Hoggett, qui fonctionne uniquement 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

Il existe également un natsort . . Il est destiné à faire partie d'un plug-in drupal , mais il fonctionne parfaitement de manière autonome.

Je sais que ce sujet est ancien mais je pense avoir trouvé un moyen de le faire:

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

Éliminer ça, le tri suivant a été mal sélectionné (c'est inutile lol):

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7: Enfants de l'Avent Final Fantasy 12 Final Fantasy 112 FF1 FF2

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top