Comment puis-je utiliser des paramètres facultatifs dans une procédure stockée T-SQL?

185

Je crée une procédure stockée pour effectuer une recherche dans une table. J'ai de nombreux champs de recherche différents, tous facultatifs. Existe-t-il un moyen de créer une procédure stockée qui gérera cela? Disons que j'ai une table avec quatre champs: ID, FirstName, LastName et Title. Je pourrais faire quelque chose comme ça:

CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
    BEGIN
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = ISNULL(@FirstName, FirstName) AND
            LastName = ISNULL(@LastName, LastName) AND
            Title = ISNULL(@Title, Title)
    END

Ce genre de travaux. Cependant, il ignore les enregistrements pour lesquels FirstName, LastName ou Title sont NULL. Si Title n'est pas spécifié dans les paramètres de recherche, je veux inclure les enregistrements où Title est NULL - idem pour FirstName et LastName. Je sais que je pourrais probablement le faire avec du SQL dynamique, mais j'aimerais éviter cela.

Corey Burnett
la source
Jetez un oeil ici: stackoverflow.com/questions/11396919/…
Mario Eis
2
Essayez de suivre l'instruction codewhere : ISNULL (FirstName, ') = ISNULL (@FirstName,' ') - cela fera de chaque NULL une chaîne vide et ceux-ci peuvent être comparés via eq. opérateur. Si vous voulez obtenir tous les titres si le paramètre d'entrée est nul, essayez quelque chose comme ça: codeFirstName = @FirstName OU @FirstName IS NULL.
baHI

Réponses:

257

La modification dynamique des recherches basées sur les paramètres donnés est un sujet compliqué et le faire d'une manière plutôt que d'une autre, même avec seulement une très légère différence, peut avoir des implications énormes sur les performances. La clé est d'utiliser un index, d'ignorer le code compact, d'ignorer le souci de la répétition du code, vous devez faire un bon plan d'exécution de requête (utiliser un index).

Lisez ceci et considérez toutes les méthodes. Votre meilleure méthode dépendra de vos paramètres, de vos données, de votre schéma et de votre utilisation réelle:

Conditions de recherche dynamiques dans T-SQL par Erland Sommarskog

La malédiction et les bénédictions du SQL dynamique par Erland Sommarskog

Si vous disposez de la version appropriée de SQL Server 2008 (SQL 2008 SP1 CU5 (10.0.2746) et versions ultérieures), vous pouvez utiliser cette petite astuce pour utiliser réellement un index:

Ajoutez OPTION (RECOMPILE)à votre requête, consultez l'article d'Erland , et SQL Server résoudra le ORde l'intérieur (@LastName IS NULL OR LastName= @LastName)avant que le plan de requête ne soit créé en fonction des valeurs d'exécution des variables locales, et un index peut être utilisé.

Cela fonctionnera pour n'importe quelle version de SQL Server (retourne des résultats corrects), mais n'inclut que OPTION (RECOMPILE) si vous utilisez SQL 2008 SP1 CU5 (10.0.2746) et versions ultérieures. L'OPTION (RECOMPILE) recompilera votre requête, seule la version répertoriée la recompilera en fonction des valeurs d'exécution actuelles des variables locales, ce qui vous donnera les meilleures performances. Si ce n'est pas sur cette version de SQL Server 2008, laissez simplement cette ligne désactivée.

CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
    BEGIN
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
                (@FirstName IS NULL OR (FirstName = @FirstName))
            AND (@LastName  IS NULL OR (LastName  = @LastName ))
            AND (@Title     IS NULL OR (Title     = @Title    ))
        OPTION (RECOMPILE) ---<<<<use if on for SQL 2008 SP1 CU5 (10.0.2746) and later
    END
KM.
la source
15
Soyez prudent avec la priorité AND / OR. AND a la priorité sur OR, donc sans les crochets appropriés, cet exemple ne produira pas les résultats attendus ... Il devrait donc se lire: (@FirstName IS NULL OR (FirstName = @FirstName)) AND (@LastNameIS NULL OR (LastName = @LastName)) AND (@TitleIS NULL OR (Title = @Title))
Bliek
... (@FirstName IS NULL OU (FirstName = @FirstName) doit être ... (FirstName = Coalesce (@ firstname, FirstName))
fcm
N'oubliez pas les parenthèses, sinon cela ne fonctionnera pas.
Pablo Carrasco Hernández
27

La réponse de @KM est bonne dans la mesure où elle disparaît, mais ne parvient pas à suivre pleinement l'un de ses premiers conseils;

..., ignorez le code compact, ignorez le souci de répéter le code, ...

Si vous cherchez à obtenir les meilleures performances, vous devez écrire une requête sur mesure pour chaque combinaison possible de critères facultatifs. Cela peut sembler extrême, et si vous avez beaucoup de critères facultatifs, cela peut l'être, mais les performances sont souvent un compromis entre l'effort et les résultats. En pratique, il peut y avoir un ensemble commun de combinaisons de paramètres qui peuvent être ciblées avec des requêtes sur mesure, puis une requête générique (comme pour les autres réponses) pour toutes les autres combinaisons.

CREATE PROCEDURE spDoSearch
    @FirstName varchar(25) = null,
    @LastName varchar(25) = null,
    @Title varchar(25) = null
AS
BEGIN

    IF (@FirstName IS NOT NULL AND @LastName IS NULL AND @Title IS NULL)
        -- Search by first name only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = @FirstName

    ELSE IF (@FirstName IS NULL AND @LastName IS NOT NULL AND @Title IS NULL)
        -- Search by last name only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            LastName = @LastName

    ELSE IF (@FirstName IS NULL AND @LastName IS NULL AND @Title IS NOT NULL)
        -- Search by title only
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            Title = @Title

    ELSE IF (@FirstName IS NOT NULL AND @LastName IS NOT NULL AND @Title IS NULL)
        -- Search by first and last name
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
            FirstName = @FirstName
            AND LastName = @LastName

    ELSE
        -- Search by any other combination
        SELECT ID, FirstName, LastName, Title
        FROM tblUsers
        WHERE
                (@FirstName IS NULL OR (FirstName = @FirstName))
            AND (@LastName  IS NULL OR (LastName  = @LastName ))
            AND (@Title     IS NULL OR (Title     = @Title    ))

END

L'avantage de cette approche est que, dans les cas courants traités par des requêtes sur mesure, la requête est aussi efficace que possible - il n'y a pas d'impact par les critères non fournis. En outre, les index et autres améliorations de performances peuvent être ciblés sur des requêtes spécifiques sur mesure plutôt que d'essayer de satisfaire toutes les situations possibles.

Rhys Jones
la source
Il serait sûrement préférable d'écrire une procédure stockée distincte pour chaque cas. Alors ne vous inquiétez pas de l'usurpation d'identité et de la recompilation.
Jodrell
5
Il va sans dire que cette approche devient rapidement un cauchemar de maintenance.
Atario
3
@Antario La facilité d'entretien par rapport aux performances est un compromis courant, cette réponse est orientée vers la performance.
Rhys Jones
26

Vous pouvez faire dans le cas suivant,

CREATE PROCEDURE spDoSearch
   @FirstName varchar(25) = null,
   @LastName varchar(25) = null,
   @Title varchar(25) = null
AS
  BEGIN
      SELECT ID, FirstName, LastName, Title
      FROM tblUsers
      WHERE
        (@FirstName IS NULL OR FirstName = @FirstName) AND
        (@LastNameName IS NULL OR LastName = @LastName) AND
        (@Title IS NULL OR Title = @Title)
END

cependant dépendent des données parfois mieux créer des requêtes dynamiques et les exécuter.

Michael Pakhantsov
la source
10

Cinq ans de retard à la fête.

Il est mentionné dans les liens fournis de la réponse acceptée, mais je pense qu'il mérite une réponse explicite sur SO - en construisant dynamiquement la requête en fonction des paramètres fournis. Par exemple:

Installer

-- drop table Person
create table Person
(
    PersonId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Person PRIMARY KEY,
    FirstName NVARCHAR(64) NOT NULL,
    LastName NVARCHAR(64) NOT NULL,
    Title NVARCHAR(64) NULL
)
GO

INSERT INTO Person (FirstName, LastName, Title)
VALUES ('Dick', 'Ormsby', 'Mr'), ('Serena', 'Kroeger', 'Ms'), 
    ('Marina', 'Losoya', 'Mrs'), ('Shakita', 'Grate', 'Ms'), 
    ('Bethann', 'Zellner', 'Ms'), ('Dexter', 'Shaw', 'Mr'),
    ('Zona', 'Halligan', 'Ms'), ('Fiona', 'Cassity', 'Ms'),
    ('Sherron', 'Janowski', 'Ms'), ('Melinda', 'Cormier', 'Ms')
GO

Procédure

ALTER PROCEDURE spDoSearch
    @FirstName varchar(64) = null,
    @LastName varchar(64) = null,
    @Title varchar(64) = null,
    @TopCount INT = 100
AS
BEGIN
    DECLARE @SQL NVARCHAR(4000) = '
        SELECT TOP ' + CAST(@TopCount AS VARCHAR) + ' *
        FROM Person
        WHERE 1 = 1'

    PRINT @SQL

    IF (@FirstName IS NOT NULL) SET @SQL = @SQL + ' AND FirstName = @FirstName'
    IF (@LastName IS NOT NULL) SET @SQL = @SQL + ' AND FirstName = @LastName'
    IF (@Title IS NOT NULL) SET @SQL = @SQL + ' AND Title = @Title'

    EXEC sp_executesql @SQL, N'@TopCount INT, @FirstName varchar(25), @LastName varchar(25), @Title varchar(64)', 
         @TopCount, @FirstName, @LastName, @Title
END
GO

Usage

exec spDoSearch @TopCount = 3
exec spDoSearch @FirstName = 'Dick'

Avantages:

  • facile à écrire et à comprendre
  • flexibilité - générer facilement la requête pour des filtrages plus délicats (par exemple, TOP dynamique)

Les inconvénients:

  • problèmes de performances possibles en fonction des paramètres fournis, des index et du volume de données

Pas de réponse directe, mais lié au problème, c'est-à-dire la vue d'ensemble

Habituellement, ces procédures stockées de filtrage ne flottent pas, mais sont appelées à partir d'une couche de service. Cela laisse la possibilité de déplacer la logique métier (filtrage) de SQL vers la couche de service.

Un exemple utilise LINQ2SQL pour générer la requête en fonction des filtres fournis:

    public IList<SomeServiceModel> GetServiceModels(CustomFilter filters)
    {
        var query = DataAccess.SomeRepository.AllNoTracking;

        // partial and insensitive search 
        if (!string.IsNullOrWhiteSpace(filters.SomeName))
            query = query.Where(item => item.SomeName.IndexOf(filters.SomeName, StringComparison.OrdinalIgnoreCase) != -1);
        // filter by multiple selection
        if ((filters.CreatedByList?.Count ?? 0) > 0)
            query = query.Where(item => filters.CreatedByList.Contains(item.CreatedById));
        if (filters.EnabledOnly)
            query = query.Where(item => item.IsEnabled);

        var modelList = query.ToList();
        var serviceModelList = MappingService.MapEx<SomeDataModel, SomeServiceModel>(modelList);
        return serviceModelList;
    }

Avantages:

  • requête générée dynamiquement basée sur les filtres fournis. Aucun indicateur de reniflage de paramètre ou de recompilation nécessaire
  • un peu plus facile à écrire pour ceux du monde OOP
  • généralement optimisé pour les performances, car des requêtes "simples" seront émises (les index appropriés sont néanmoins nécessaires)

Les inconvénients:

  • Les limitations LINQ2QL peuvent être atteintes et forcer une rétrogradation vers LINQ2Objects ou revenir à une solution SQL pure selon le cas
  • une écriture imprudente de LINQ peut générer des requêtes terribles (ou de nombreuses requêtes, si les propriétés de navigation sont chargées)
Alexei
la source
1
Assurez-vous que TOUTES vos chaînes intermédiaires sont N '' plutôt que '' - vous rencontrerez des problèmes de troncature si votre SQL dépasse 8000 caractères.
Alan Singfield
1
Vous devrez peut-être également placer une clause «WITH EXECUTE AS OWNER» sur la procédure stockée si vous avez refusé l'autorisation SELECT directe à l'utilisateur. Attention toutefois à éviter l'injection SQL si vous utilisez cette clause.
Alan Singfield
8

Prolongez votre WHEREcondition:

WHERE
    (FirstName = ISNULL(@FirstName, FirstName)
    OR COALESCE(@FirstName, FirstName, '') = '')
AND (LastName = ISNULL(@LastName, LastName)
    OR COALESCE(@LastName, LastName, '') = '')
AND (Title = ISNULL(@Title, Title)
    OR COALESCE(@Title, Title, '') = '')

c'est-à-dire combiner différents cas avec des conditions booléennes.

devio
la source
-3

Cela fonctionne également:

    ...
    WHERE
        (FirstName IS NULL OR FirstName = ISNULL(@FirstName, FirstName)) AND
        (LastName IS NULL OR LastName = ISNULL(@LastName, LastName)) AND
        (Title IS NULL OR Title = ISNULL(@Title, Title))
v2h
la source