SQL优先级匹配

时间:2008-12-22 19:42:38

标签: sql sql-server stored-procedures matching

我正在尝试在存储过程中的表上进行优先匹配。这些要求有点难以解释,但希望这是有道理的。假设我们有一个名为books的表,包含id,author,title,date和pages字段。

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

这是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和其他人,我尝试实现你的代码,它在第一个Order by Clause上窒息,我们在那里添加了所有的计数。它给我一个无效的列名错误。当我注释掉总计数,并按个别别名排序时,proc编译得很好。

有人有什么想法吗?

这是在Microsoft Sql Server 2005中

7 个答案:

答案 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。

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

  • 将3个值散列到一个小的7行表变量=非常快。
  • 将它们连接到Hash_index表中的索引=非常快速的索引查找
  • 循环结果集将导致通过rowid =非常快
  • 进行1或2或3个表访问

当然,所有这些都可能比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子句中使用别名。有点慢,但更清洁。