如何有条件地加入一张桌子?

时间:2011-05-19 20:26:20

标签: sql stored-procedures join full-text-search

我正在网站中实现搜索机制,并偶然发现它的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时不起作用。

其次,如果我只按标题搜索,那么加入StoryTagsTagsStoryUsersUsers表只是一个无用的开销。

所以问题是我说的两点:我可以有条件地省略连接以优化执行时间吗?

如果对此采取完全不同的方法,欢迎您分享;我主张开箱即用。

3 个答案:

答案 0 :(得分:2)

没有任何内容表明您在联接中的比较以及where子句必须实际涉及表列。尝试这样的事情:

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 :(得分:1)

鉴于当@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)

或者,如果由于某种原因冒犯了你,那总是The Curse and Blessings of Dynamic SQL

答案 2 :(得分:0)

据我所知,有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