Come posso usare parametri opzionali in una procedura memorizzata T-SQL?


185

Sto creando una procedura memorizzata per effettuare una ricerca attraverso una tabella. Ho molti campi di ricerca diversi, tutti facoltativi. C'è un modo per creare una procedura memorizzata che gestirà questo? Diciamo che ho una tabella con quattro campi: ID, FirstName, LastName e Title. Potrei fare qualcosa del genere:

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

Questo tipo di opere. Tuttavia ignora i record in cui FirstName, LastName o Title sono NULL. Se il titolo non è specificato nei parametri di ricerca, voglio includere i record in cui il titolo è NULL, lo stesso per FirstName e LastName. So che probabilmente potrei farlo con SQL dinamico ma vorrei evitarlo.



2
Prova a seguire la seguente istruzione: codeISNULL (FirstName, ') = ISNULL (@FirstName,' ') - questo renderà ogni NULL in una stringa vuota e questi potranno essere confrontati tramite eq. operatore. Se si desidera ottenere tutto il titolo se il parametro di input è null, provare qualcosa del genere: codeFirstName = @FirstName OR @FirstName IS NULL.
baHI,

Risposte:


257

Modificare dinamicamente le ricerche in base ai parametri indicati è un argomento complicato e farlo in un modo piuttosto che in un altro, anche se con una leggera differenza, può avere enormi implicazioni in termini di prestazioni. La chiave è utilizzare un indice, ignorare il codice compatto, ignorare la preoccupazione di ripetere il codice, è necessario creare un buon piano di esecuzione della query (utilizzare un indice).

Leggi questo e considera tutti i metodi. Il tuo metodo migliore dipenderà dai tuoi parametri, dai tuoi dati, dal tuo schema e dal tuo effettivo utilizzo:

Condizioni di ricerca dinamica in T-SQL di Erland Sommarskog

The Curse and Blessings of Dynamic SQL di Erland Sommarskog

Se si dispone della versione corretta di SQL Server 2008 (SQL 2008 SP1 CU5 (10.0.2746) e successive), è possibile utilizzare questo piccolo trucco per utilizzare effettivamente un indice:

Aggiungi OPTION (RECOMPILE)alla tua query, vedi l'articolo di Erland e SQL Server risolverà l' ORinterno (@LastName IS NULL OR LastName= @LastName)prima che il piano di query venga creato in base ai valori di runtime delle variabili locali e sia possibile utilizzare un indice.

Funzionerà con qualsiasi versione di SQL Server (restituisce risultati corretti), ma include solo OPTION (RECOMPILE) se si utilizza SQL 2008 SP1 CU5 (10.0.2746) e versioni successive. OPTION (RECOMPILE) ricompilerà la tua query, solo la versione elencata la ricompilerà in base ai valori correnti di runtime delle variabili locali, che ti daranno le migliori prestazioni. Se non su quella versione di SQL Server 2008, lascia quella linea disattivata.

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

15
Fai attenzione con la precedenza AND / OR. AND ha la precedenza su OR, quindi senza le parentesi appropriate questo esempio non produrrà i risultati previsti ... Quindi dovrebbe leggere: (@FirstName IS NULL OR (FirstName = @FirstName)) AND (@LastNameIS NULL OR (LastName = @LastName)) AND (@TitleIS NULL OR (Title = @Title))
Bliek,

... (@FirstName IS NULL OR (FirstName = @FirstName) dovrebbe essere ... (FirstName = Coalesce (@ firstname, FirstName))
fcm

Non dimenticare le parentesi, altrimenti non funzionerà.
Pablo Carrasco Hernández,

27

La risposta di @KM è buona, ma non riesce a dare seguito a uno dei suoi primi consigli;

..., ignora il codice compatto, ignora la preoccupazione di ripetere il codice, ...

Se stai cercando di ottenere le migliori prestazioni, dovresti scrivere una query su misura per ogni possibile combinazione di criteri opzionali. Questo potrebbe sembrare estremo, e se hai molti criteri opzionali potrebbe esserlo, ma le prestazioni sono spesso un compromesso tra sforzo e risultati. In pratica, potrebbe esserci un insieme comune di combinazioni di parametri che possono essere targetizzate con query su misura, quindi una query generica (come per le altre risposte) per tutte le altre combinazioni.

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

Il vantaggio di questo approccio è che, nei casi comuni gestiti da query su misura, la query è quanto più efficiente possibile - non ha alcun impatto sui criteri non forniti. Inoltre, gli indici e altri miglioramenti delle prestazioni possono essere mirati a specifiche richieste su misura piuttosto che cercare di soddisfare tutte le possibili situazioni.


Sicuramente sarebbe meglio scrivere una procedura memorizzata separata per ciascun caso. Quindi non preoccuparti di spoofing e ricompilazione.
Jodrell,

5
Va da sé che questo approccio diventa rapidamente un incubo per la manutenzione.
Atario,

3
@ Ontario La facilità di manutenzione rispetto alle prestazioni è un compromesso comune, questa risposta è orientata verso le prestazioni.
Rhys Jones,

26

Puoi fare nel seguente caso,

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

tuttavia dipende dai dati a volte meglio creare query dinamiche ed eseguirle.


10

Cinque anni di ritardo alla festa.

È menzionato nei collegamenti forniti della risposta accettata, ma penso che meriti una risposta esplicita su SO - costruendo dinamicamente la query in base ai parametri forniti. Per esempio:

Impostare

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

Procedura

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

uso

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

Professionisti:

  • facile da scrivere e capire
  • flessibilità: genera facilmente la query per filtri più complicati (ad es. TOP dinamico)

Contro:

  • possibili problemi di prestazioni a seconda dei parametri, degli indici e del volume di dati forniti

Risposta non diretta, ma relativa al problema noto anche come quadro generale

Di solito, queste procedure di filtraggio memorizzate non fluttuano, ma vengono chiamate da un livello di servizio. Ciò lascia l'opzione di spostare la logica aziendale (filtro) da SQL al livello di servizio.

Un esempio sta usando LINQ2SQL per generare la query in base ai filtri forniti:

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

Professionisti:

  • query generata dinamicamente in base ai filtri forniti. Non sono necessari suggerimenti di sniffing o ricompilazione dei parametri
  • un po 'più facile da scrivere per quelli nel mondo OOP
  • in genere ottimizzato per le prestazioni, poiché verranno inviate query "semplici" (sono comunque necessari indici appropriati)

Contro:

  • Limitazioni di LINQ2QL possono essere raggiunte e forzare un downgrade a LINQ2Objects o tornare alla soluzione SQL pura a seconda del caso
  • la scrittura incurante di LINQ potrebbe generare query terribili (o molte query, se le proprietà di navigazione sono caricate)

1
Assicurati che TUTTE le tue stringhe intermedie siano N '' anziché '' - incontrerai problemi di troncamento se il tuo SQL supera 8000 caratteri.
Alan Singfield l'

1
Inoltre, potrebbe essere necessario inserire una clausola "WITH EXECUTE AS OWNER" sulla procedura memorizzata, se è stata negata l'autorizzazione SELECT diretta all'utente. Fai molta attenzione a evitare l'iniezione di SQL se usi questa clausola.
Alan Singfield l'

8

Estendi le tue WHEREcondizioni:

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

cioè combinare diversi casi con condizioni booleane.


-3

Questo funziona anche:

    ...
    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))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.