Как избежать динамического SQL при использовании неопределенного количества параметров? - PullRequest
7 голосов
/ 23 августа 2009

У меня есть StackOverflow-подобная система тегов для базы данных, над которой я работаю. И я пишу хранимую процедуру, которая ищет результаты на основе неопределенного количества тегов в предложении WHERE. Для фильтрации результатов может быть от 0 до 10 тегов. Так, например, пользователь может искать элементы с тегами «яблоко», «апельсин» и «банан», и результат каждый должен включать все 3 тега. Мой запрос сделан еще более сложным, потому что я также имею дело с таблицей перекрестных ссылок для тегирования, но для целей этого вопроса я не буду вдаваться в подробности.

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

Какие методы вы использовали, чтобы избежать динамического SQL в сценарии такого типа?

По многочисленным просьбам вот запрос, с которым я работаю:

SELECT ft.[RANK], s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM shader s 
INNER JOIN FREETEXTTABLE(shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
WHERE EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'color')
AND EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'saturation')
ORDER BY ft.[RANK] DESC

Это функционально, но жестко запрограммировано. Вы увидите, что он настроен на поиск тегов 'color' и 'saturation'.

Ответы [ 8 ]

13 голосов
/ 23 августа 2009

Подробный обзор по этой и аналогичным проблемам см .: http://www.sommarskog.se/dyn-search-2005.html

Специфическая для вашего вопроса часть здесь: http://www.sommarskog.se/dyn-search-2005.html#AND_ISNOTNULL

Также учтите, что (прямое) динамическое решение не обязательно медленнее, чем (возможно, извилистое) статическое, поскольку планы запросов все еще могут кэшироваться: см. http://www.sommarskog.se/dyn-search-2005.html#dynsql

Таким образом, вам придется тщательно проверять / сравнивать свои параметры с реалистичными объемами данных, принимая во внимание реалистичные запросы (например, поиск с одним или двумя параметрами может быть более распространенным, чем поиск с десятью и т. Д.)


РЕДАКТИРОВАТЬ: Опрашивающий дал хорошую причину для оптимизации этого в комментариях, следовательно, немного убрав предупреждение о преждевременных действиях:

(стандартное;) слово предупреждения применимо, однако: Это очень похоже на преждевременную оптимизацию! - Вы уверены, что этот sproc будет вызываться так часто, что использование динамического SQL будет значительно медленнее (то есть по сравнению с другими вещами, происходящими в вашем приложении)?

3 голосов
/ 23 августа 2009

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

В настоящее время моя база данных заполнена примерно 200 шейдерами и 500 тегами. Я выполнил, как мне кажется, несколько реалистичный тест, где я выполнил 35 различных поисковых запросов для моего сохраненного процесса с различным количеством тегов, с поисковым запросом и без него. Я поместил все это в один оператор SQL, а затем сравнил результаты в ASP.NET. Он последовательно запускал эти 35 поисков менее чем за 200 миллисекунд. Если я уменьшу его до 5 запросов, то время сократится до 10 мс. Такое представление потрясающе. Помогает, что размер моей базы данных небольшой. Но я думаю, что также помогает то, что запрос хорошо использует индексы.

Одна вещь, которую я изменил в своем запросе, - это способ поиска тегов. Сейчас я ищу теги по их идентификатору, а не по имени. Делая это, я могу сойтись с выполнением на 1 соединение меньше, и у меня есть преимущество использования индекса для поиска. А потом я также добавил «дбо». перед именами таблиц, узнав, что SQL кэширует запросы для каждого пользователя.

На случай, если кому-то интересно, вот мой законченный сохраненный процесс:

ALTER PROCEDURE [dbo].[search] 
    @search_term    varchar(100) = NULL,
    @tag1           int = NULL,
    @tag2           int = NULL,
    @tag3           int = NULL,
    @tag4           int = NULL,
    @tag5           int = NULL,
    @tag6           int = NULL,
    @tag7           int = NULL,
    @tag8           int = NULL,
    @tag9           int = NULL,
    @tag10          int = NULL
AS
BEGIN
    SET NOCOUNT ON;

    IF LEN(@search_term) > 0
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            INNER JOIN FREETEXTTABLE(dbo.shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
            ORDER BY ft.[RANK] DESC
        END
    ELSE
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
        END
END

Несмотря на то, что я не исчерпал все варианты, это все равно было хорошим упражнением, потому что я доказал себе, что дизайн моей базы данных работает очень хорошо для этой задачи. И я также многому научился, разместив этот вопрос. Я знал, что exec () был плох, потому что он не кэширует план запроса. Но я не знал, что sp_executesql кеширует планы запросов, и это очень круто. Я также не знал об общих табличных выражениях. И ссылка, размещенная Хенриком Опелем, полна хороших советов для этого типа задач.

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

UPDATE:

Итак, у меня есть рабочий пример этой поисковой системы в Интернете по адресу http://www.silverlightxap.com/controls, если кому-то интересно увидеть это в действии.

1 голос
/ 23 августа 2009

Как избежать динамического SQL при использовании неопределенное количество параметров?

Вместо этого вы можете динамически генерировать соответствующие параметризованные (подготовленные) шаблоны SQL .

Создайте и подготовьте шаблон выписки, когда параметры представляются в первый раз, кэшируя подготовленные выписки для повторного использования, когда снова появляется такое же количество параметров.

Это может быть сделано в приложении или в достаточно сложной хранимой процедуре.

Я предпочитаю этот подход, скажем, процедуре, которая принимает не более 10 тегов и имеет простую логику, чтобы любой из них имел значение NULL.

Ответ GROUP BY Билла Карвина в этом вопросе, вероятно, является самым простым шаблоном для построения - вы просто объединяете заполнители для предиката IN и обновляете предложение COUNT. Другие решения, включающие объединения по тегам, потребуют увеличения псевдонимов таблиц (например, xref1, xref2 и т. Д.) По ходу работы.

1 голос
/ 23 августа 2009

Ваш запрос идеально подходит для использования выражения общей таблицы (CTE) из-за дублированного коррелированного подзапроса в предложениях EXISTS:

WITH attribute AS(
  SELECT tsx.shader_id,
         t.tag_name
    FROM TAG_SHADER_XREF tsx ON tsx.shader_id = s.shader_id
    JOIN TAG t ON t.tad_id = tsx.tag_id)
SELECT ft.[RANK], 
       s.shader_id, 
       s.page_name, 
       s.name, 
       s.description, 
       s.download_count, 
       s.rating, 
       s.price 
  FROM SHADER s 
  JOIN FREETEXTTABLE(SHADER, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
  JOIN attribute a1 ON a1.shader_id = s.shader_id AND a1.tag_name = 'color'
  JOIN attribute a2 ON a2.shader_id = s.shader_id AND a2.tag_name = 'saturation'
 ORDER BY ft.[RANK] DESC

Используя CTE, я также конвертировал EXISTS в JOIN.

Если говорить о вашем первоначальном вопросе об использовании динамического SQL - единственной альтернативой является проверка входящего параметра на наличие критериев экранирования перед его применением. IE:

WHERE (@param1 IS NULL OR a1.tag_name = @param1)

Если @ param1 содержит значение NULL, более поздняя часть SQL в скобках не выполняется. Я предпочитаю подход динамического SQL, так как в противном случае вы создаете JOINs / etc, которые могут не использоваться - это пустая трата ресурсов.

Какие проблемы с производительностью вы считаете существующими с динамическим SQL? Использование sp_executesql кэширует план запроса. Честно говоря, я нахожу странным, что план запроса не будет кэшироваться, если запрос будет проверен на синтаксис / etc (с использованием exec или sp_executesql) - проверка будет происходить до плана запроса, поэтому этот шаг впоследствии пропускается

1 голос
/ 23 августа 2009

Я видел два типа решения этой проблемы:

Во-первых, нужно присоединить таблицу shader к tags (при необходимости через внешнюю ссылку) один раз для каждого тега, который вы ищете. Результат внутреннего объединения включает только шейдеры, которые соответствуют всем тэгам.

SELECT s.*
FROM shader s
JOIN tag_shader_xref x1 ON (s.shader_id = x1.shader_id)
JOIN tag t1 ON (t1.tag_id = x1.tag_id AND t1.tag_name = 'color')
JOIN tag_shader_xref x2 ON (s.shader_id = x2.shader_id)
JOIN tag t2 ON (t2.tag_id = x2.tag_id AND t2.tag_name = 'saturation')
JOIN tag_shader_xref x3 ON (s.shader_id = x3.shader_id)
JOIN tag t3 ON (t3.tag_id = x3.tag_id AND t3.tag_name = 'transparency');

Второе решение состоит в том, чтобы присоединиться к этим тегам один раз, ограничив тегами три нужные вам, а затем GROUP BY shader_id, чтобы вы могли подсчитать совпадения. Счет будет равен трем, только если все теги были найдены (при условии уникальности в таблице внешних ссылок).

SELECT s.shader_id
FROM shader s
JOIN tag_shader_xref x ON (s.shader_id = x.shader_id)
JOIN tag t ON (t.tag_id = x.tag_id 
  AND t.tag_name IN ('color', 'saturation', 'transparency'))
GROUP BY s.shader_id
HAVING COUNT(DISTINCT t.tag_name) = 3;

Что вы должны использовать? Зависит от того, насколько хорошо ваш бренд базы данных оптимизирует тот или иной метод. Я обычно использую MySQL, который не так хорошо работает с GROUP BY, поэтому лучше использовать первый метод. В Microsoft SQL Server последнее решение может работать лучше.

0 голосов
/ 23 августа 2009

Я одобрил ответ Хенрика, но я могу придумать еще одну альтернативу: получить теги поиска во временной таблице или табличной переменной, а затем выполнить для них JOIN или использовать предложение IN с вложенным SELECT. Поскольку вам нужны результаты с всеми найденными тегами, вам нужно сначала подсчитать количество тегов запроса, а затем найти результаты, в которых количество совпавших тегов равно этому числу.

Как поместить значения в таблицу? Если теги передаются в вашу хранимую процедуру, и если вы используете SQL Server 2008, тогда вы можете использовать новую функцию табличных параметров и передавать табличную переменную непосредственно в вашу хранимую процедуру.

В противном случае, если вы получите теги в одной строке, вы можете использовать хранимую функцию, которая возвращает таблицу, например, функция SplitString, показанная здесь . Вы можете сделать что-то вроде:

... WHERE @SearchTagCount = (SELECT COUNT(tsx.shader_id) FROM tag_shader_xref tsx
INNER JOIN tag t ON tsx.tag_id = t.tag_id
WHERE tsx.shader_id = s.shader_id AND t.tag_name IN (SELECT * FROM dbo.SplitString(@SearchTags,',')))
0 голосов
/ 23 августа 2009

Это может быть не самый быстрый метод, но вы могли бы просто сгенерировать строку запроса для каждого тега и затем соединить их с помощью "INTERSECT"?

Edit: не видел тег sproc, поэтому я не знаю, возможно ли это.

0 голосов
/ 23 августа 2009

Объедините теги вместе с запятой, разделяющей их «яблоко», «оранжевый», а затем передайте их одному параметру, который использует предложение IN для вашей хранимой процедуры.

Конечно, если у вас есть значения (ключ) из таблицы поиска для этих тегов, я бы использовал их.

EDIT:

Так как вам нужны все теги в результате ....

К сожалению, я думаю, что независимо от того, что вы делаете, ИП будет в опасности от плана, восстанавливаемого.

Вы можете использовать необязательные параметры и использовать CASE и ISNULL для создания аргументов.

Я все еще думаю, что это означает, что ваш SP потерял большую часть своего кешированного совершенства, но я считаю, что это лучше, чем просто строка 'exec'

...