ORDER BY et comparaison de chaînes mixtes de lettres et de chiffres


9

Nous devons faire des rapports sur les valeurs qui sont généralement des chaînes mixtes de chiffres et de lettres qui doivent être triées «naturellement». Des choses comme, par exemple, "P7B18" ou "P12B3". @Les chaînes seront principalement des séquences de lettres puis des nombres alternés. Cependant, le nombre de ces segments et la longueur de chacun peuvent varier.

Nous aimerions que les parties numériques de celles-ci soient triées dans l'ordre numérique. Évidemment, si je gère directement ces valeurs de chaîne avec ORDER BY, alors "P12B3" va précéder "P7B18", car "P1" est antérieur à "P7", mais j'aimerais l'inverse, car "P7" précède naturellement "P12".

Je voudrais également pouvoir faire des comparaisons de gamme, par exemple, @bin < 'P13S6'ou certaines autres. Je n'ai pas à gérer les nombres à virgule flottante ou négatifs; ce seront strictement des entiers non négatifs avec lesquels nous avons affaire. Les longueurs de chaîne et le nombre de segments peuvent potentiellement être arbitraires, sans limites supérieures fixes.

Dans notre cas, la casse des chaînes n'est pas importante, bien que s'il existe un moyen de le faire de manière sensible au classement, d'autres pourraient le trouver utile. La partie la plus laide de tout cela est que j'aimerais pouvoir faire à la fois le tri et le filtrage de plage dans la WHEREclause.

Si je faisais cela en C #, ce serait une tâche assez simple: faire une analyse pour séparer l'alpha du numérique, implémenter IComparable, et vous avez essentiellement terminé. Bien sûr, SQL Server ne semble pas offrir de fonctionnalités similaires, du moins pour autant que je sache.

Quelqu'un connaît de bonnes astuces pour faire fonctionner cela? Y a-t-il une possibilité peu médiatisée de créer des types CLR personnalisés qui implémentent IComparable et ont ce comportement comme prévu? Je ne suis pas non plus opposé aux astuces XML stupides (voir aussi: concaténation de liste), et j'ai aussi des fonctions de wrapper de correspondance / extraction / remplacement de regex CLR disponibles sur le serveur.

EDIT: Comme un exemple un peu plus détaillé, je voudrais que les données se comportent quelque chose comme ça.

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0

c'est-à-dire briser les chaînes en jetons de toutes les lettres ou de tous les nombres et les trier respectivement par ordre alphabétique ou numérique, les jetons les plus à gauche étant le terme de tri le plus significatif. Comme je l'ai mentionné, morceau de gâteau dans .NET si vous implémentez IComparable, mais je ne sais pas comment (ou si) vous pouvez faire ce genre de chose dans SQL Server. Ce n'est certainement pas quelque chose que j'ai rencontré en une dizaine d'années de travail avec.


Vous pouvez le faire avec une sorte de colonne calculée indexée, transformant la chaîne en un entier. Cela P7B12pourrait donc devenir P 07 B 12(via ASCII) 80 07 65 12, donc80076512
Philᵀᴹ

Je vous suggère de créer une colonne calculée qui remplit chaque composant numérique sur une grande longueur (c'est-à-dire 10 zéros). Étant donné que le format est assez arbitraire, vous aurez besoin d'une expression en ligne assez grande, mais c'est faisable. Ensuite, vous pouvez indexer / trier par / où sur cette colonne autant que vous le souhaitez.
Nick.McDermaid

Veuillez consulter le lien que je viens d'ajouter en haut de ma réponse :)
Solomon Rutzky

1
@srutzky Nice, j'ai voté pour.
db2

Hey db2: en raison du passage de Microsoft de Connect à UserVoice et du fait de ne pas garder exactement le décompte des votes (ils l'ont mis dans un commentaire mais pas sûr de le regarder), vous devrez peut-être voter à nouveau pour cela: Soutenir le "tri naturel" / DIGITSASNUMBERS comme option de classement . Merci!
Solomon Rutzky

Réponses:


8

Vous voulez un moyen sensé et efficace de trier les nombres en chaînes en tant que nombres réels? Envisagez de voter pour ma suggestion Microsoft Connect: Prise en charge du "tri naturel" / DIGITSASNUMBERS comme option de classement


Il n'y a pas de moyen facile et intégré de le faire, mais voici une possibilité:

Normalisez les chaînes en les reformatant en segments de longueur fixe:

  • Créez une colonne de tri de type VARCHAR(50) COLLATE Latin1_General_100_BIN2. La longueur maximale de 50 peut devoir être ajustée en fonction du nombre maximal de segments et de leurs longueurs maximales potentielles.
  • Bien que la normalisation puisse être effectuée plus efficacement dans la couche d'application, la gestion de cela dans la base de données à l'aide d'un UDF T-SQL permettrait de placer l'UDF scalaire dans un AFTER [or FOR] INSERT, UPDATEdéclencheur de sorte que vous êtes assuré de définir correctement la valeur de tous les enregistrements, même ceux entrant via des requêtes ad hoc, etc. Bien sûr, cette UDF scalaire peut également être gérée via SQLCLR, mais elle devra être testée pour déterminer laquelle est réellement plus efficace. **
  • L'UDF (qu'il soit en T-SQL ou SQLCLR) doit:
    • Traitez un nombre inconnu de segments en lisant chaque caractère et en vous arrêtant lorsque le type passe d'alpha à numérique ou de numérique à alpha.
    • Pour chaque segment, il doit renvoyer une chaîne de longueur fixe définie au maximum de caractères / chiffres possibles de tout segment (ou peut-être max + 1 ou 2 pour tenir compte de la croissance future).
    • Les segments alpha doivent être justifiés à gauche et remplis à droite avec des espaces.
    • Les segments numériques doivent être justifiés à droite et remplis à gauche de zéros.
    • Si les caractères alpha peuvent apparaître en casse mixte mais que l'ordre doit être insensible à la casse, appliquez la UPPER()fonction au résultat final de tous les segments (de sorte qu'elle ne doit être effectuée qu'une seule fois et non par segment). Cela permettra un tri correct étant donné le classement binaire de la colonne de tri.
  • Créez un AFTER INSERT, UPDATEdéclencheur sur la table qui appelle l'UDF pour définir la colonne de tri. Pour améliorer les performances, utilisez la UPDATE()fonction pour déterminer si cette colonne de code se trouve même dans la SETclause de l' UPDATEinstruction (simplement RETURNsi elle est fausse), puis joignez les INSERTEDet DELETEDpseudo-tables de la colonne de code pour ne traiter que les lignes dont les valeurs de code ont été modifiées . Veillez à spécifier COLLATE Latin1_General_100_BIN2cette condition JOIN pour garantir la précision de la détermination de la modification.
  • Créez un index sur la nouvelle colonne de tri.

Exemple:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"

Dans cette approche, vous pouvez trier via:

ORDER BY tbl.SortColumn

Et vous pouvez effectuer un filtrage de plage via:

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')

ou:

DECLARE @RangeStart VARCHAR(50),
        @RangeEnd VARCHAR(50);
SELECT @RangeStart = dbo.MyUDF('P7B18'),
       @RangeEnd = dbo.MyUDF('P12B3');

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

Le filtre ORDER BYet le WHEREfiltre doivent utiliser le classement binaire défini pour en SortColumnraison de la priorité du classement .

Les comparaisons d'égalité seraient toujours effectuées sur la colonne de valeur d'origine.


D'autres pensées:

  • Utilisez un UDT SQLCLR. Cela pourrait fonctionner, bien qu'il ne soit pas clair s'il présente un gain net par rapport à l'approche décrite ci-dessus.

    Oui, un UDT SQLCLR peut faire remplacer ses opérateurs de comparaison par des algorithmes personnalisés. Cela gère les situations dans lesquelles la valeur est comparée à une autre valeur qui est déjà du même type personnalisé ou à une valeur qui doit être implicitement convertie. Cela devrait gérer le filtre de gamme dans un WHEREétat.

    En ce qui concerne le tri de l'UDT en tant que type de colonne normal (pas une colonne calculée), cela n'est possible que si l'UDT est "ordonné en octets". Être "ordonné par octets" signifie que la représentation binaire de l'UDT (qui peut être définie dans l'UDT) trie naturellement dans l'ordre approprié. En supposant que la représentation binaire est traitée de manière similaire à l'approche décrite ci-dessus pour la colonne VARCHAR (50) qui a des segments de longueur fixe qui sont rembourrés, cela serait admissible. Ou, s'il n'était pas facile de garantir que la représentation binaire serait naturellement ordonnée de la bonne manière, vous pourriez exposer une méthode ou une propriété de l'UDT qui génère une valeur qui serait correctement ordonnée, puis créer une PERSISTEDcolonne calculée sur celle-ci. méthode ou propriété. La méthode doit être déterministe et marquée comme IsDeterministic = true.

    Les avantages de cette approche sont:

    • Pas besoin d'un champ "valeur d'origine".
    • Pas besoin d'appeler un UDF pour insérer les données ou comparer des valeurs. En supposant que la Parseméthode de l'UDT prend la P7B18valeur et la convertit, vous devriez pouvoir simplement insérer les valeurs naturellement comme P7B18. Et avec la méthode de conversion implicite définie dans l'UDT, la condition WHERE permettrait également d'utiliser simplement P7B18 ».

    Les conséquences de cette approche sont:

    • Si vous sélectionnez simplement le champ, la représentation binaire sera renvoyée si vous utilisez l'octet UDT ordonné comme type de données de colonne. Ou si vous utilisez une PERSISTEDcolonne calculée sur une propriété ou une méthode de l'UDT, vous obtiendrez alors la représentation renvoyée par la propriété ou la méthode. Si vous souhaitez la P7B18valeur d' origine , vous devez appeler une méthode ou une propriété de l'UDT codée pour renvoyer cette représentation. Étant donné que vous devez de ToStringtoute façon remplacer la méthode, c'est un bon candidat pour fournir cela.
    • Il n'est pas clair (du moins pour moi en ce moment car je n'ai pas testé cette partie) à quel point il serait facile / difficile d'apporter des modifications à la représentation binaire. La modification de la représentation stockée et triable peut nécessiter la suppression et l'ajout du champ. En outre, la suppression de l'assembly contenant l'UDT échouerait si elle était utilisée de l'une ou l'autre manière, vous devez donc vous assurer qu'il n'y avait rien d'autre dans l'assembly que cet UDT. Vous pouvez ALTER ASSEMBLYremplacer la définition, mais il existe certaines restrictions à ce sujet.

      D'un autre côté, le VARCHAR()champ est constitué de données déconnectées de l'algorithme et ne nécessiterait que la mise à jour de la colonne. Et s'il y a des dizaines de millions de lignes (ou plus), cela peut être fait dans une approche par lots.

  • Implémentez la bibliothèque ICU qui permet de faire ce tri alphanumérique. Bien que très fonctionnelle, la bibliothèque n'est disponible qu'en deux langues: C / C ++ et Java. Ce qui signifie que vous devrez peut-être effectuer quelques ajustements pour le faire fonctionner dans Visual C ++, ou il y a de fortes chances que le code Java puisse être converti en MSIL à l'aide d' IKVM . Il existe un ou deux projets côté .NET liés sur ce site qui fournissent une interface COM accessible en code managé, mais je pense qu'ils n'ont pas été mis à jour depuis un certain temps et je ne les ai pas essayés. Le mieux serait ici de gérer cela dans la couche d'application dans le but de générer des clés de tri. Les clés de tri seraient alors enregistrées dans une nouvelle colonne de tri.

    Ce n'est peut-être pas l'approche la plus pratique. Cependant, il est toujours très cool qu'une telle capacité existe. J'ai fourni un examen plus détaillé d'un exemple de cela dans la réponse suivante:

    Existe-t-il un classement pour trier les chaînes suivantes dans l'ordre suivant 1,2,3,6,10,10A, 10B, 11?

    Mais le schéma traité dans cette question est un peu plus simple. Pour un exemple montrant que le type de modèle traité dans cette question fonctionne également, veuillez vous rendre sur la page suivante:

    ICU Collation Demo

    Sous "Paramètres", définissez l'option "numérique" sur "activé" et toutes les autres doivent être définies sur "par défaut". Ensuite, à droite du bouton "trier", décochez l'option "forces de diff" et cochez l'option "trier les clés". Remplacez ensuite la liste des éléments de la zone de texte "Entrée" par la liste suivante:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23

    Cliquez sur le bouton "trier". La zone de texte "Sortie" doit afficher les éléments suivants:

    as456456hgjg6786867
        29 4D 0F 7A EA C8 37 35 3B 35 0F 84 17 A7 0F 93 90 , 0D , , 0D .
    P7B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P007B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P7BA19
        47 0F 09 2B 29 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19
        47 0F 09 2B 2B 0F 15 , 09 , FD F2 , DC C5 DC 06 .
    P7BB19
        47 0F 09 2B 2B 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19z23
        47 0F 09 2B 2B 0F 15 5B 0F 19 , 0B , FD F4 , DC C5 DC 08 .
    P7Bb20
        47 0F 09 2B 2B 0F 16 , 09 , FD F2 , DC C5 DC 06 .
    P12B3
        47 0F 0E 2B 0F 05 , 08 , FD F1 , DC C5 DC 05 .
    P12B22
        47 0F 0E 2B 0F 18 , 08 , FD F1 , DC C5 DC 05 .

    Veuillez noter que les clés de tri sont structurées en plusieurs champs, séparés par des virgules. Chaque champ doit être trié indépendamment, ce qui présente un autre petit problème à résoudre si vous devez l'implémenter dans SQL Server.


** En cas de problème de performances concernant l'utilisation des fonctions définies par l'utilisateur, veuillez noter que les approches proposées les utilisent le moins possible. En fait, la principale raison du stockage de la valeur normalisée était d'éviter d'appeler un UDF pour chaque ligne de chaque requête. Dans l'approche principale, l'UDF est utilisé pour définir la valeur de SortColumn, et cela se fait uniquement sur INSERTet UPDATEvia le déclencheur. La sélection de valeurs est beaucoup plus courante que l'insertion et la mise à jour, et certaines valeurs ne sont jamais mises à jour. Pour chaque SELECTrequête qui utilise le SortColumnpour un filtre de plage dans la WHEREclause, l'UDF n'est nécessaire qu'une seule fois pour chacune des valeurs range_start et range_end pour obtenir les valeurs normalisées; l'UDF n'est pas appelé par ligne.

En ce qui concerne l'UDT, l'utilisation est en fait la même qu'avec l'UDF scalaire. La signification, l'insertion et la mise à jour appellent la méthode de normalisation une fois par chaque ligne pour définir la valeur. Ensuite, la méthode de normalisation serait appelée une fois par requête pour chaque range_start et range_value dans un filtre de plage, mais pas par ligne.

Un point en faveur de la gestion de la normalisation entièrement dans un FDU SQLCLR est que, étant donné qu'il ne fait aucun accès aux données et est déterministe, s'il est marqué comme IsDeterministic = true, il peut alors participer à des plans parallèles (ce qui pourrait aider les opérations INSERTet UPDATE) alors qu'un T-SQL UDF empêchera l'utilisation d'un plan parallèle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.