7

我有一个类似 StackOverflow 的标记系统,用于我正在处理的数据库。我正在编写一个存储过程,它根据 WHERE 子句中未确定数量的标签查找结果。可以有 0 到 10 个标签来过滤结果。因此,例如,用户可能正在搜索标记为“apple”、“orange”和“banana”的项目,并且每个结果必须包含所有 3 个标签。我的查询变得更加复杂,因为我还要处理用于标记的交叉引用表,但出于这个问题的目的,我不会讨论这个问题。

我知道我可以做一些字符串操作并向 exec() 函数提供一个查询来处理这个问题,但我不希望与动态 SQL 相关的性能问题。我认为最好是 SQL 缓存存储过程的查询计划。

在这种情况下,您使用了哪些技术来避免动态 SQL?

根据大众的需求,这是我正在使用的查询:

SELECT ft.[RANK], s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM shader s 
INNER JOIN FREETEXTTABLE(shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
WHERE EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'color')
AND EXISTS(SELECT tsx.shader_id FROM tag_shader_xref tsx INNER JOIN tag t ON tsx.tag_id = t.tag_id WHERE tsx.shader_id = s.shader_id AND t.tag_name = 'saturation')
ORDER BY ft.[RANK] DESC

这是功能性的,但硬编码。您会看到我已将其设置为查找“颜色”和“饱和度”标签。

4

8 回答 8

13

有关此问题和类似问题的广泛概述,请参见: http: //www.sommarskog.se/dyn-search-2005.html

具体到您的问题是这里的部​​分:http: //www.sommarskog.se/dyn-search-2005.html#AND_ISNOTNULL

还要考虑到(直接)动态解决方案不一定比(可能复杂的)静态解决方案慢,因为查询计划仍然可以被缓存:请参阅http://www.sommarskog.se/dyn-search-2005.html #dynsql

因此,您必须根据实际数据量仔细测试/衡量您的选项,同时考虑实际查询(例如,使用一个或两个参数的搜索可能比使用十个参数的搜索更常见,等等)


编辑:发问者在评论中给出了优化这一点的充分理由,因此将“过早”警告移开了一点:

但是,(标准;)警告词适用:这闻起来很像过早的优化!- 你确定这个存储过程会经常被调用,使用动态 SQL 会显着慢(也就是说,与你的应用程序中发生的其他事情相比)?

于 2009-08-23T01:09:28.497 回答
3

所以这比我预期的要容易。在实现了一个相当简单的查询来处理这个问题之后,我立即获得了比我想象的要好得多的性能。所以我不确定是否有必要实施和测试其他解决方案。

我目前的数据库中有大约 200 个着色器和 500 个标签。我运行了一个我认为有点现实的测试,其中我对我的存储过程执行了 35 个不同的搜索查询,带有不同数量的标签,有和没有搜索词。我将所有这些都放在一条 SQL 语句中,然后在 ASP.NET 中对结果进行基准测试。它始终在 200 毫秒内运行这 35 次搜索。如果我将它减少到只有 5 次搜索,那么时间会下降到 10 毫秒。这样的表现太棒了。这有助于我的数据库大小很小。但我认为查询很好地利用索引也有帮助。

我在查询中更改的一件事是我查找标签的方式。我现在正在通过它们的 id 而不是名称来查找标签。通过这样做,我可以少做 1 次连接,并受益于使用索引进行搜索。然后我还添加了“dbo”。在了解到 SQL 以每个用户为基础缓存查询之后,放在表名的前面。

如果有人感兴趣,这是我完成的存储过程:

ALTER PROCEDURE [dbo].[search] 
    @search_term    varchar(100) = NULL,
    @tag1           int = NULL,
    @tag2           int = NULL,
    @tag3           int = NULL,
    @tag4           int = NULL,
    @tag5           int = NULL,
    @tag6           int = NULL,
    @tag7           int = NULL,
    @tag8           int = NULL,
    @tag9           int = NULL,
    @tag10          int = NULL
AS
BEGIN
    SET NOCOUNT ON;

    IF LEN(@search_term) > 0
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            INNER JOIN FREETEXTTABLE(dbo.shader, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
            ORDER BY ft.[RANK] DESC
        END
    ELSE
        BEGIN
            SELECT s.shader_id, s.page_name, s.name, s.description, s.download_count, s.rating, s.price FROM dbo.shader s 
            WHERE (@tag1 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag1))
            AND   (@tag2 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag2))
            AND   (@tag3 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag3))
            AND   (@tag4 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag4))
            AND   (@tag5 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag5))
            AND   (@tag6 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag6))
            AND   (@tag7 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag7))
            AND   (@tag8 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag8))
            AND   (@tag9 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag9))
            AND   (@tag10 IS NULL OR EXISTS(SELECT 1 AS num FROM dbo.tag_shader_xref tsx WHERE tsx.shader_id = s.shader_id AND tsx.tag_id = @tag10))
        END
END

尽管我没有用尽所有选项,但这仍然是一个很好的练习,因为我向自己证明了我的数据库设计非常适合这项任务。我也从发布这个问题中学到了很多东西。我知道 exec() 不好,因为它不缓存查询计划。但我不知道 sp_executesql 缓存查询计划,这很酷。我也不知道公用表表达式。并且 Henrik Opel 发布的链接充满了这类任务的好技巧。

当然,如果数据库急剧增长,我仍然可能在一年后重新审视这一点。在此之前,感谢大家的帮助。

更新:

因此,如果有人有兴趣看到它的实际效果,我在http://www.silverlightxap.com/controls上有一个在线搜索引擎的工作示例。

于 2009-08-23T17:54:36.297 回答
1

我见过两种解决这个问题的方法:

第一种是将shader表格加入tags(根据需要通过外部参照)为您要查找的每个标签一次。内连接的结果仅包括与所有标签匹配的着色器。

SELECT s.*
FROM shader s
JOIN tag_shader_xref x1 ON (s.shader_id = x1.shader_id)
JOIN tag t1 ON (t1.tag_id = x1.tag_id AND t1.tag_name = 'color')
JOIN tag_shader_xref x2 ON (s.shader_id = x2.shader_id)
JOIN tag t2 ON (t2.tag_id = x2.tag_id AND t2.tag_name = 'saturation')
JOIN tag_shader_xref x3 ON (s.shader_id = x3.shader_id)
JOIN tag t3 ON (t3.tag_id = x3.tag_id AND t3.tag_name = 'transparency');

第二种解决方案是加入该标签一次,将标签限制为您需要的三个标签,然后GROUP BYshader_id可以计算匹配项。仅当找到所有标记时,计数才会为 3(假设外部参照表中的唯一性)。

SELECT s.shader_id
FROM shader s
JOIN tag_shader_xref x ON (s.shader_id = x.shader_id)
JOIN tag t ON (t.tag_id = x.tag_id 
  AND t.tag_name IN ('color', 'saturation', 'transparency'))
GROUP BY s.shader_id
HAVING COUNT(DISTINCT t.tag_name) = 3;

你应该使用哪个?取决于您的数据库品牌对一种方法或另一种方法的优化程度。我一般用MySQL,用的不太好GROUP BY,所以还是用前一种方法比较好。在 Microsoft SQL Server 中,后一种解决方案可能会做得更好。

于 2009-08-23T01:20:29.873 回答
1

由于 EXISTS 子句中有重复的相关子查询,您的查询非常适合使用公用表表达式 (CTE):

WITH attribute AS(
  SELECT tsx.shader_id,
         t.tag_name
    FROM TAG_SHADER_XREF tsx ON tsx.shader_id = s.shader_id
    JOIN TAG t ON t.tad_id = tsx.tag_id)
SELECT ft.[RANK], 
       s.shader_id, 
       s.page_name, 
       s.name, 
       s.description, 
       s.download_count, 
       s.rating, 
       s.price 
  FROM SHADER s 
  JOIN FREETEXTTABLE(SHADER, *, @search_term) AS ft ON s.shader_id = ft.[KEY]
  JOIN attribute a1 ON a1.shader_id = s.shader_id AND a1.tag_name = 'color'
  JOIN attribute a2 ON a2.shader_id = s.shader_id AND a2.tag_name = 'saturation'
 ORDER BY ft.[RANK] DESC

通过使用 CTE,我还将 EXISTS 转换为 JOIN。

谈到您关于使用动态 SQL 的原始问题 - 唯一的选择是在应用之前检查传入参数的转义标准。IE:

WHERE (@param1 IS NULL OR a1.tag_name = @param1)

如果 @param1 包含 NULL 值,则不执行括号中 SQL 的后面部分。我更喜欢动态 SQL 方法,否则你正在制作可能不会使用的 JOIN/etc - 这是对资源的浪费。

您认为动态 SQL 存在哪些性能问题?Usingsp_executesql确实缓存了查询计划。exec坦率地说,我觉得奇怪的是,如果查询的语法/等(使用or )被验证,查询计划不会被缓存sp_executesql- 验证会在查询计划之前发生,为什么之后的步骤被跳过?

于 2009-08-23T01:34:28.763 回答
1

使用不确定数量的参数时如何避免使用动态 SQL?

您可以改为动态生成适当的参数化(准备好的)SQL 模板。

当参数第一次出现时构建和准备语句模板,缓存准备好的语句以在再次出现相同数量的参数时重复使用。

这可以在应用程序或足够复杂的存储过程中完成。

我更喜欢这种方法,比如说,一个最多需要 10 个标签的过程,并且具有处理其中任何一个为 NULL 的逻辑。

Bill Karwin 在这个问题中的GROUP BY回答可能是最容易构建的模板——您只需连接IN谓词的占位符并更新COUNT子句。其他涉及 joins-per-tag 的解决方案将需要在执行过程中增加表别名(例如 、xref1xref2)。

于 2009-08-23T02:20:00.670 回答
0

这可能不是最快的方法,但您可以为每个标签生成一个查询字符串,然后将它们与“INTERSECT”连接起来吗?

编辑:没有看到 sproc 标签,所以我不知道这是否可能。

于 2009-08-23T01:14:21.410 回答
0

我赞成 Henrik 的回答,但我能想到的另一种选择是将搜索标签放入临时表或表变量中,然后对其执行 JOIN 或使用带有子 SELECT 的 IN 子句。由于您想要包含所有搜索标签的结果,因此您需要先计算查询标签的数量,然后找到匹配标签数量等于该数量的结果。

如何将值放入表中?如果标签被传递给您的存储过程,并且您使用的是 SQL Server 2008,那么您可以使用新的表值参数功能并将表变量直接传递给您的存储过程。

否则,如果您收到单个字符串中的标签,那么您可以使用返回表的存储函数,例如此处显示的 SplitString 函数。您可以执行以下操作:

... WHERE @SearchTagCount = (SELECT COUNT(tsx.shader_id) FROM tag_shader_xref tsx
INNER JOIN tag t ON tsx.tag_id = t.tag_id
WHERE tsx.shader_id = s.shader_id AND t.tag_name IN (SELECT * FROM dbo.SplitString(@SearchTags,',')))
于 2009-08-23T01:53:55.933 回答
-1

将标记串在一起,用逗号分隔它们 'apple','orange',然后将其传递给在存储过程中使用 IN 子句的一个参数。

当然,如果您有这些标签的查找表中的值(键),我会使用它们。

编辑:

由于您需要结果中的所有标签....

不幸的是,我认为无论你做什么,SP 都会危及正在重生的计划。

您可以使用可选参数并使用 CASE 和 ISNULL 来构建参数。

我仍然认为这意味着您的 SP 已经失去了大部分缓存的优点,但我相信它比直接 exec 'string' 更好。

于 2009-08-23T00:03:41.407 回答