web-dev-qa-db-fra.com

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

Je crée une procédure stockée pour effectuer une recherche dans une table. J'ai beaucoup de champs de recherche différents, qui sont tous facultatifs. Y at-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, Prénom, Nom et Titre. 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 où 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.

177
Corey Burnett

Le fait de modifier dynamiquement les recherches en fonction des paramètres donnés est un sujet compliqué et le faire d'une manière sur une autre, même avec très peu de différence, peut avoir des conséquences énormes sur les performances. La clé est d’utiliser un index, d’ignorer le code compact, d’ignorer les répétitions de code, vous devez établir un bon plan d’exécution de la requête (utilisez 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 dynamique dans T-SQL par Erland Sommarskog

La malédiction et les bénédictions de 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, voir l'article d'Erland , et SQL Server résoudra le OR à partir de (@LastName IS NULL OR LastName= @LastName) avant la création du plan de requête en fonction des valeurs d'exécution. des variables locales, et un index peut être utilisé.

Cela fonctionnera pour toutes les versions de SQL Server (renverra les résultats appropriés), mais n'inclura que l'option OPTION (RECOMPILE) si vous utilisez SQL 2008 SP1 CU5 (10.0.2746) ou une version ultérieure. L'OPTION (RECOMPILE) recompilera votre requête. Seule la version répertoriée le 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 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
251
KM.

La réponse de @KM est bonne dans la mesure du possible mais ne donne pas suite à l'un de ses premiers conseils.

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

Si vous souhaitez 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 optionnels, ce peut être le cas, mais la performance est souvent un compromis entre effort et résultats. En pratique, il peut exister un ensemble commun de combinaisons de paramètres pouvant être ciblé avec des requêtes personnalisées, 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 - les critères non pris en charge n'ont aucun impact. En outre, les index et autres améliorations des performances peuvent être ciblés sur des requêtes spécifiques sur mesure plutôt que d'essayer de satisfaire toutes les situations possibles.

27
Rhys Jones

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épendre des données, il vaut parfois mieux créer une requête dynamique et les exécuter.

25
Michael Pakhantsov

Cinq ans de retard à la fête.

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

Configuration

-- 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

Utilisation

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

Avantages:

  • facile à écrire et à comprendre
  • flexibilité - générez facilement la requête pour les 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ée au problème, alias la grande image

Généralement, ces procédures stockées de filtrage ne sont pas flottantes, mais sont appelées à partir d'une couche de service. Cela laisse l'option d'abandonner 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 en fonction des filtres fournis. Non recherche de paramètre ou recompiler astuces nécessaires
  • un peu plus facile à écrire pour ceux du monde OOP
  • généralement favorable aux performances, car des requêtes "simples" seront émises (des index appropriés sont néanmoins nécessaires)

Les inconvénients:

  • Les limitations de LINQ2QL peuvent être atteintes et obliger à rétrograder LINQ2Objects ou à revenir à une solution SQL pure, selon le cas.
  • une écriture négligente de LINQ peut générer des requêtes épouvantables (ou de nombreuses requêtes si les propriétés de navigation sont chargées)
8
Alexei

Étendez votre condition WHERE:

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, '') = '')

je. e. combinez différents cas avec des conditions booléennes.

8
devio