Naturel de Tri dans MySQL

y a-t-il une façon élégante d'avoir un tri performant et naturel 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: Chains of Promathia
  • Final Fantasy Adventure
  • Final Fantasy Origines
  • Final Fantasy Tactics

toute autre solution élégante que de diviser les noms des jeux dans leurs composants

  • Titre : "Final Fantasy"
  • Nombre : "12"
  • Sous-titre : "chaînes de Promathia "

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

cela est une douleur dans le** parce que chaque maintenant et puis, il y a un autre jeu qui casse le mécanisme de l'analyse du titre du jeu (par exemple, "Warhammer 40,000", "James Bond 007")

71
demandé sur BlaM 2008-09-30 19:42:27

19 réponses

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

une solution pourrait être de créer une autre colonne dans votre tableau pour le"SortKey". Ceci pourrait être une version épurée du titre qui est conforme à un modèle que vous créez pour un tri facile ou un compteur.

22
répondu Michael Haren 2008-09-30 15:44:44

Voici une solution rapide:

SELECT alphanumeric, 
       integer
FROM sorting_test
ORDER BY LENGTH(alphanumeric), alphanumeric
82
répondu slotishtype 2017-09-06 14:57:24

vient de trouver ceci:

SELECT names FROM your_table ORDER BY games + 0 ASC

fait un tri naturel quand les nombres sont à l'avant, pourrait fonctionner pour le milieu aussi bien.

51
répondu 2011-11-25 22:28:04

même fonction que Postée par @plalx, mais réécrite à 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, ".")
44
répondu Richard Toth 2016-08-18 13:42:26

MySQL ne permet pas ce genre de "tri naturel", il semble donc que la meilleure façon d'obtenir ce que vous recherchez est de diviser vos données configurées comme vous l'avez décrit ci-dessus (champ ID séparé, etc), ou à défaut, effectuer un tri basé sur un élément non-titre, élément indexé dans votre db (date, id inséré dans la db, etc).

avoir le db faire le tri pour vous sera presque toujours plus rapide que la lecture de grands ensembles de données dans le langage de programmation de votre choix et le tri là-bas, donc si vous avez un contrôle sur tout le schéma de base de données ici, puis regarder à ajouter des champs facilement triés comme décrit ci-dessus, il vous fera économiser beaucoup de tracas et de maintenance à long terme.

des demandes d'ajout d'une " sorte naturelle "surgissent de temps en temps sur les bogues MySQL et forums de discussion , et de nombreuses solutions tournent autour de la suppression de certaines parties de vos données et de leur moulage pour le ORDER BY partie de la requête, par exemple

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

ce genre de solution pourrait à peu près être fait pour travailler sur votre dernier exemple de fantaisie ci-dessus, mais n'est pas particulièrement flexible et peu probable d'étendre proprement à un ensemble de données, y compris, disons," Warhammer 40,000 "et" James Bond 007 " j'ai bien peur.

15
répondu ConroyP 2008-09-30 16:02:43

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
répondu plalx 2013-09-26 18:57:10

donc, alors que je sais 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 raisonnablement bien dans SQL et nous allions devoir utiliser javascript sur un tableau JSON.

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

j'ai eu 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" moulé " les choses bien que je supposons que cela ait aussi fonctionné.

j'ai d'abord remplacé les parties qui étaient immuables dans les données, dans ce cas "scène", puis fait un LPAD pour aligner les choses. Cela semble permettre assez bien pour l'alpha chaînes de trier correctement ainsi que le numérotés.

Mon ORDER BY clause ressemble à:

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

évidemment cela n'aide pas avec le problème original qui n'était pas si uniforme - mais j'imagine que ce serait probablement travailler pour beaucoup d'autres problèmes liés, donc mettre là-bas.

8
répondu FilmJ 2011-11-25 22:29:40
  1. ajouter une clé de tri (rang) dans votre tableau. ORDER BY rank

  2. utilisez la colonne" Date de libération". ORDER BY release_date

  3. lors de L'extraction des données à partir de SQL, faites votre objet faire le tri, par exemple, si l'extraction dans un ensemble, en faire un arbre, et faire de votre modèle de données mettre en œuvre Comparable et d'appliquer l'algorithme de tri naturel ici (tri d'insertion sera suffisant si vous utilisez un langue sans collections) comme vous allez lire les lignes de SQL un par un comme vous créez votre modèle et l'insérez dans la collection)

5
répondu JeeBee 2011-11-25 22:28:39

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

méfiez-vous des chaînes encodées UTF8 qui contiennent des caractères et des nombres de 2 octets (ou plus), p.ex.

12 南新宿

en utilisant la fonction LENGTH() de MySQL dans udf_NaturalSortFormat retournera la longueur de byte de la chaîne et sera incorrect, à la place d'utiliser CHAR_LENGTH() qui retournera la longueur de caractère correcte.

dans mon cas, l'utilisation de LENGTH() a causé des requêtes qui n'ont jamais été complétées et a entraîné une utilisation 100% CPU 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 assez de réputation (encore)

5
répondu Luke Hoggett 2017-05-23 12:18:20

une autre option est de faire le tri en mémoire après avoir tiré 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 des listes énormes, vous devriez être très bien.

si vous jetez un coup d'oeil au billet de Jeff, vous pouvez trouver beaucoup d'algorithmes pour n'importe quelle langue avec laquelle vous pourriez travailler. http://www.codinghorror.com/blog/archives/001018.html

4
répondu Bob 2008-09-30 15:54:00

si vous ne voulez pas réinventer la roue ou avoir un mal de tête avec beaucoup de code qui ne fonctionne pas, il suffit d'utiliser Drupal Natural Sort ... Il suffit de lancer le SQL qui vient zippé (MySQL ou Postgre), et c'est tout. Lorsque vous faites une requête, commandez simplement en utilisant:

... ORDER BY natsort_canon(column_name, 'natural')
4
répondu Neto Queiroz 2017-05-04 17:56:31

Ajouter un champ pour "clé de tri" qui a toutes les chaînes de chiffres zéro-rembourré à une longueur fixe et puis trier sur ce champ à la place.

si vous pouvez avoir de longues chaînes de chiffres, une autre méthode consiste à pré-tendre le nombre de chiffres (Largeur fixe, zéro-rembourré) à chaque chaîne de chiffres. Par exemple, si vous n'avez pas plus de 99 chiffres dans une rangée, alors pour "Super Blast 10 Ultra" la touche de tri serait "Super Blast 0210 Ultra".

3
répondu tye 2008-10-02 04:31:08

vous pouvez aussi 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 pour trier.

dans ma requête, je voulais le " - " devant tout, puis les nombres, puis le texte. Ce qui pourrait donner quelque chose comme:

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

de cette façon vous n'avez pas à maintenir la colonne de tri dans le bon ordre que vous ajoutez des données. Vous pouvez également modifier votre ordre de tri selon ce que vous besoin.

3
répondu antoine 2013-10-17 12:35:27
utilisez cette requête:

SELECT 
    column_name 
FROM 
    table_name 
ORDER BY
    column_name REGEXP '^\d*[^\da-z&\.\' \-\"\!\@\#$\%\^\*\(\)\;\:\,\?\/\~\`\|\_\-]' DESC, 
    column_name + 0, 
    column_name;
3
répondu Guma 2016-06-13 15:16:55

j'ai essayé plusieurs solutions mais le fait 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
répondu Tarik 2016-08-25 16:13:08

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 future, mais la demande de fonctionnalité (#1588) est ouverte depuis 2003, donc je ne retiendrai pas mon souffle.

1
répondu Bob Fanger 2011-03-09 22:36:50

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 dans le domaine, 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
répondu bonger 2015-03-02 11:33:19

il y a aussi natsort . Il est destiné à faire partie d'un Drupal plugin , mais il fonctionne bien autonome.

0
répondu Peter V. Mørch 2011-06-20 07:58:55

je sais que ce sujet est ancien mais je pense que j'ai 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

la Ferraille, il a trié les suivantes mal réglé (C'est inutile lol):

Final Fantasy 1 Final Fantasy 2 Final Fantasy 5 Final Fantasy 7 Final Fantasy 7: Advent Children Final Fantasy 12 Final Fantasy 112 FF1 FF2

-4
répondu user1467716 2012-11-12 15:43:00