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

时间:2009-08-22 23:55:50

标签: sql sql-server tsql stored-procedures sql-match-all

我有一个类似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

这是功能性但硬编码。你会看到我设置它来寻找'颜色'和'饱和度'标签。

8 个答案:

答案 0 :(得分: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

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


编辑:提问者给出了一个很好的理由在评论中对此进行优化,因此将“过早”警告移开了一点:

(标准;)警告词适用,但是:这有点像过早的优化! - 你确定这个sproc会被调用,通常使用动态SQL会显着较慢(也就是说,与您应用中发生的其他事情相比)?

答案 1 :(得分: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缓存查询计划,这非常酷。我也不知道Common Table Expressions。 Henrik Opel发布的链接充满了针对此类任务的优秀提示。

当然,如果数据库大幅增长,我可能会在一年后重新审视这个问题。在此之前,感谢大家的帮助。

更新:

所以,如果有人有兴趣了解这一点,我会在http://www.silverlightxap.com/controls在线搜索此搜索引擎。

答案 2 :(得分: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 BY shader_id,以便您可以计算匹配数。只有找到所有标签时,计数才为三(假设外部参照表中的唯一性)。

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中,后一种解决方案可能会做得更好。

答案 3 :(得分: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存在哪些性能问题?使用sp_executesql会缓存查询计划。坦率地说,如果查询验证语法/ etc(使用execsp_executesql),查询计划将不会被缓存,我觉得很奇怪 - 验证将在查询计划之前进行,为什么之后被跳过?

答案 4 :(得分:1)

  

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

您可以动态生成相应的参数化(准备好的)SQL模板

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

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

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

这个问题中的

Bill Karwin's GROUP BY answer可能是最容易构建的模板 - 您只需连接IN谓词的占位符并更新COUNT子句。其他涉及每个标记连接的解决方案需要递增表别名(例如,xref1xref2等)。

答案 5 :(得分:0)

这可能不是最快的方法,但你可以为每个标签生成一个查询字符串,然后用“INTERSECT”加入它们吗?

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

答案 6 :(得分:0)

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

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

否则,如果您在单个字符串中收到标记,则可以使用返回表的存储函数,例如SplitString function shown here。你可以这样做:

... 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,',')))

答案 7 :(得分:-1)

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

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

编辑:

因为你需要结果中的所有标签....

不幸的是,我认为无论你做什么,SP都将面临重建计划的危险。

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

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

相关问题