Como posso usar parâmetros opcionais em um procedimento armazenado T-SQL?

185

Estou criando um procedimento armazenado para fazer uma pesquisa em uma tabela. Eu tenho muitos campos de pesquisa diferentes, todos opcionais. Existe uma maneira de criar um procedimento armazenado que irá lidar com isso? Digamos que eu tenha uma tabela com quatro campos: ID, Nome, Sobrenome e Título. Eu poderia fazer algo assim:

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

Esse tipo de trabalho. No entanto, ignora registros onde Nome, Sobrenome ou Título são NULL. Se o título não for especificado nos parâmetros de pesquisa, quero incluir registros em que o título é NULL - o mesmo para nome e sobrenome. Eu sei que provavelmente poderia fazer isso com SQL dinâmico, mas gostaria de evitar isso.

Corey Burnett
fonte
Dê uma olhada aqui: stackoverflow.com/questions/11396919/…
Mario Eis
2
Tente seguir a instrução where: codeISNULL (Nome, ') = ISNULL (@FirstName,' ') - isso fará com que cada NULL seja uma string vazia e esses possam ser comparados via eq. operador. Se você deseja obter todo o título se o parâmetro de entrada for nulo, tente algo assim: codeNome = @FirstName OU @FirstName IS NULL.
precisa saber é

Respostas:

257

Alterar dinamicamente as pesquisas com base nos parâmetros fornecidos é um assunto complicado, e fazê-lo de uma maneira sobre a outra, mesmo com uma diferença muito pequena, pode ter implicações enormes no desempenho. A chave é usar um índice, ignorar o código compacto, ignorar a preocupação com a repetição de código, você deve fazer um bom plano de execução de consulta (use um índice).

Leia isso e considere todos os métodos. Seu melhor método dependerá de seus parâmetros, dados, esquema e uso real:

Condições de pesquisa dinâmica em T-SQL por Erland Sommarskog

A maldição e as bênçãos do SQL dinâmico por Erland Sommarskog

Se você possui a versão apropriada do SQL Server 2008 (SQL 2008 SP1 CU5 (10.0.2746) e posterior), poderá usar este pequeno truque para realmente usar um índice:

Adicione OPTION (RECOMPILE)à sua consulta, consulte o artigo de Erland , e o SQL Server resolverá isso ORde dentro (@LastName IS NULL OR LastName= @LastName)antes que o plano de consulta seja criado com base nos valores de tempo de execução das variáveis ​​locais e um índice possa ser usado.

Isso funcionará para qualquer versão do SQL Server (retorne resultados adequados), mas inclua apenas OPTION (RECOMPILE) se você estiver no SQL 2008 SP1 CU5 (10.0.2746) e posterior. A OPTION (RECOMPILE) recompilará sua consulta, apenas a versão listada a recompilará com base nos valores atuais do tempo de execução das variáveis ​​locais, o que fornecerá o melhor desempenho. Se não estiver nessa versão do SQL Server 2008, deixe essa linha desativada.

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.
fonte
15
Tenha cuidado com a precedência AND / OR. AND tem precedência sobre OR, portanto, sem os colchetes adequados, este exemplo não produzirá os resultados esperados ... Portanto, ele deve ler: (@FirstName É NULL OR ((FirstName = @FirstName)) AND (@LastNameIS NULL OR (LastName = @LastName)) AND (@TitleIS NULL OR (Title = @Title))
Bliek
... (@FirstName é nulo ou (nome = @FirstName) deve ser ... (nome = Coalesce (@ firstname, Nome))
FCM
Não esqueça os parênteses, caso contrário não funcionará.
Pablo Carrasco Hernández
27

A resposta do @KM é boa, mas não segue completamente um de seus primeiros conselhos;

..., ignore o código compacto, ignore a preocupação com a repetição de código, ...

Se você deseja obter o melhor desempenho, deve escrever uma consulta sob medida para cada combinação possível de critérios opcionais. Isso pode parecer extremo, e se você tiver muitos critérios opcionais, poderá ser, mas o desempenho geralmente é uma troca entre esforço e resultados. Na prática, pode haver um conjunto comum de combinações de parâmetros que pode ser direcionado com consultas personalizadas e, em seguida, uma consulta genérica (conforme as outras respostas) para todas as outras combinações.

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

A vantagem dessa abordagem é que, nos casos mais comuns tratados por consultas personalizadas, a consulta é a mais eficiente possível - não há impacto pelos critérios não fornecidos. Além disso, índices e outros aprimoramentos de desempenho podem ser direcionados para consultas específicas, em vez de tentar satisfazer todas as situações possíveis.

Rhys Jones
fonte
Certamente, seria melhor escrever um procedimento armazenado separado para cada caso. Então não se preocupe com falsificação e recompilação.
Jodrell #
5
Não é preciso dizer que essa abordagem rapidamente se torna um pesadelo de manutenção.
Atario 24/05
3
@ Ontario: Facilidade de manutenção versus desempenho é uma troca comum, essa resposta é voltada para o desempenho.
Rhys Jones
26

Você pode fazer no seguinte 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

no entanto, dependendo dos dados, às vezes é melhor criar uma consulta dinâmica e executá-las.

Michael Pakhantsov
fonte
10

Cinco anos atrasado para a festa.

É mencionado nos links fornecidos da resposta aceita, mas acho que merece uma resposta explícita no SO - construindo dinamicamente a consulta com base nos parâmetros fornecidos. Por exemplo:

Configuração

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

Procedimento

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'

Prós:

  • fácil de escrever e entender
  • flexibilidade - gere facilmente a consulta para filtros mais complicados (por exemplo, TOP dinâmico)

Contras:

  • possíveis problemas de desempenho, dependendo dos parâmetros, índices e volume de dados fornecidos

Resposta não direta, mas relacionada ao problema, também conhecido como quadro geral

Geralmente, esses procedimentos armazenados de filtragem não flutuam, mas estão sendo chamados de alguma camada de serviço. Isso deixa a opção de afastar a lógica de negócios (filtragem) do SQL para a camada de serviço.

Um exemplo é o uso do LINQ2SQL para gerar a consulta com base nos filtros fornecidos:

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

Prós:

  • consulta gerada dinamicamente com base nos filtros fornecidos. Nenhum parâmetro sniffing ou recompile necessárias dicas de
  • um pouco mais fácil de escrever para aqueles no mundo OOP
  • normalmente com desempenho otimizado, pois consultas "simples" serão emitidas (ainda são necessários índices apropriados)

Contras:

  • As limitações do LINQ2QL podem ser alcançadas e forçar um downgrade para o LINQ2Objects ou retornar à solução SQL pura, dependendo do caso
  • a escrita descuidada do LINQ pode gerar consultas terríveis (ou muitas consultas, se as propriedades de navegação forem carregadas)
Alexei
fonte
1
Verifique se TODAS as seqüências intermediárias são N '' em vez de '' - você terá problemas de truncamento se o SQL exceder 8000 caracteres.
Alan Singfield 11/03
1
Além disso, pode ser necessário colocar uma cláusula "WITH EXECUTE AS OWNER" no procedimento armazenado, se você tiver negado a permissão direta SELECT para o usuário. Tenha muito cuidado para evitar a injeção de SQL se você usar esta cláusula.
Alan Singfield 11/03
8

Estenda sua WHEREcondição:

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

ou seja, combine casos diferentes com condições booleanas.

devio
fonte
-3

Isso também funciona:

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