我正在尝试在存储过程中的表上进行优先匹配。这些要求有点难以解释,但希望这是有道理的。假设我们有一个名为books的表,包含id,author,title,date和pages字段。
我们还有一个存储过程,它将查询与表中的一行匹配。
这是proc的签名:
create procedure match
@pAuthor varchar(100)
,@pTitle varchar(100)
,@pDate varchar(100)
,@pPages varchar(100)
as
...
优先规则如下:
我已经逐案实施了这个案例。例如:
select @lvId = id
from books
where
author = @pAuthor
,title = @pTitle
,date = @pDate
,pages = @pPages
if @@rowCount = 1 begin
select @lvId
return
end
select @lvId = id
from books
where
author = @pAuthor
,title = @pTitle
,date = @pDate
if @@rowCount = 1 begin
select @lvId
return
end
....
但是,对于表中的每个新列,单个检查的数量增加了2个。我真的想将此概括为X个列;但是,我无法提出计划。
感谢您的阅读,我可以提供所需的任何其他信息。
加了:
Dave和其他人,我尝试实现你的代码,它在第一个Order by Clause上窒息,我们在那里添加了所有的计数。它给我一个无效的列名错误。当我注释掉总计数,并按个别别名排序时,proc编译得很好。
有人有什么想法吗?
这是在Microsoft Sql Server 2005中
答案 0 :(得分:2)
我相信你所做的答案是迄今为止最简单的。但我也相信在SQL服务器中,它们将始终是全表扫描。 (在Oracle中,如果表没有经历大量的同时DML,你可以使用位图索引)
更复杂的解决方案,但更高效的解决方案是建立自己的索引。不是SQL Server索引,而是您自己的索引。
创建一个包含3列的表(Hash-index)(lookup-hash,rank,Rowid)
假设您有3列要搜索。 A,B,C
对于添加到Books的每一行,您将通过触发器或CRUD proc将7行插入hash_index。
首先你会
insert into hash_index
SELECT HASH(A & B & C), 7 , ROWID
FROM Books
哪里&是连接运算符,HASH是一个函数
然后你会为A& amp插入哈希值B,A& C和B& C。 你现在有一些灵活性,你可以给他们所有相同的等级或如果A& B是B& B的绝佳搭档。 C你可以给他们更高的排名。
然后单独插入Hashes for A和B和C使用相同的等级选择...所有相同的数字或全部不同...你甚至可以说A上的匹配比匹配更高的选择B& C.此解决方案为您提供了很大的灵活性。
当然,这会增加很多INSERT开销,但是如果Book上的DML很低或者性能不相关那么你就没问题了。
现在当你去搜索时,你将创建一个函数,为你的@ A,@ B和@C返回一个HASH表。你将拥有一个包含7个值的小表,你将加入到hash-index表中的lookup-hash中。这将为您提供所有可能的匹配以及可能的一些错误匹配(这只是哈希的本质)。您将获得该结果,在排名列上订购desc。然后将第一个rowid带回book表,确保@A @B @C的所有值实际都在该行中。如果没有机会,你就会被误判,你需要检查下一个rowid。
这个“滚动你自己”的每个操作都非常快。
当然,所有这些都可能比FTS慢......但是FTS会继续变得越来越慢。将有一个FTS慢于此的大小。你必须玩它。
答案 1 :(得分:1)
如果多个结果与达到的任何给定参数集匹配,则不解释会发生什么,因此您需要更改此项以考虑这些业务规则。现在我已经将它设置为返回与之后的参数匹配的书籍。例如,作者,标题和页面上的匹配将出现在与作者和标题匹配的匹配之前。
你的RDBMS可能有不同的处理“TOP”的方式,所以你可能也需要调整它。
SELECT TOP 1
author,
title,
date,
pages
FROM
Books
WHERE
author = @author OR
title = @title OR
date = @date OR
pages = @pages OR
ORDER BY
CASE WHEN author = @author THEN 1 ELSE 0 END +
CASE WHEN title = @title THEN 1 ELSE 0 END +
CASE WHEN date = @date THEN 1 ELSE 0 END +
CASE WHEN pages = @pages THEN 1 ELSE 0 END DESC,
CASE WHEN author = @author THEN 8 ELSE 0 END +
CASE WHEN title = @title THEN 4 ELSE 0 END +
CASE WHEN date = @date THEN 2 ELSE 0 END +
CASE WHEN pages = @pages THEN 1 ELSE 0 END DESC
答案 2 :(得分:1)
我没有时间写出查询,但我认为这个想法会奏效。
对于您的谓词,请使用“author = @pAuthor OR title = @ptitle ...”,以便获得所有候选行。
使用CASE表达式或任何您喜欢的方法在结果集中创建虚拟列,例如:
SELECT CASE WHEN author = @pAuthor THEN 1 ELSE 0 END author_match,
...
然后添加此订单并获取返回的第一行:
ORDER BY (author_match+title_match+date_match+page_match) DESC,
author_match DESC,
title_match DESC,
date_match DESC
page_match DESC
您仍然需要为每个新列扩展它,但只需要一点点。
答案 3 :(得分:0)
select id,
CASE WHEN @pPages = pages
THEN 1 ELSE 0
END
+ Case WHEN @pAuthor=author
THEN 1 ELSE 0
END AS
/* + Do this for each attribute. If each of your
attributes are just as important as the other
for example matching author is jsut as a good as matching title then
leave the values alone, if different matches are more
important then change the values */ as MatchRank
from books
where author = @pAuthor OR
title = @pTitle OR
date = @pDate
ORDER BY MatchRank DESC
当我运行此查询(仅修改为适合我自己的一个表)时,它在SQL2005中工作正常。
我建议使用where子句,但是您可以尝试使用它来查看性能影响。您将需要使用OR子句,否则您将失去潜在的匹配
答案 4 :(得分:0)
好的,让我重申一下我对您的问题的理解:您需要一个可以获取可变数量参数的存储过程,并传回与SQL Server 2005上传递的加权优先顺序中的参数匹配的顶行。 / p>
理想情况下,它将使用WHERE子句来防止全表扫描并利用索引并将“短路”搜索 - 如果可以提前找到,则不希望搜索所有可能的组合。也许我们也可以允许其他比较符= =例如> =表示日期,LIKE表示字符串等。
一种可能的方法是将参数作为XML传递给this article并使用.Net存储过程,但现在让它保持简单的vanilla T-SQL。
这对我来说就像对参数进行二元搜索一样:搜索所有参数,然后删除最后一个参数,然后删除最后一个,但包括最后一个,等等。
让我们将参数作为分隔字符串传递,因为存储过程不允许将数组作为参数传递。这将允许我们在存储过程中获取可变数量的参数,而不需要为每个参数变化存储过程。
为了允许任何类型的比较,我们将传递整个WHERE子句列表,如下所示:title like'%something%'
传递多个参数意味着在字符串中分隔它们。我们将使用波浪号〜字符来分隔参数,例如:author ='Chris Latta'〜标题如'%something%'~pages> = 100
然后,只需要对符合我们的有序参数列表的第一行进行二进制加权搜索(希望带有注释的存储过程是不言自明的,但如果没有,请告诉我)。请注意,始终保证您得到一个结果(假设您的表至少有一行),因为最后一次搜索是无参数的。
以下是存储过程代码:
CREATE PROCEDURE FirstMatch
@SearchParams VARCHAR(2000)
AS
BEGIN
DECLARE @SQLstmt NVARCHAR(2000)
DECLARE @WhereClause NVARCHAR(2000)
DECLARE @OrderByClause NVARCHAR(500)
DECLARE @NumParams INT
DECLARE @Pos INT
DECLARE @BinarySearch INT
DECLARE @Rows INT
-- Create a temporary table to store our parameters
CREATE TABLE #params
(
BitMask int, -- Uniquely identifying bit mask
FieldName VARCHAR(100), -- The field name for use in the ORDER BY clause
WhereClause VARCHAR(100) -- The bit to use in the WHERE clause
)
-- Temporary table identical to our result set (the books table) so intermediate results arent output
CREATE TABLE #junk
(
id INT,
author VARCHAR(50),
title VARCHAR(50),
printed DATETIME,
pages INT
)
-- Ill use tilde ~ as the delimiter that separates parameters
SET @SearchParams = LTRIM(RTRIM(@SearchParams))+ '~'
SET @Pos = CHARINDEX('~', @SearchParams, 1)
SET @NumParams = 0
-- Populate the #params table with the delimited parameters passed
IF REPLACE(@SearchParams, '~', '') <> ''
BEGIN
WHILE @Pos > 0
BEGIN
SET @NumParams = @NumParams + 1
SET @WhereClause = LTRIM(RTRIM(LEFT(@SearchParams, @Pos - 1)))
IF @WhereClause <> ''
BEGIN
-- This assumes your field names dont have spaces and that you leave a space between the field name and the comparator
INSERT INTO #params (BitMask, FieldName, WhereClause) VALUES (POWER(2, @NumParams - 1), LTRIM(RTRIM(LEFT(@WhereClause, CHARINDEX(' ', @WhereClause, 1) - 1))), @WhereClause)
END
SET @SearchParams = RIGHT(@SearchParams, LEN(@SearchParams) - @Pos)
SET @Pos = CHARINDEX('~', @SearchParams, 1)
END
END
-- Set the binary search to search from all parameters down to one in order of preference
SET @BinarySearch = POWER(2, @NumParams)
SET @Rows = 0
WHILE (@BinarySearch > 0) AND (@Rows = 0)
BEGIN
SET @BinarySearch = @BinarySearch - 1
SET @WhereClause = ' WHERE '
SET @OrderByClause = ' ORDER BY '
SELECT @OrderByClause = @OrderByClause + FieldName + ', ' FROM #params WHERE (@BinarySearch & BitMask) = BitMask ORDER BY BitMask
SET @OrderByClause = LEFT(@OrderByClause, LEN(@OrderByClause) - 1) -- Remove the trailing comma
SELECT @WhereClause = @WhereClause + WhereClause + ' AND ' FROM #params WHERE (@BinarySearch & BitMask) = BitMask ORDER BY BitMask
SET @WhereClause = LEFT(@WhereClause, LEN(@WhereClause) - 4) -- Remove the trailing AND
IF @BinarySearch = 0
BEGIN
-- If nothing found so far, return the top row in the order of the parameters fields
SET @WhereClause = ''
-- Use the full order sequence of fields to return the results
SET @OrderByClause = ' ORDER BY '
SELECT @OrderByClause = @OrderByClause + FieldName + ', ' FROM #params ORDER BY BitMask
SET @OrderByClause = LEFT(@OrderByClause, LEN(@OrderByClause) - 1) -- Remove the trailing comma
END
-- Find out if there are any results for this search
SET @SQLstmt = 'SELECT TOP 1 id, author, title, printed, pages INTO #junk FROM books' + @WhereClause + @OrderByClause
Exec (@SQLstmt)
SET @Rows = @@RowCount
END
-- Stop the result set being eaten by the junk table
SET @SQLstmt = REPLACE(@SQLstmt, 'INTO #junk ', '')
-- Uncomment the next line to see the SQL you are producing
--PRINT @SQLstmt
-- This gives the result set
Exec (@SQLstmt)
END
调用此存储过程如下:
FirstMatch 'author = ''Chris Latta''~pages > 100~title like ''%something%'''
你有它 - 一个完全可扩展,优化的搜索加权优先顺序的最高结果。这是一个有趣的问题,并展示了您可以使用本机T-SQL实现的目标。
有几个小问题:
答案 5 :(得分:0)
试试这个:
ALTER PROCEDURE match
@pAuthor varchar(100)
,@pTitle varchar(100)
,@pDate varchar(100)
,@pPages varchar(100)
-- exec match 'a title', 'b author', '1/1/2007', 15
AS
SELECT id,
CASE WHEN author = @pAuthor THEN 1 ELSE 0 END
+ CASE WHEN title = @pTitle THEN 1 ELSE 0 END
+ CASE WHEN bookdate = @pDate THEN 1 ELSE 0 END
+ CASE WHEN pages = @pPages THEN 1 ELSE 0 END AS matches,
CASE WHEN author = @pAuthor THEN 4 ELSE 0 END
+ CASE WHEN title = @pTitle THEN 3 ELSE 0 END
+ CASE WHEN bookdate = @pDate THEN 2 ELSE 0 END
+ CASE WHEN pages = @pPages THEN 1 ELSE 0 END AS score
FROM books
WHERE author = #pAuthor
OR title = @pTitle
OR bookdate = @PDate
OR pages = @pPages
ORDER BY matches DESC, score DESC
但是,这当然会导致表扫描。你可以通过使它成为一个CTE和4个WHERE子句的联合来避免这种情况,每个属性一个 - 会有重复,但你仍然可以选择TOP 1。
编辑:添加了WHERE ... OR子句。如果它是
,我会感觉更舒服SELECT ... FROM books WHERE author = @pAuthor
UNION
SELECT ... FROM books WHERE title = @pTitle
UNION
...
答案 6 :(得分:0)
关于Order By子句无法编译:
正如递归所说(在评论中),别名'可能不在Order By子句中使用的表达式中。为了解决这个问题,我使用了一个返回行的子查询,然后在外部查询中进行排序。通过这种方式,我可以在order by子句中使用别名。有点慢,但更清洁。