1

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

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

这是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 中

4

7 回答 7

2

我相信您正在研究的答案是迄今为止最简单的。但我也相信,在 SQL Server 中,它们将永远是全表扫描。(在 Oracle 中,如果表没有同时进行大量 DML,您可以使用位图索引)

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

创建一个包含 3 列(lookup-hash、rank、Rowid)的表(Hash-index)

假设您有 3 列可供搜索。甲、乙、丙

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

首先你会

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

其中 & 是连接运算符,HASH 是一个函数

然后你将为 A & B、A & C 和 B & C 插入哈希值。你现在有一些灵活性,你可以给它们所有相同的排名,或者如果 A & B 与 B & C 更匹配,你可以给它们一个更高的等级。

然后为 A 本身和 B 和 C 插入具有相同等级选择的哈希......所有相同的数字或完全不同......您甚至可以说 A 上的匹配比 B 和 C 上的匹配具有更高的选择. 这个解决方案给你很大的灵活性。

当然,这会增加很多 INSERT 开销,但如果 Books 上的 DML 很低或性能不相关,你就可以了。

现在,当您进行搜索时,您将创建一个函数,该函数返回您的@A、@B 和@C 的哈希表。您将有一个包含 7 个值的小表,您将在散列索引表中加入查找散列。这将为您提供所有可能的匹配以及可能的一些错误匹配(这只是哈希的本质)。你会得到这个结果,在排名列上排序 desc。然后将第一个 rowid 带回 book 表并确保 @A @B @C 的所有值实际上都在该行中。万一不是,并且您收到了误报,您需要检查下一个 rowid。

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

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

当然,所有这些加起来可能比 FTS 慢……但是 FTS 将继续变得越来越慢。会有一个 FTS 比这慢的大小。你必须玩它。

于 2008-12-23T21:23:48.433 回答
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
于 2008-12-22T19:58:23.443 回答
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

您仍然需要为每个新列扩展它,但只需要一点点。

于 2008-12-22T19:58:33.067 回答
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
...
于 2008-12-23T08:58:29.297 回答
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 子句,否则您将失去潜在的匹配

于 2008-12-22T20:02:48.177 回答
0

好的,让我重申一下我对您的问题的理解:您需要一个存储过程,它可以采用可变数量的参数,并将与 SQL Server 2005 上传递的加权优先顺序中的参数匹配的第一行传回。

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

一种可能的方法是像本文中那样将参数作为 XML 传递并使用 .Net 存储过程,但现在让我们保持普通的普通 T-SQL。

在我看来,这就像对参数的二进制搜索:搜索所有参数,然后删除最后一个,然后删除倒数第二个但包括最后一个,等等。

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

为了允许进行任何类型的比较,我们将传递整个 WHERE 子句列表,如下所示: title like '%something%'

传递多个参数意味着将它们分隔在一个字符串中。我们将使用波浪号 ~ 字符来分隔参数,如下所示: 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 可以实现的目标。

这有几个小问题:

  • 它依赖于调用者知道他们必须在字段名称后留一个空格才能使参数正常工作
  • 你不能有带有空格的字段名称 - 可以通过一些努力来修复
  • 它假设相关的排序顺序总是升序的
  • 下一个必须查看此过程的程序员会认为您疯了 :)
于 2008-12-23T07:53:04.403 回答
0

关于无法编译的 Order By 子句:

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

于 2008-12-23T13:41:24.513 回答