Как условно присоединиться к столу? - PullRequest
1 голос
/ 20 мая 2011

Я внедряю поисковый механизм на веб-сайте и наткнулся на его аспект SQL.

Пользователь может искать истории по любой комбинации этих фильтров: заголовок истории, тег истории или имя пользователя автора истории. Если фильтр не предусмотрен, просто верните все истории.

Мое непосредственное решение этой проблемы - хранимая процедура:

(
@TitleFilter varchar(50) = NULL
,@TagFilter varchar(30) = NULL
,@UserFilter varchar(30) = NULL
)

SELECT
    story.Title
    ,story.AddedDTS

FROM
    Stories story
    INNER JOIN FREETEXTTABLE(Stories, Title, @TitleFilter) ft
        ON ft.[key] = story.ID
    LEFT JOIN StoryTags st
        ON st.StoryID = story.ID
    LEFT JOIN Tags tag
        ON tag.ID = st.TagID
    LEFT JOIN StoryUser su
        ON su.StoryID = story.ID
    LEFT JOIN Users u
        ON u.ID = su.UserID

WHERE
    1=1
    AND (
            (@TagFilter IS NULL AND @UserFilter IS NULL)
            OR (@TagFilter IS NOT NULL AND tag.Name = @TagFilter)
            OR (@UserFilter IS NOT NULL AND u.Username = @UserFilter)
        )

Однако есть несколько проблем с этим, и мне еще предстоит найти лучший подход.

Прежде всего, в таблице Stories включен полнотекстовый поиск, поэтому я должен использовать механизм FREETEXTTABLE, который предписывает предикату не быть NULL, поэтому эта хранимая процедура не будет работать с @TitleFilter установлен на NULL.

Во-вторых, если я выполняю поиск только по названию, то объединение с таблицами StoryTags, Tags, StoryUsers и Users - это просто бесполезные накладные расходы.

Итак, вопрос к обоим пунктам, которые я указал: могу ли я условно опустить соединения, чтобы оптимизировать время выполнения?

Если к этому есть совершенно другой подход, вы можете поделиться им; Я выступаю за нестандартное мышление.

Ответы [ 3 ]

2 голосов
/ 20 мая 2011

Ничто не говорит о том, что ваши сравнения в ваших объединениях и где предложения должны включать столбцы таблицы.Попробуйте что-то вроде этого:

declare @useTable1 bit -- set to 0/1 to indicate whether it should be used.
declare @useTable2 bit -- set to 0/1 to indicate whether it should be used.
declare @useTable3 bit -- set to 0/1 to indicate whether it should be used.

select *
from      requiredTable   t
left join optionalTable_1 t1 on t1.requiredTableID = t.ID and @useTable1 = 1
left join optionalTable_2 t2 on t2.requiredTableID = t.ID and @useTable2 = 1
left join optionalTable_3 t3 on t3.requiredTableID = t.ID and @useTable3 = 1

Оптимизатор SQL Server, по крайней мере, достаточно умен, чтобы закорачивать вещи, основываясь на сравнении с инвариантом.

Работает как шарм.

1 голос
/ 20 мая 2011

Учитывая, что когда @TitleFilter равен null, он не работает с FREETEXTTABLE, я бы либо разбил его оператором if, например, так.

IF @TitleFilter is not null

    SELECT

        story.Title
        ,story.AddedDTS

    FROM
        Stories story
        INNER JOIN FREETEXTTABLE(Stories, Title, @TitleFilter) ft
         ON ft.[key] = story.ID
ELSE 

    SELECT
        story.Title
        ,story.AddedDTS

    FROM
        Stories story
        LEFT JOIN StoryTags st
        ON st.StoryID = story.ID
        LEFT JOIN Tags tag
        ON tag.ID = st.TagID
        LEFT JOIN StoryUser su
        ON su.StoryID = story.ID
        LEFT JOIN Users u
        ON u.ID = su.UserID
    WHERE

        (@TagFilter IS NULL AND @UserFilter IS NULL)
         OR (@TagFilter IS NOT NULL AND tag.Name = @TagFilter)
        OR (@UserFilter IS NOT NULL AND u.Username = @UserFilter)

Или, если это по какой-то причине вас оскорбляет, всегда есть Проклятие и благословения динамического SQL

0 голосов
/ 16 июля 2014

Насколько я вижу, есть 3 решения (по крайней мере):

    --Solution #1
DECLARE @TitleFilter varchar(50) = NULL
        ,@TagFilter varchar(30) = NULL
        ,@UserFilter varchar(30) = NULL

IF (@TitleFilter IS NOT NULL)
    SELECT story.Title
          ,story.AddedDTS
    FROM   Stories AS story
            INNER JOIN FREETEXTTABLE(Stories, Title, @TitleFilter) AS ft
                ON story.ID = ft.[key]
        LEFT OUTER JOIN StoryTags AS st 
            ON story.ID = st.StoryID AND (@TagFilter IS NOT NULL)
        LEFT OUTER JOIN Tags AS tag
            ON  st.TagID = tag.ID AND (@TagFilter IS NOT NULL)
        LEFT OUTER JOIN StoryUser AS su
            ON story.ID = su.StoryID AND (@UserFilter IS NOT NULL)
        LEFT OUTER JOIN Users AS u 
            ON su.UserID = u.ID AND (@UserFilter IS NOT NULL)
    WHERE     (@TagFilter IS NULL OR tag.Name = @TagFilter)
          AND (@UserFilter IS NULL OR u.Username = @UserFilter)
    OPTION (RECOMPILE);--Use it in SQL 2008 R2 or later
ELSE
    SELECT story.Title
          ,story.AddedDTS
    FROM   Stories AS story
        LEFT OUTER JOIN StoryTags AS st 
            ON story.ID = st.StoryID AND (@TagFilter IS NOT NULL)
        LEFT OUTER JOIN Tags AS tag
            ON  st.TagID = tag.ID AND (@TagFilter IS NOT NULL)
        LEFT OUTER JOIN StoryUser AS su
            ON story.ID = su.StoryID AND (@UserFilter IS NOT NULL)
        LEFT OUTER JOIN Users AS u 
            ON su.UserID = u.ID AND (@UserFilter IS NOT NULL)
    WHERE     (@TagFilter IS NULL OR tag.Name = @TagFilter)
          AND (@UserFilter IS NULL OR u.Username = @UserFilter)
    OPTION (RECOMPILE);--Use it in SQL 2008 R2 or later
GO


--Solution #2
DECLARE @TitleFilter varchar(50) = NULL
        ,@TagFilter varchar(30) = NULL
        ,@UserFilter varchar(30) = NULL

IF (@TitleFilter IS NOT NULL)
    SELECT story.Title
          ,story.AddedDTS
    FROM   Stories AS story
            INNER JOIN FREETEXTTABLE(Stories, Title, @TitleFilter) AS ft
                ON story.ID = ft.[key]
    WHERE     (@TagFilter IS NULL OR EXISTS(SELECT 1 FROM StoryTags AS st INNER JOIN Tags AS tag ON st.TagID = tag.ID WHERE tag.Name = @TagFilter))
          AND (@UserFilter IS NULL OR EXISTS(SELECT 1 FROM StoryUser AS su INNER JOIN Users AS u ON su.UserID = u.ID WHERE u.Username = @UserFilter))
ELSE
    SELECT story.Title
          ,story.AddedDTS
    FROM   Stories AS story
    WHERE     (@TagFilter IS NULL OR EXISTS(SELECT 1 FROM StoryTags AS st INNER JOIN Tags AS tag ON st.TagID = tag.ID WHERE tag.Name = @TagFilter))
          AND (@UserFilter IS NULL OR EXISTS(SELECT 1 FROM StoryUser AS su INNER JOIN Users AS u ON su.UserID = u.ID WHERE u.Username = @UserFilter))
  --Don't get confused by the execution plan. You will see StoryTags, Tags, StoryUser and Users tables with some persentage. But those tables will be used 
  --only if the corresponding filter will allow to do so (look at the Filter operator).
  --You can use OPTION (RECOMPILE) if you want to recompile the query every time it runs.
 GO

 --Solution #3
 DECLARE @TitleFilter varchar(50) = NULL
        ,@TagFilter varchar(30) = NULL
        ,@UserFilter varchar(30) = NULL

 DECLARE @SqlScript nvarchar(MAX), @ParamDefinition nvarchar(512);
 SET @SqlScript = '
    SELECT story.Title
          ,story.AddedDTS
    FROM   dbo.Stories AS story';
 IF (@TitleFilter IS NOT NULL)
     SET @SqlScript += '
                INNER JOIN FREETEXTTABLE(Stories, Title, @TitleFilter) AS ft
                    ON story.ID = ft.[key]';
 IF (@TagFilter IS NOT NULL)
     SET @SqlScript += '
        INNER JOIN dbo.StoryTags AS st 
            ON story.ID = st.StoryID
        INNER JOIN dbo.Tags AS tag
            ON st.TagID = tag.ID AND tag.Name = @TagFilter';

 IF (@UserFilter IS NOT NULL)
     SET @SqlScript += '
        INNER JOIN dbo.StoryUser AS su
            ON story.ID = su.StoryID
        INNER JOIN dbo.Users AS u 
            ON su.UserID = u.ID AND u.Username = @UserFilter';

 SET @ParamDefinition = '@TitleFilter varchar(50) 
                       ,@TagFilter varchar(30),
                       ,@UserFilter varchar(30)';

 EXEC sp_executesql @SqlScript, @ParamDefinition, @TitleFilter, @TagFilter, @UserFilter;
GO
...