我正在网站中实现搜索机制,并偶然发现它的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
表只是一个无用的开销。
所以问题是我说的两点:我可以有条件地省略连接以优化执行时间吗?
如果对此采取完全不同的方法,欢迎您分享;我主张开箱即用。
答案 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