Question

Nous avons une grande base de données sur laquelle nous avons une pagination côté DB.C'est rapide, renvoyant une page de 50 lignes à partir de millions d'enregistrements en une petite fraction de seconde.

Les utilisateurs peuvent définir leur propre tri, en choisissant essentiellement la colonne sur laquelle trier.Les colonnes sont dynamiques : certaines comportent des valeurs numériques, d'autres des dates et du texte.

Alors que la plupart trient comme prévu, le texte est trié de manière stupide.Eh bien, je dis bête, cela a du sens pour les ordinateurs, mais frustre les utilisateurs.

Par exemple, le tri par identifiant d'enregistrement de chaîne donne quelque chose comme :

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...et ainsi de suite.

Je veux que cela tienne compte du numéro, donc :

rec1
rec2
rec3
rec4
rec10
rec14
rec20

Je ne peux pas contrôler l'entrée (sinon je formaterais simplement en 000) et je ne peux pas compter sur un seul format - certains sont des choses comme "{code alpha}-{code département}-{identifiant d'enregistrement}".

Je connais plusieurs façons de procéder en C#, mais je ne peux pas extraire tous les enregistrements pour les trier, car cela ralentirait.

Quelqu'un connaît-il un moyen d'appliquer rapidement un tri naturel sur le serveur SQL ?


Nous utilisons :

ROW_NUMBER() over (order by {field name} asc)

Et puis nous passons par là.

Nous pouvons ajouter des déclencheurs, même si nous ne le ferions pas.Toutes leurs entrées sont paramétrées, etc., mais je ne peux pas changer le format - s'ils mettent "rec2" et "rec10", ils s'attendent à ce qu'ils soient renvoyés comme ça, et dans l'ordre naturel.


Nous disposons d'entrées utilisateur valides qui suivent différents formats pour différents clients.

On pourrait aller rec1, rec2, rec3, ...rec100, rec101

Tandis qu'un autre pourrait aller :grp1rec1, grp1rec2, ...grp20rec300, grp20rec301

Quand je dis que nous ne pouvons pas contrôler l'entrée, je veux dire que nous ne pouvons pas forcer les utilisateurs à modifier ces normes - elles ont une valeur comme grp1rec1 et je ne peux pas la reformater en grp01rec001, car cela modifierait quelque chose utilisé pour les recherches et liaison avec des systèmes externes.

Ces formats varient beaucoup, mais sont souvent des mélanges de lettres et de chiffres.

Les trier en C# est facile - il suffit de les diviser en { "grp", 20, "rec", 301 } puis comparez les valeurs de séquence tour à tour.

Cependant, il peut y avoir des millions d'enregistrements et les données sont paginées, j'ai besoin que le tri soit effectué sur le serveur SQL.

Le serveur SQL trie par valeur, pas par comparaison - en C#, je peux diviser les valeurs pour les comparer, mais en SQL, j'ai besoin d'une logique qui (très rapidement) obtient une valeur unique qui trie de manière cohérente.

@moebius - votre réponse pourrait fonctionner, mais cela ressemble à un vilain compromis d'ajouter une clé de tri pour toutes ces valeurs de texte.

Était-ce utile?

La solution

La plupart des solutions basées sur SQL que j'ai vues échouent lorsque les données deviennent suffisamment complexes (par ex.il contient plus d'un ou deux chiffres).Au départ, j'ai essayé d'implémenter une fonction NaturalSort dans T-SQL qui répondait à mes exigences (entre autres choses, gère un nombre arbitraire de nombres dans la chaîne), mais les performances étaient chemin trop lent.

En fin de compte, j'ai écrit une fonction CLR scalaire en C# pour permettre un tri naturel, et même avec du code non optimisé, les performances en l'appelant depuis SQL Server sont incroyablement rapides.Il présente les caractéristiques suivantes :

  • triera correctement les 1 000 premiers caractères environ (facilement modifiés dans le code ou transformés en paramètre)
  • trie correctement les décimales, donc 123,333 précède 123,45
  • à cause de ce qui précède, il est probable que les éléments tels que les adresses IP ne seront PAS triés correctement ;si vous souhaitez un comportement différent, modifiez le code
  • prend en charge le tri d'une chaîne contenant un nombre arbitraire de nombres
  • triera correctement les nombres jusqu'à 25 chiffres (facilement modifiés en code ou transformés en paramètre)

Le code est ici :

using System;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public class UDF
{
    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic=true)]
    public static SqlString Naturalize(string val)
    {
        if (String.IsNullOrEmpty(val))
            return val;

        while(val.Contains("  "))
            val = val.Replace("  ", " ");

        const int maxLength = 1000;
        const int padLength = 25;

        bool inNumber = false;
        bool isDecimal = false;
        int numStart = 0;
        int numLength = 0;
        int length = val.Length < maxLength ? val.Length : maxLength;

        //TODO: optimize this so that we exit for loop once sb.ToString() >= maxLength
        var sb = new StringBuilder();
        for (var i = 0; i < length; i++)
        {
            int charCode = (int)val[i];
            if (charCode >= 48 && charCode <= 57)
            {
                if (!inNumber)
                {
                    numStart = i;
                    numLength = 1;
                    inNumber = true;
                    continue;
                }
                numLength++;
                continue;
            }
            if (inNumber)
            {
                sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));
                inNumber = false;
            }
            isDecimal = (charCode == 46);
            sb.Append(val[i]);
        }
        if (inNumber)
            sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));

        var ret = sb.ToString();
        if (ret.Length > maxLength)
            return ret.Substring(0, maxLength);

        return ret;
    }

    static string PadNumber(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

Pour l'enregistrer afin de pouvoir l'appeler depuis SQL Server, exécutez les commandes suivantes dans l'Analyseur de requêtes :

CREATE ASSEMBLY SqlServerClr FROM 'SqlServerClr.dll' --put the full path to DLL here
go
CREATE FUNCTION Naturalize(@val as nvarchar(max)) RETURNS nvarchar(1000) 
EXTERNAL NAME SqlServerClr.UDF.Naturalize
go

Ensuite, vous pouvez l'utiliser comme ceci :

select *
from MyTable
order by dbo.Naturalize(MyTextField)

Note:Si vous obtenez une erreur dans SQL Server du type L'exécution du code utilisateur dans le .NET Framework est désactivée.Activez l'option de configuration "clr activé"., Suivez les instructions ici pour l'activer.Assurez-vous de considérer les implications en matière de sécurité avant de le faire.Si vous n'êtes pas l'administrateur de la base de données, assurez-vous d'en discuter avec votre administrateur avant d'apporter des modifications à la configuration du serveur.

Note 2:Ce code ne prend pas correctement en charge l'internationalisation (par exemple, il suppose que le marqueur décimal est ".", n'est pas optimisé pour la vitesse, etc.Les suggestions pour l’améliorer sont les bienvenues !

Modifier: Renommé la fonction en Naturaliser au lieu de Tri Naturel, car il n'effectue aucun tri réel.

Autres conseils

order by LEN(value), value

Pas parfait, mais fonctionne bien dans de nombreux cas.

Je sais que c'est une vieille question mais je viens de tomber dessus et comme elle n'a pas de réponse acceptée.

J'ai toujours utilisé des méthodes similaires à celle-ci :

SELECT [Column] FROM [Table]
ORDER BY RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))), 1000)

Les seuls cas courants où cela pose problème sont si votre colonne ne sera pas convertie en VARCHAR(MAX), ou si LEN([Column]) > 1000 (mais vous pouvez changer ce 1000 en autre chose si vous le souhaitez), mais vous pouvez utiliser cette idée approximative pour ce dont vous avez besoin.

De plus, les performances sont bien pires que la normale ORDER BY [Column], mais cela vous donne le résultat demandé dans l'OP.

Modifier:Juste pour clarifier davantage, ce qui précède ne fonctionnera pas si vous avez des valeurs décimales telles que avoir 1, 1.15 et 1.5, (ils seront triés comme {1, 1.5, 1.15}) car ce n'est pas ce qui est demandé dans le PO, mais cela peut facilement être fait en :

SELECT [Column] FROM [Table]
ORDER BY REPLACE(RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))) + REPLICATE('0', 100 - CHARINDEX('.', REVERSE(LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX))))), 1)), 1000), '.', '0')

Résultat: {1, 1.15, 1.5}

Et toujours entièrement en SQL.Cela ne triera pas les adresses IP car vous entrez maintenant dans des combinaisons de chiffres très spécifiques, par opposition à un simple texte + chiffre.

Réponse de RedFilter est idéal pour les ensembles de données de taille raisonnable où l'indexation n'est pas critique, mais si vous souhaitez un index, plusieurs ajustements sont nécessaires.

Tout d’abord, marquez la fonction comme n’effectuant aucun accès aux données et comme étant déterministe et précise :

[SqlFunction(DataAccess = DataAccessKind.None,
                          SystemDataAccess = SystemDataAccessKind.None,
                          IsDeterministic = true, IsPrecise = true)]

Ensuite, MSSQL a une limite de 900 octets sur la taille de la clé d'index, donc si la valeur naturalisée est la seule valeur de l'index, elle doit contenir au maximum 450 caractères.Si l'index comprend plusieurs colonnes, la valeur de retour doit être encore plus petite.Deux changements :

CREATE FUNCTION Naturalize(@str AS nvarchar(max)) RETURNS nvarchar(450)
    EXTERNAL NAME ClrExtensions.Util.Naturalize

et dans le code C# :

const int maxLength = 450;

Enfin, vous devrez ajouter une colonne calculée à votre table, et elle devra être conservée (car MSSQL ne peut pas prouver que Naturalize est déterministe et précis), ce qui signifie que la valeur naturalisée est effectivement stockée dans la table mais est toujours maintenue automatiquement :

ALTER TABLE YourTable ADD nameNaturalized AS dbo.Naturalize(name) PERSISTED

Vous pouvez maintenant créer l'index !

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

J'ai également apporté quelques modifications au code de RedFilter :utiliser des caractères pour plus de clarté, intégrer la suppression des espaces en double dans la boucle principale, quitter une fois que le résultat est plus long que la limite, définir la longueur maximale sans sous-chaîne, etc.Voici le résultat :

using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public static class Util
{
    [SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)]
    public static SqlString Naturalize(string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        const int maxLength = 450;
        const int padLength = 15;

        bool isDecimal = false;
        bool wasSpace = false;
        int numStart = 0;
        int numLength = 0;

        var sb = new StringBuilder();
        for (var i = 0; i < str.Length; i++)
        {
            char c = str[i];
            if (c >= '0' && c <= '9')
            {
                if (numLength == 0)
                    numStart = i;
                numLength++;
            }
            else
            {
                if (numLength > 0)
                {
                    sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));
                    numLength = 0;
                }
                if (c != ' ' || !wasSpace)
                    sb.Append(c);
                isDecimal = c == '.';
                if (sb.Length > maxLength)
                    break;
            }
            wasSpace = c == ' ';
        }
        if (numLength > 0)
            sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));

        if (sb.Length > maxLength)
            sb.Length = maxLength;
        return sb.ToString();
    }

    private static string pad(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

Je sais que c'est un peu vieux à ce stade, mais dans ma recherche d'une meilleure solution, je suis tombé sur cette question.J'utilise actuellement une fonction pour commander.Cela fonctionne bien pour mon objectif de trier les enregistrements nommés avec des caractères alphanumériques mixtes ("élément 1", "élément 10", "élément 2", etc.)

CREATE FUNCTION [dbo].[fnMixSort]
(
    @ColValue NVARCHAR(255)
)
RETURNS NVARCHAR(1000)
AS

BEGIN
    DECLARE @p1 NVARCHAR(255),
        @p2 NVARCHAR(255),
        @p3 NVARCHAR(255),
        @p4 NVARCHAR(255),
        @Index TINYINT

    IF @ColValue LIKE '[a-z]%'
        SELECT  @Index = PATINDEX('%[0-9]%', @ColValue),
            @p1 = LEFT(CASE WHEN @Index = 0 THEN @ColValue ELSE LEFT(@ColValue, @Index - 1) END + REPLICATE(' ', 255), 255),
            @ColValue = CASE WHEN @Index = 0 THEN '' ELSE SUBSTRING(@ColValue, @Index, 255) END
    ELSE
        SELECT  @p1 = REPLICATE(' ', 255)

    SELECT  @Index = PATINDEX('%[^0-9]%', @ColValue)

    IF @Index = 0
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255),
            @ColValue = ''
    ELSE
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    SELECT  @Index = PATINDEX('%[0-9,a-z]%', @ColValue)

    IF @Index = 0
        SELECT  @p3 = REPLICATE(' ', 255)
    ELSE
        SELECT  @p3 = LEFT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    IF PATINDEX('%[^0-9]%', @ColValue) = 0
        SELECT  @p4 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255)
    ELSE
        SELECT  @p4 = LEFT(@ColValue + REPLICATE(' ', 255), 255)

    RETURN  @p1 + @p2 + @p3 + @p4

END

Alors appelle

select item_name from my_table order by fnMixSort(item_name)

Cela triple facilement le temps de traitement pour une simple lecture de données, ce n’est donc peut-être pas la solution parfaite.

Voici une solution écrite pour SQL 2000.Il peut probablement être amélioré pour les versions SQL plus récentes.

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

Voici une autre solution que j'aime bien :http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

Ce n'est pas Microsoft SQL, mais comme je me suis retrouvé ici alors que je cherchais une solution pour Postgres, j'ai pensé que l'ajouter ici aiderait les autres.

Pour la suite varchar données:

BR1
BR2
External Location
IR1
IR2
IR3
IR4
IR5
IR6
IR7
IR8
IR9
IR10
IR11
IR12
IR13
IR14
IR16
IR17
IR15
VCR

Cela a mieux fonctionné pour moi :

ORDER BY substring(fieldName, 1, 1), LEN(fieldName)

Si vous rencontrez des difficultés pour charger les données de la base de données pour les trier en C#, je suis sûr que vous serez déçu par toute approche consistant à le faire par programme dans la base de données.Lorsque le serveur doit trier, il doit calculer l'ordre "perçu" comme vous l'auriez fait - à chaque fois.

Je vous suggère d'ajouter une colonne supplémentaire pour stocker la chaîne triable prétraitée, en utilisant une méthode C#, lorsque les données sont insérées pour la première fois.Vous pouvez essayer de convertir les chiffres en plages de largeur fixe, par exemple, afin que « xyz1 » se transforme en « xyz00000001 ».Ensuite, vous pouvez utiliser le tri SQL Server normal.

Au risque de me vanter, j'ai écrit un article CodeProject implémentant le problème tel que posé dans l'article CodingHorror.Ne hésitez pas à voler mon code.

Je viens de lire un article quelque part sur un tel sujet.Le point clé est :vous n'avez besoin que de la valeur entière pour trier les données, tandis que la chaîne « rec » appartient à l'interface utilisateur.Vous pouvez diviser les informations en deux champs, par exemple alpha et num, trier par alpha et num (séparément), puis afficher une chaîne composée de alpha + num.Vous pouvez utiliser une colonne calculée pour composer la chaîne ou une vue.J'espère que cela aide

Vous pouvez utiliser le code suivant pour résoudre le problème :

Select *, 
    substring(Cote,1,len(Cote) - Len(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1)))alpha,
    CAST(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1) AS INT)intv 
FROM Documents 
   left outer join Sites ON Sites.IDSite = Documents.IDSite 
Order BY alpha, intv

Cordialement, rabihkahaleh@hotmail.com

Il vous suffit de trier par

ORDER BY 
cast (substring(name,(PATINDEX('%[0-9]%',name)),len(name))as int)

 ##

Je ne comprends toujours pas (probablement à cause de mon mauvais anglais).

Tu pourrais essayer:

ROW_NUMBER() OVER (ORDER BY dbo.human_sort(field_name) ASC)

Mais cela ne fonctionnera pas pour des millions de disques.

C'est pourquoi j'ai suggéré d'utiliser le déclencheur qui remplit séparé colonne avec valeur humaine.

De plus:

  • Les fonctions T-SQL intégrées sont vraiment lentes et Microsoft suggère d'utiliser les fonctions .NET à la place.
  • valeur humaine est constant, il n'y a donc aucun point de calculer à chaque fois lorsque la requête fonctionne.
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top