Как я могу использовать необязательные параметры в хранимой процедуре T-SQL?

185

Я создаю хранимую процедуру для поиска в таблице. У меня есть много разных полей поиска, все из которых являются необязательными. Есть ли способ создать хранимую процедуру, которая будет обрабатывать это? Допустим, у меня есть таблица с четырьмя полями: ID, FirstName, LastName и Title. Я мог бы сделать что-то вроде этого:

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

Такого рода работы. Однако он игнорирует записи, в которых FirstName, LastName или Title имеют значение NULL. Если заголовок не указан в параметрах поиска, я хочу включить записи, где заголовок равен NULL - то же самое для FirstName и LastName. Я знаю, что мог бы сделать это с помощью динамического SQL, но я бы хотел этого избежать.

Кори Бернетт
источник
Посмотрите здесь: stackoverflow.com/questions/11396919/…
Марио Эйс
2
Попробуйте codeвыполнить инструкцию where : ISNULL (FirstName, ') = ISNULL (@FirstName,' ') - это сделает каждый NULL пустой строкой, и их можно сравнить через eq. оператор. Если вы хотите получить все заголовки, если входной параметр равен нулю, попробуйте что-то вроде этого: codeFirstName = @FirstName ИЛИ @FirstName IS NULL.
baHI

Ответы:

257

Динамическое изменение поисков на основе заданных параметров - сложная задача, и выполнение одного способа по другому, даже с очень небольшим отличием, может иметь серьезные последствия для производительности. Ключ заключается в том, чтобы использовать индекс, игнорировать компактный код, игнорировать беспокойство по поводу повторения кода, вы должны составить хороший план выполнения запроса (используйте индекс).

Прочитайте это и рассмотрите все методы. Ваш лучший метод будет зависеть от ваших параметров, ваших данных, вашей схемы и вашего фактического использования:

Условия динамического поиска в T-SQL by Эрланд Соммарског

Проклятие и благословения динамического SQL Эрланда Соммарского

Если у вас есть правильная версия SQL Server 2008 (SQL 2008 с пакетом обновления 1 (SP1) CU5 (10.0.2746) и выше), вы можете использовать этот маленький трюк для фактического использования индекса:

Добавьте OPTION (RECOMPILE)к вашему запросу, см. Статью Эрланда , и SQL Server разрешит его ORизнутри (@LastName IS NULL OR LastName= @LastName)до того, как план запроса будет создан на основе значений времени выполнения локальных переменных, и индекс может быть использован.

Это будет работать для любой версии SQL Server (возвращать правильные результаты), но включайте только OPTION (RECOMPILE), если вы используете SQL 2008 SP1 CU5 (10.0.2746) и выше. ОПЦИЯ (RECOMPILE) перекомпилирует ваш запрос, только перечисленная версия перекомпилирует его на основе текущих значений времени выполнения локальных переменных, что даст вам наилучшую производительность. Если не в этой версии SQL Server 2008, просто оставьте эту строку отключенной.

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.
источник
15
Будьте осторожны с приоритетом И / ИЛИ. И имеет приоритет над ИЛИ, поэтому без правильных скобок этот пример не даст ожидаемых результатов ... Поэтому он должен прочитать: (@FirstName IS NULL ИЛИ (FirstName = @FirstName)) И (@LastNameIS NULL ИЛИ (LastName = @LastName)) И (@TitleIS NULL ИЛИ (Title = @Title))
Bliek
... (@FirstName IS NULL ИЛИ (FirstName = @FirstName) должно быть ... (FirstName = Coalesce (@ firstname, FirstName))
fcm
Не забывайте скобки, иначе это не сработает.
Пабло Карраско Эрнандес
27

Ответ @KM хорош, но он не в состоянии полностью выполнить один из его ранних советов;

..., игнорировать компактный код, не беспокоиться о повторении кода, ...

Если вы хотите добиться максимальной производительности, вам следует написать индивидуальный запрос для каждой возможной комбинации необязательных критериев. Это может показаться экстремальным, и если у вас есть много дополнительных критериев, то это может быть, но производительность часто является компромиссом между усилием и результатами. На практике может существовать общий набор комбинаций параметров, которые могут быть нацелены на заказные запросы, затем общий запрос (согласно другим ответам) для всех других комбинаций.

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

Преимущество этого подхода состоит в том, что в общих случаях, обрабатываемых заказными запросами, запрос настолько эффективен, насколько это возможно - это не влияет на непредоставленные критерии. Кроме того, индексы и другие улучшения производительности могут быть нацелены на конкретные заказные запросы, а не пытаться удовлетворить все возможные ситуации.

Рис Джонс
источник
Конечно, было бы лучше написать отдельную хранимую процедуру для каждого случая. Тогда не беспокойтесь о подмене и перекомпиляции.
Джодрелл
5
Само собой разумеется, что этот подход быстро становится кошмаром обслуживания.
Atario
3
@Atario Простота обслуживания по сравнению с производительностью является распространенным компромиссом, этот ответ ориентирован на производительность.
Рис Джонс
26

Вы можете сделать в следующем случае,

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

Однако в зависимости от данных иногда лучше создавать динамические запросы и выполнять их.

Михаил Паханцов
источник
10

Пять лет опоздал на вечеринку.

Это упомянуто в предоставленных ссылках принятого ответа, но я думаю, что оно заслуживает явного ответа на SO - динамическое построение запроса на основе предоставленных параметров. Например:

Настроить

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

Процедура

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

использование

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

Плюсы:

  • легко написать и понять
  • гибкость - легко сгенерировать запрос для хитрой фильтрации (например, динамический TOP)

Минусы:

  • возможные проблемы с производительностью в зависимости от предоставленных параметров, индексов и объема данных

Не прямой ответ, но связанный с проблемой, иначе общая картина

Обычно эти фильтрующие хранимые процедуры не распространяются, а вызываются из некоторого сервисного уровня. Это оставляет возможность перенести бизнес-логику (фильтрацию) с SQL на уровень обслуживания.

Одним из примеров является использование LINQ2SQL для генерации запроса на основе предоставленных фильтров:

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

Плюсы:

  • динамически генерируемый запрос на основе предоставленных фильтров. Нет необходимости анализировать параметры или перекомпилировать подсказки
  • несколько легче написать для тех, кто в мире ООП
  • как правило, дружественные к производительности, так как будут выдаваться «простые» запросы (все же необходимы соответствующие индексы)

Минусы:

  • Могут быть достигнуты ограничения LINQ2QL, что приведет к понижению до объектов LINQ2Objects или возврату к чисто SQL-решению в зависимости от ситуации.
  • неосторожное написание LINQ может привести к ужасным запросам (или множеству запросов, если загружены свойства навигации)
Алексей
источник
1
Убедитесь, что ВСЕ ваши промежуточные строки имеют N '', а не '' - вы столкнетесь с проблемами усечения, если ваш SQL превышает 8000 символов.
Алан Сингфилд
1
Кроме того, вам может потребоваться поместить в хранимую процедуру предложение «WITH EXECUTE AS OWNER», если вы запретили прямое разрешение SELECT для пользователя. Будьте очень осторожны, избегая SQL-инъекций, если используете это предложение.
Алан Сингфилд
8

Продлите ваше 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, '') = '')

т.е. объединить разные случаи с логическими условиями.

devio
источник
-3

Это также работает:

    ...
    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
источник