¿Cómo puedo usar parámetros opcionales en un procedimiento almacenado T-SQL?

185

Estoy creando un procedimiento almacenado para hacer una búsqueda en una tabla. Tengo muchos campos de búsqueda diferentes, todos los cuales son opcionales. ¿Hay alguna manera de crear un procedimiento almacenado que se encargue de esto? Digamos que tengo una tabla con cuatro campos: ID, Nombre, Apellido y Título. Podría hacer algo como esto:

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

Este tipo de trabajos. Sin embargo, ignora los registros donde FirstName, LastName o Title son NULL. Si el Título no se especifica en los parámetros de búsqueda, quiero incluir registros donde el Título sea NULO, lo mismo para FirstName y LastName. Sé que probablemente podría hacer esto con SQL dinámico, pero me gustaría evitarlo.

Corey Burnett
fuente
Echa un vistazo aquí: stackoverflow.com/questions/11396919/…
Mario Eis
2
Intente seguir la instrucción codewhere : ISNULL (FirstName, ') = ISNULL (@FirstName,' '): esto hará que cada NULL sea una cadena vacía y se puedan comparar a través de la ecuación. operador. Si desea obtener todo el título si el parámetro de entrada es nulo, intente algo como eso: codeFirstName = @FirstName O @FirstName IS NULL.
baHI

Respuestas:

257

Cambiar dinámicamente las búsquedas basadas en los parámetros dados es un tema complicado y hacerlo de una manera sobre otra, incluso con solo una ligera diferencia, puede tener implicaciones masivas de rendimiento. La clave es usar un índice, ignorar el código compacto, ignorar la preocupación de repetir el código, debe hacer un buen plan de ejecución de consultas (use un índice).

Lea esto y considere todos los métodos. Su mejor método dependerá de sus parámetros, sus datos, su esquema y su uso real:

Condiciones de búsqueda dinámica en T-SQL por Erland Sommarskog

La maldición y las bendiciones del SQL dinámico por Erland Sommarskog

Si tiene la versión adecuada de SQL Server 2008 (SQL 2008 SP1 CU5 (10.0.2746) y posterior), puede usar este pequeño truco para usar un índice:

Agregue OPTION (RECOMPILE)a su consulta, vea el artículo de Erland , y SQL Server resolverá ORdesde dentro (@LastName IS NULL OR LastName= @LastName)antes de que se cree el plan de consulta basado en los valores de tiempo de ejecución de las variables locales, y se puede usar un índice.

Esto funcionará para cualquier versión de SQL Server (devolver los resultados correctos), pero solo incluye la OPCIÓN (RECOMPILAR) si está en SQL 2008 SP1 CU5 (10.0.2746) y posterior. La OPCIÓN (RECOMPILAR) volverá a compilar su consulta, solo la versión enumerada la volverá a compilar según los valores actuales de tiempo de ejecución de las variables locales, lo que le brindará el mejor rendimiento. Si no está en esa versión de SQL Server 2008, simplemente deje esa línea apagada.

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.
fuente
15
Tenga cuidado con la precedencia AND / OR. Y tiene precedencia sobre OR, por lo que sin los corchetes adecuados, este ejemplo no producirá los resultados esperados ... Por lo tanto, debe leer: (@FirstName IS NULL OR (FirstName = @FirstName)) AND (@LastNameIS NULL OR (LastName = @LastName)) Y (@TitleIS NULL OR (Title = @Title))
Bliek
... (@FirstName IS NULL OR (FirstName = @FirstName) debe ser ... (FirstName = Coalesce (@ firstname, FirstName))
fcm
No olvides los paréntesis, de lo contrario no funcionará.
Pablo Carrasco Hernández
27

La respuesta de @KM es buena en la medida de lo posible, pero no puede seguir completamente uno de sus primeros consejos;

..., ignore el código compacto, ignore la preocupación de repetir el código, ...

Si desea lograr el mejor rendimiento, debe escribir una consulta a medida para cada combinación posible de criterios opcionales. Esto puede sonar extremo, y si tiene muchos criterios opcionales, podría serlo, pero el rendimiento a menudo es una compensación entre el esfuerzo y los resultados. En la práctica, puede haber un conjunto común de combinaciones de parámetros que pueden orientarse con consultas a medida, luego una consulta genérica (según las otras respuestas) para todas las demás combinaciones.

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

La ventaja de este enfoque es que, en los casos comunes manejados por consultas a medida, la consulta es tan eficiente como puede ser, no hay impacto por los criterios no suministrados. Además, los índices y otras mejoras de rendimiento pueden dirigirse a consultas específicas a medida en lugar de tratar de satisfacer todas las situaciones posibles.

Rhys Jones
fuente
Seguramente sería mejor escribir un procedimiento almacenado separado para cada caso. Entonces no se preocupe por la suplantación de identidad y la compilación.
Jodrell
55
No hace falta decir que este enfoque se convierte rápidamente en una pesadilla de mantenimiento.
Atario
3
@Atario La facilidad de mantenimiento versus rendimiento es una compensación común, esta respuesta está orientada al rendimiento.
Rhys Jones
26

Puedes hacer en el siguiente 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

Sin embargo, depender de los datos a veces es mejor crear consultas dinámicas y ejecutarlas.

Michael Pakhantsov
fuente
10

Cinco años tarde a la fiesta.

Se menciona en los enlaces proporcionados de la respuesta aceptada, pero creo que merece una respuesta explícita en SO, generando dinámicamente la consulta en función de los parámetros proporcionados. P.ej:

Preparar

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

Procedimiento

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'

Pros:

  • fácil de escribir y entender
  • flexibilidad: genere fácilmente la consulta para filtraciones más complicadas (por ejemplo, TOP dinámico)

Contras:

  • posibles problemas de rendimiento en función de los parámetros, índices y volumen de datos proporcionados

No es una respuesta directa, pero está relacionada con el problema, también conocido como panorama general.

Por lo general, estos procedimientos almacenados de filtrado no flotan, sino que se llaman desde alguna capa de servicio. Esto deja la opción de alejar la lógica de negocios (filtrado) de SQL a la capa de servicio.

Un ejemplo es usar LINQ2SQL para generar la consulta basada en los filtros proporcionados:

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

Pros:

  • consulta generada dinámicamente en función de los filtros proporcionados. No se necesitan husmear parámetros ni recompilar sugerencias
  • algo más fácil de escribir para aquellos en el mundo OOP
  • normalmente amigable con el rendimiento, ya que se emitirán consultas "simples" (aunque todavía se necesitan índices apropiados)

Contras:

  • Se pueden alcanzar las limitaciones de LINQ2QL y forzar una rebaja a LINQ2Objects o volver a la solución SQL pura según el caso
  • la escritura descuidada de LINQ podría generar consultas horribles (o muchas consultas, si se cargan las propiedades de navegación)
Alexei
fuente
1
Asegúrese de que TODAS sus cadenas intermedias sean N '' en lugar de '': se encontrará con problemas de truncamiento si su SQL supera los 8000 caracteres.
Alan Singfield
1
Además, es posible que deba poner una cláusula "CON EJECUTAR COMO PROPIETARIO" en el procedimiento almacenado, si ha denegado el permiso SELECT directo al usuario. Sin embargo, tenga mucho cuidado de evitar la inyección de SQL si usa esta cláusula.
Alan Singfield
8

Extiende tu WHEREcondición:

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

es decir, combinar diferentes casos con condiciones booleanas.

devio
fuente
-3

Esto también 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
fuente