2008-12-22 79 views
1

我想在存储过程的表上进行优先级匹配。这些要求有点棘手的解释,但希望这是有道理的。假设我们有一张名为书籍的表格,其中包含ID,作者,标题,日期和页面字段。SQL优先匹配

我们还有一个存储过程,它将查询与表中的一行进行匹配。

这里是PROC的签名:

create procedure match 
    @pAuthor varchar(100) 
    ,@pTitle varchar(100) 
    ,@pDate varchar(100) 
    ,@pPages varchar(100) 

as 

... 

的优先规则如下:

  • 首先,尝试和比赛的所有4个参数。如果我们找到匹配的回报。
  • 接下来尝试使用任何3个参数进行匹配。第一个参数的优先级最高,第四个最低。如果我们发现任何比赛都会返回比赛。
  • 接下来我们检查两个参数是否匹配,最后是否匹配(仍然遵循参数顺序的优先规则)。

我已经实施了这种情况。例如:

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和其他人,我试着执行您的代码,它是由条,在这里我们添加所有的罪名窒息的首份订单。它给了我一个无效的列名错误。当我将总数注释出来,然后按单个别名排序时,proc编译就会正常。

任何人有任何想法?

这是在Microsoft SQL Server 2005

+0

使用AND连接WHERE子句中的条件时发生了什么? – 2008-12-22 23:03:25

+0

@JL这是否有趣? – 2008-12-23 19:11:48

回答

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 
+0

我认为这不是他想要的,因为在作者+标题上的匹配会在标题+日期+页面上击败匹配。我想他想要3场比赛总是击败2场比赛。 – 2008-12-22 20:00:16

+0

@Dave,正确我需要3场比赛才能到达2. @汤姆,我们可以假设我们只会找到一场比赛。选择Top 1会正常工作。 – 2008-12-22 20:15:32

+0

我做了一些更改以解决此问题,但没有WHERE子句,性能可能会成为问题。尽管如此,我会给它更多的想法。 – 2008-12-22 20:20:29

1

我没有时间写出查询,但我认为这个想法可行。

对于您的谓词,请使用“author = @pAuthor或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 

你还是需要为每个新列扩展它,但只有一点点。

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子句,否则你将失去潜在的匹配

0

好吧,让我重申我对你的问题的理解:你想要一个存储过程,可以获取可变数量的参数并传回匹配的顶行偏好的加权顺序参数在SQL Server 2005上传递。

理想情况下,它将使用WHERE子句来防止全表扫描以及利用索引,并将“短路”搜索 - 您不想搜索所有可能的组合,如果可以早找到的话。也许我们也可以允许其他比较器比如=,例如> =用于日期,LIKE用于字符串等。

一种可能的方式是像在this article中那样传递XML参数并使用.Net存储过程,但让我们保持原样现在是香草T-SQL。

这看起来对我来说,在参数二进制搜索:搜索所有参数,然后删除最后一个,然后挂断第二个最后一个,但包括最后一个,等

让我们传递的参数作为由于存储过程不允许将数组作为参数传递,因此使用分隔字符串。这将允许我们在我们的存储过程中获取可变数量的参数,而不需要为每个参数变化存储过程。

为了让任何形式的比较,我们会通过整个WHERE子句列表,就像这样:标题LIKE“%%的东西”

传递多个参数指在一个字符串界定他们。我们将使用代字符〜字符来分隔参数,如下所示:author ='Chris Latta'〜title like'%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实现的功能。

跟这个有几个小问题:

  • 它依赖于调用者知道,他们必须在字段名称后留出空间为参数正常工作
  • 你不能有场用空格名字在其中 - 可以解决的一些努力
  • 它假定相关的排序顺序总是上升
  • 下一个程序员有来看待这个过程会觉得你疯了:)
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子句的联合来避免这种情况,每个属性都有一个 - 将会有重复,但是无论如何你都可以进入前1名。

编辑:添加了WHERE ... OR子句。我会觉得更舒服,如果它是

SELECT ... FROM books WHERE author = @pAuthor 
UNION 
SELECT ... FROM books WHERE title = @pTitle 
UNION 
... 
0

在问候ORDER BY子句未能编译:

递归说,(在评论),别名可能不被使用的表达式中在Order By子句中。为了解决这个问题,我使用了一个返回行的子查询,然后在外部查询中排序。通过这种方式,我可以在order by子句中使用别名。慢一点,但更清洁。

2

我相信你工作的答案是迄今为止最简单的。但我也相信,在SQL服务器中,他们将始终是全表扫描。 (在Oracle中,如果表没有经过大量的同时DML,则可以使用位图索引)

更复杂的解决方案,但性能更高的方法是构建自己的索引。不是SQL Server索引,而是您自己的。

创建一个表(哈希索引)3列(查找,哈希,职级,ROWID)

假设你有3列搜索上。 A,B,C

对于添加到书籍的每一行,您都会通过触发器或CRUD proc将7行插入到hash_index中。

首先,您

insert into hash_index 
SELECT HASH(A & B & C), 7 , ROWID 
FROM Books 

哪里&是连接运算符和哈希函数

那么你会插入散列对于A & B,A & C和B & C. 你现在有一些灵活性,你可以给他们所有的相同的等级,或者如果A B & C的优势匹配,你可以给他们一个更高的等级。

然后插入散列为A本身和B和C与相同的等级选择...所有相同的数字或所有不同的...你甚至可以说A上的一个匹配比一个匹配更高的选择在B & C.这种解决方案给你很大的灵活性。

当然,这会增加很多INSERT开销,但是如果书上的DML很低或者性能不相关,那就很好。

现在,当您去搜索时,您将创建一个函数,为您的@A,@B和@C返回一个HASH表。您将有一个包含7个值的小表,您将加入哈希索引表中的查找哈希。这会给你一切可能的匹配,并可能有一些错误的匹配(这只是散列的性质)。你会得到这个结果,在秩列上订购desc。然后将第一个rowid返回到书本表,并确保@A @B @C的所有值实际上都在该行中。如果不是这样,你就会被误判,你需要检查下一个rowid。

这个“滚动你自己”的每一个操作都非常快。

  • 把你的3个值散列成一个小的7行表变量=非常快。
  • 加入他们的指数在Hash_index表=非常快速索引查找
  • 遍历结果集将导致1个或可能通过ROWID =非常快

当然2次或3台访问,所有其中的一些可能会比FTS慢......但是FTS将继续变得越来越慢。 FTS的规模会比这慢。你必须玩它。