web-dev-qa-db-fra.com

Tri dynamique dans les procédures stockées SQL

C'est une question sur laquelle j'ai passé des heures à faire des recherches dans le passé. Cela me semble être quelque chose qui aurait dû être abordé dans les solutions modernes RDBMS , mais je n'ai encore rien trouvé qui réponde réellement à ce que je considère être un besoin incroyablement commun dans les applications Web ou Windows. avec une base de données.

Je parle de tri dynamique. Dans mon monde imaginaire, cela devrait être aussi simple que quelque chose comme:

ORDER BY @sortCol1, @sortCol2

Il s’agit de l’exemple canonique donné par les novices SQL et les développeurs procédure stockée sur l’ensemble des forums sur Internet. "Pourquoi ce n'est pas possible?" ils demandent. Invariablement, quelqu'un finit par leur expliquer la nature compilée des procédures stockées, des plans d'exécution en général, et toutes sortes d'autres raisons pour lesquelles il est impossible d'insérer un paramètre directement dans une clause ORDER BY.


Je sais ce que certains d'entre vous pensent déjà: "Laissez le client faire le tri, alors." Naturellement, cela décharge le travail de votre base de données. Dans notre cas cependant, nos serveurs de base de données ne sont même pas en sueur 99% du temps et ils ne sont même pas encore multi-core ni aucune des innombrables améliorations apportées à l'architecture système tous les 6 mois. Pour cette seule raison, le fait que nos bases de données gèrent le tri ne serait pas un problème. De plus, les bases de données sont very bonnes pour le tri. Ils sont optimisés pour cela et ont eu des années pour bien faire les choses, le langage utilisé est incroyablement flexible, intuitif et simple, et tout écrivain débutant en langage SQL sait le faire et, plus important encore, il sait le modifier, apporter des modifications, faire de la maintenance, etc. Lorsque vos bases de données sont loin d'être taxées et que vous souhaitez simplement simplifier (et raccourcir!) le temps de développement, cela semble être un choix évident.

Ensuite, il y a le problème du Web. J'ai joué avec JavaScript qui effectuera un tri côté client des tableaux HTML, mais ceux-ci ne sont inévitablement pas assez souples pour répondre à mes besoins et, encore une fois, mes bases de données ne sont pas excessivement taxées et peuvent vraiment trier vraiment facilement, j’ai du mal à justifier le temps qu’il faudrait pour réécrire ou trier JavaScript par ma propre. Il en va généralement de même pour le tri côté serveur, même s'il est probablement déjà préférable à JavaScript. Je ne suis pas du genre à aimer les frais généraux liés aux ensembles de données, alors poursuivez-moi en justice.

Mais cela ramène le fait que ce n'est pas possible - ou plutôt, pas facilement. J'ai utilisé, avec des systèmes antérieurs, un moyen incroyablement astucieux d'obtenir un tri dynamique. Ce n'était pas joli, ni intuitif, simple ou flexible et un rédacteur SQL débutant serait perdu en quelques secondes. Déjà, il ne s’agit pas tant d’une "solution" que d’une "complication".


Les exemples suivants ne sont pas destinés à exposer les meilleures pratiques, un bon style de codage ou quoi que ce soit, et ils ne sont pas révélateurs de mes capacités en tant que programmeur T-SQL. Ils sont ce qu’ils sont et j’admets pleinement qu’ils sont déroutants, de mauvaise forme et tout simplement du hack.

Nous passons une valeur entière en tant que paramètre à une procédure stockée (appelons simplement le paramètre "sort") et à partir de là, nous déterminons un tas d'autres variables. Par exemple ... Disons que le tri est 1 (ou la valeur par défaut):

DECLARE @sortCol1 AS varchar(20)
DECLARE @sortCol2 AS varchar(20)
DECLARE @dir1 AS varchar(20)
DECLARE @dir2 AS varchar(20)
DECLARE @col1 AS varchar(20)
DECLARE @col2 AS varchar(20)

SET @col1 = 'storagedatetime';
SET @col2 = 'vehicleid';

IF @sort = 1                -- Default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'asc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'asc';
END
ELSE IF @sort = 2           -- Reversed order default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'desc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'desc';
END

Vous pouvez déjà voir comment, si je déclarais plus de variables @colX pour définir d'autres colonnes, je pourrais vraiment être créatif avec les colonnes à trier en fonction de la valeur de "sort" ... pour l'utiliser, cela finit généralement par ressembler à ce qui suit clause incroyablement salissante:

ORDER BY
    CASE @dir1
        WHEN 'desc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir1
        WHEN 'asc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END,
    CASE @dir2
        WHEN 'desc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir2
        WHEN 'asc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END

Évidemment, ceci est un exemple très dépouillé. Le vrai matériel, puisque nous avons généralement quatre ou cinq colonnes sur lesquelles appuyer le tri, chacune avec éventuellement une deuxième ou même une troisième colonne (par exemple date décroissante puis triée secondairement par nom croissant) et tri directionnel qui double effectivement le nombre de cas. Ouais ... ça devient poilu très vite.

L'idée est que l'on pourrait "facilement" changer les cas de tri de telle sorte que vehicleid soit trié avant l'heure de stockage stockée ... mais la pseudo-flexibilité, du moins dans cet exemple simple, s'arrête là. Essentiellement, chaque cas qui échoue à un test (car notre méthode de tri ne lui est pas applicable cette fois-ci) génère une valeur NULL. Et ainsi vous vous retrouvez avec une clause qui fonctionne comme suit:

ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah

Vous avez eu l'idée. Cela fonctionne car SQL Server ignore effectivement les valeurs null dans les clauses order by. C’est incroyablement difficile à maintenir, comme le savent probablement tous ceux qui ont une connaissance de base de SQL. Si j'ai perdu l'un de vous, ne vous sentez pas mal. Il nous a fallu beaucoup de temps pour le faire fonctionner et nous sommes toujours perplexes lorsque nous essayons de le modifier ou d'en créer de nouveaux. Heureusement, il n'est pas nécessaire de changer souvent, sinon cela deviendrait vite "ne vaut pas la peine".

Pourtant, il a fait a fonctionné.


Ma question est alors:existe-t-il un meilleur moyen?

Les solutions autres que les procédures stockées me conviennent, car je me rends compte que ce n’est peut-être pas la solution. De préférence, j'aimerais savoir si quelqu'un peut mieux le faire avec la procédure stockée, mais sinon, comment pouvez-vous tous laisser l'utilisateur trier de manière dynamique les tables de données (également dans les deux sens) avec ASP.NET?

Et merci d'avoir lu (ou au moins écumé) une si longue question!

PS: soyez heureux de ne pas avoir illustré mon exemple de procédure stockée prenant en charge le tri dynamique, le filtrage dynamique/la recherche de texte dans les colonnes, la pagination via ROWNUMBER () OVER,ETtry ... catch avec l'annulation de transaction sur les erreurs ... "taille gigantesque" ne commence même pas à les décrire.


Mettre à jour:

  • Je voudraiséviter le SQL dynamique. Le fait d’analyser une chaîne ensemble et d’exécuter un EXEC annule l’objet principal d’une procédure stockée. Parfois, je me demande cependant si les inconvénients de faire une telle chose ne valent pas la peine, du moins dans ces cas spéciaux de tri dynamique. Malgré tout, je me sens toujours sale chaque fois que je fais des chaînes SQL dynamiques comme celle-ci - comme si je vivais toujours dans le monde Classic ASP.
  • La plupart des raisons pour lesquelles nous voulons des procédures stockées sont poursecurity. Je n'arrive pas à faire appel aux problèmes de sécurité, je suggère seulement des solutions. Avec SQL Server 2005, nous pouvons définir des autorisations (au cas par cas, au cas par cas) au niveau du schéma sur des procédures stockées individuelles, puis refuser directement toute requête sur les tables. Critiquer le pour et le contre de cette approche relève peut-être d'une autre question, mais là encore, ce n'est pas ma décision. Je ne suis que le singe à code principal. :)
126
Sean Hanley

Oui, c'est pénible, et ta façon de faire ressemble à ce que je fais:

order by
case when @SortExpr = 'CustomerName' and @SortDir = 'ASC' 
    then CustomerName end asc, 
case when @SortExpr = 'CustomerName' and @SortDir = 'DESC' 
    then CustomerName end desc,
...

Pour moi, cela reste bien meilleur que la création de code SQL dynamique à partir de code, qui se transforme en un cauchemar d’évolutivité et de maintenance pour les administrateurs de base de données.

Ce que je fais du code est de refactoriser la pagination et le tri afin que je n’ai au moins pas beaucoup de répétition avec des valeurs remplies pour @SortExpr et @SortDir.

En ce qui concerne le code SQL, conservez la même conception et le même formatage entre différentes procédures stockées afin qu’il soit au moins propre et reconnaissable lorsque vous apportez des modifications.

92
Eric Z Beard

Cette approche empêche les colonnes triables d'être dupliquées deux fois dans l'ordre et est un peu plus lisible:

SELECT
  s.*
FROM
  (SELECT
    CASE @SortCol1
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol1,
    CASE @SortCol2
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol2,
    t.*
  FROM
    MyTable t) as s
ORDER BY
  CASE WHEN @dir1 = 'ASC'  THEN SortCol1 END ASC,
  CASE WHEN @dir1 = 'DESC' THEN SortCol1 END DESC,
  CASE WHEN @dir2 = 'ASC'  THEN SortCol2 END ASC,
  CASE WHEN @dir2 = 'DESC' THEN SortCol2 END DESC
23
Jason DeFontes

Mes applications le font souvent mais elles construisent toutes dynamiquement le code SQL. Cependant, lorsque je traite des procédures stockées, je le fais:

  1. Faites de la procédure stockée une fonction qui retourne une table de vos valeurs - pas de tri.
  2. Ensuite, dans votre code d’application, faites une select * from dbo.fn_myData() where ... order by ... pour pouvoir y spécifier dynamiquement l’ordre de tri.

La partie dynamique se trouve alors au moins dans votre application, mais la base de données continue de s’acquitter de tâches lourdes.

6
Ron Savage

Le SQL dynamique est toujours une option. Vous devez simplement décider si cette option est plus acceptable que ce que vous avez actuellement.

Voici un article qui montre que: http://www.4guysfromrolla.com/webtech/010704-1.shtml

6
jop

Il existe peut-être une troisième option, car votre serveur dispose de nombreux cycles de secours: utilisez une procédure d'assistance pour effectuer le tri via une table temporaire. Quelque chose comme

create procedure uspCallAndSort
(
    @sql varchar(2048),        --exec dbo.uspSomeProcedure arg1,'arg2',etc.
    @sortClause varchar(512)    --comma-delimited field list
)
AS
insert into #tmp EXEC(@sql)
declare @msql varchar(3000)
set @msql = 'select * from #tmp order by ' + @sortClause
EXEC(@msql)
drop table #tmp
GO

Mise en garde: je n'ai pas testé cela, mais cela "devrait" fonctionner dans SQL Server 2005 (ce qui créera une table temporaire à partir d'un jeu de résultats sans spécifier les colonnes à l'avance.)

4
Steven A. Lowe

Une technique de procédure stockée (hack?) Utilisée pour éviter le SQL dynamique pour certains travaux consiste à avoir une colonne de tri unique. C'est à dire.,

SELECT
   name_last,
   name_first,
   CASE @sortCol WHEN 'name_last' THEN [name_last] ELSE 0 END as mySort
FROM
   table
ORDER BY 
    mySort

Celui-ci est facile à battre en soumission - vous pouvez concaténer des champs dans votre colonne mySort, inverser l'ordre avec des fonctions mathématiques ou de date, etc.

De préférence cependant, j'utilise mes vues de grille asp.net ou d'autres objets avec un tri intégré pour effectuer le tri pour moi APRÈS la récupération des données de Sql-Server. Ou même si cela n’est pas intégré - par exemple, des tables de données, etc. dans asp.net.

4
dave

Il y a différentes façons de pirater ceci. 

Conditions préalables:

  1. Une seule instruction SELECT dans le fichier Sp 
  2. Oubliez le tri (ou ayez Un défaut)

Puis insérez dans une table temporaire:

create table #temp ( your columns )

insert #temp
exec foobar

select * from #temp order by whatever

Méthode n ° 2: configurez un serveur lié sur lui-même, puis sélectionnez-le à l'aide de openquery: http://www.sommarskog.se/share_data.html#OPENQUERY

4
Matt Rogish

Un argument contre le tri côté client concerne les données volumineuses et la pagination. Une fois que votre nombre de lignes dépasse ce que vous pouvez facilement afficher, vous effectuez souvent un tri dans le cadre d'un saut/reprise que vous souhaitez probablement exécuter en SQL.

Pour Entity Framework, vous pouvez utiliser une procédure stockée pour gérer votre recherche de texte. Si vous rencontrez le même problème de tri, la solution que j'ai vue consiste à utiliser un proc stocké pour la recherche, en renvoyant uniquement une clé id définie pour la correspondance. Ensuite, interrogez à nouveau la base de données (avec le tri) en utilisant les identifiants d'une liste (contient). EF le gère très bien, même lorsque le jeu d’ID est assez volumineux. Oui, il s'agit de deux allers-retours, mais cela vous permet de toujours conserver votre tri dans la base de données, ce qui peut être important dans certaines situations, et vous empêche d'écrire une charge de logique dans la procédure stockée.

2
Paul Schirf

Je suis d'accord, utilisez le côté client. Mais il semble que ce ne soit pas la réponse que vous voulez entendre.

Donc, c'est parfait comme ça. Je ne sais pas pourquoi vous voudriez le changer, ou même demander "Y a-t-il une meilleure façon." Vraiment, ça devrait s'appeler "The Way". En outre, il semble bien fonctionner et répondre parfaitement aux besoins du projet et sera probablement suffisamment extensible pour les années à venir. Puisque vos bases de données ne sont pas taxées et que le tri est vraiment très facile il devrait le rester pendant des années.

Je ne voudrais pas transpirer.

2
D.S.

Lorsque vous paginez des résultats triés, le SQL dynamique est une bonne option. Si vous êtes paranoïaque à propos de l'injection SQL, vous pouvez utiliser les numéros de colonne au lieu du nom de la colonne. J'ai déjà fait cela avant d'utiliser des valeurs négatives pour décroissant. Quelque chose comme ça...

declare @o int;
set @o = -1;

declare @sql nvarchar(2000);
set @sql = N'select * from table order by ' + 
    cast(abs(@o) as varchar) + case when @o < 0 then ' desc' else ' asc' end + ';'

exec sp_executesql @sql

Ensuite, vous devez simplement vous assurer que le nombre est compris entre 1 et # des colonnes. Vous pouvez même développer ceci en une liste de numéros de colonnes et l’analyser dans un tableau d’entités à l’aide d’une fonction comme this . Ensuite, vous construirez l'ordre par clause comme si ...

declare @cols varchar(100);
set @cols = '1 -2 3 6';

declare @order_by varchar(200)

select @order_by = isnull(@order_by + ', ', '') + 
        cast(abs(number) as varchar) + 
        case when number < 0 then ' desc' else '' end
from dbo.iter_intlist_to_tbl(@cols) order by listpos

print @order_by

Un inconvénient est que vous devez vous rappeler l'ordre de chaque colonne du côté client. En particulier lorsque vous n’affichez pas toutes les colonnes ou si vous les affichez dans un ordre différent. Lorsque le client souhaite effectuer un tri, vous mappez les noms de colonne sur leur ordre et générez la liste des entiers. 

2
dotjoe

À un moment donné, ne vaut-il pas la peine de s’éloigner des procédures stockées et d’utiliser uniquement des requêtes paramétrées pour éviter ce type de piratage?

2
Hank Gay

Désolé, je suis en retard pour la fête, mais voici une autre option pour ceux qui veulent vraiment éviter le SQL dynamique, mais veulent la flexibilité qu'il offre:

Au lieu de générer dynamiquement le code SQL à la volée, écrivez du code afin de générer un proc unique pour chaque variante possible. Ensuite, vous pouvez écrire une méthode dans le code pour examiner les options de recherche et lui demander de choisir le proc approprié à appeler.

Si vous ne disposez que de quelques variantes, vous pouvez simplement créer les procs à la main. Mais si vous avez beaucoup de variations, alors au lieu de toutes les conserver, maintenez simplement votre générateur de proc pour qu'il soit recréé.

De plus, vous obtiendrez de meilleurs plans SQL pour de meilleures performances en procédant de la même manière.

0
BVernon

Pourquoi ne pas gérer le tri sur les éléments affichant les résultats - grilles, rapports, etc. plutôt que sur SQL?

MODIFIER:

Pour clarifier les choses depuis que cette réponse a été votée plus tôt, je vais élaborer un peu ...

Vous avez déclaré que vous connaissiez le tri côté client, mais que vous souhaitiez l'éviter. C'est votre appel, bien sûr.

Ce que je veux souligner, cependant, c’est qu’en le faisant côté client, vous pouvez extraire les données UNE FOIS et ensuite les utiliser comme vous le souhaitez - au lieu de faire plusieurs allers et retours vers le serveur à chaque fois. le genre est changé.

Votre serveur SQL n'est pas taxé pour le moment et c'est génial. Cela ne devrait pas être. Mais ce n'est pas encore surchargé que cela ne veut pas dire que ça va rester comme ça pour toujours.

Si vous utilisez l'un des éléments ASP.NET les plus récents pour l'affichage sur le Web, une grande partie de ces éléments est déjà intégrée.

Vaut-il la peine d’ajouter autant de code à chaque procédure stockée pour gérer le tri? Encore une fois, votre appel. 

Je ne suis pas celui qui sera finalement chargé de le soutenir. Mais réfléchissez à ce qui va être impliqué lorsque des colonnes sont ajoutées/supprimées dans les divers jeux de données utilisés par les procédures stockées (nécessitant des modifications des instructions CASE) ou lorsque, soudainement, au lieu de trier sur deux colonnes, l’utilisateur en décide trois - vous obligeant à mettre à jour maintenant chacune de vos procédures stockées utilisant cette méthode.

Pour moi, cela vaut la peine d’obtenir une solution côté client qui fonctionne et de l’appliquer à la poignée d’affichages d’affichage de données destinés à l’utilisateur et d’en finir. Si une nouvelle colonne est ajoutée, elle est déjà gérée. Si l'utilisateur souhaite effectuer un tri sur plusieurs colonnes, il peut en effectuer deux ou vingt.

0
Kevin Fairchild