【问题标题】:Proper way to handle 'optional' where clause filters in SQL?在 SQL 中处理“可选”where 子句过滤器的正确方法?
【发布时间】:2010-12-14 21:45:41
【问题描述】:

假设您有一个存储过程,它需要一个可选参数。您想在 SQL 查询中使用此可选参数。通常这就是我所看到的完成方式:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND (@MyOptionalParam IS NULL OR t1.MyField = @MyOptionalParam)

这似乎工作得很好,但是如果您在 STATISTICS IO ON 的情况下运行查询,它会导致大量逻辑读取。我还尝试了以下变体:

SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = 'test'
AND t1.MyField = CASE WHEN @MyOptionalParam IS NULL THEN t1.MyField ELSE @MyOptionalParam END

并且它产生相同数量的高读取。如果我们将 SQL 转换为字符串,然后在其上调用 sp_ExecuteSQL,读取几乎为零:

DECLARE @sql nvarchar(max)

SELECT @sql = 'SELECT * FROM dbo.MyTableName t1
WHERE t1.ThisField = ''test'''

IF @MyOptionalParam IS NOT NULL
BEGIN
     SELECT @sql = @sql + ' AND t1.MyField = @MyOptionalParam '
END

EXECUTE sp_ExecuteSQL @sql, N'@MyOptionalParam', @MyOptionalParam

我疯了吗?为什么可选的 where 子句很难正确?

更新:我基本上是在问是否有办法将标准语法保留在存储过程中并获得低逻辑读取,就像 sp_ExecuteSql 方法一样。建立一个字符串对我来说似乎完全疯狂......更不用说它使维护、调试和可视化变得更加困难......

【问题讨论】:

  • Nicholas,请参阅下面的联合方法,了解在没有动态 sql 的情况下使用标准 sql 语法的方法 - 我很想看到您发布它在您的场景中的表现......
  • @Nicholas:在执行查询之前将查询构造为字符串确切地动态 SQL。这是调试的小问题 - 复制/粘贴,摆脱字符串连接语法。

标签: sql optimization


【解决方案1】:

如果我们将 SQL 转换为字符串,然后对其调用 sp_ExecuteSQL,读取几乎为零...

  1. 因为您的查询不再评估 OR,正如您所见,这会扼杀 sargability
  2. 使用 sp_executesql 时查询计划被缓存; SQL Server 不必进行硬解析...

优秀资源:The Curse & Blessing of Dynamic SQL

只要您使用参数化查询,就应该远离SQL Injection attacks

【讨论】:

    【解决方案2】:

    这是可选参数技术的另一种变体:

    SELECT * FROM dbo.MyTableName t1
    WHERE t1.ThisField = 'test'
    AND t1.MyField = COALESCE(@MyOptionalParam, t1.MyField)
    

    我很确定它也会有同样的性能问题。如果性能是第一,那么您可能会被分叉逻辑和几乎重复的查询或构建字符串所困扰,这在 TSQL 中同样令人痛苦。

    【讨论】:

      【解决方案3】:

      您在前两个 SQL 语句上使用了“OR”子句(隐式和显式)。最后一个是“AND”标准。 “OR”总是比“AND”标准更昂贵。不,你没有疯,应该是意料之中的。

      【讨论】:

      • EXEC sp_executesql 确实缓存查询计划,从 v2005 开始:sommarskog.se/dynamic_sql.html#queryplans
      • 你是对的。我没有注意到他在 sp_ExecuteSQL 上使用参数。
      • 相应地改变了我的答案。谢谢。
      【解决方案4】:

      编辑:添加link to similar question/answer with context as to why the union / if...else approach works better than OR logic(仅供参考,Remus,此链接中的回答者,曾在 SQL Server 团队开发服务代理和其他技术)

      从使用“或”语法更改为联合方法,您会看到 2 个查找应该使您的逻辑读取计数尽可能低:

      SELECT * FROM dbo.MyTableName t1
      WHERE t1.ThisField = 'test'
      AND @MyOptionalParam IS NULL 
      union all
      SELECT * FROM dbo.MyTableName t1
      WHERE t1.ThisField = 'test'
      AND t1.MyField = @MyOptionalParam
      

      如果要对结果进行重复数据删除,请使用“union”而不是“union all”。

      编辑:演示显示优化器足够聪明,可以排除在 UNION 中使用空变量值进行扫描:

      if object_id('tempdb..#data') > 0
          drop table #data
      go
      
      -- Put in some data
      select  top 1000000
              cast(a.name as varchar(100)) as thisField, cast(newid() as varchar(50)) as myField
      into    #data
      from    sys.columns a
      cross join sys.columns b
      cross join sys.columns c;
      go
      
      -- Shwo count
      select count(*) from #data;
      go
      
      -- Index on thisField
      create clustered index ixc__blah__temp on #data (thisField);
      go
      
      set statistics io on;
      go
      
      -- Query with a null parameter value
      declare @MyOptionalParam varchar(50);
      select  *
      from    #data d 
      where   d.thisField = 'test'
      and     @MyOptionalParam is null;
      go
      
      -- Union query
      declare @MyOptionalParam varchar(50);
      select  *
      from    #data d 
      where   d.thisField = 'test'
      and     @MyOptionalParam is null
      union all
      select  *
      from    #data d 
      where   d.thisField = 'test'
      and     d.myField = '5D25E9F8-EA23-47EE-A954-9D290908EE3E';
      go
      
      -- Union query with value
      declare @MyOptionalParam varchar(50);
      select @MyOptionalParam = '5D25E9F8-EA23-47EE-A954-9D290908EE3E'
      select  *
      from    #data d 
      where   d.thisField = 'test'
      and     @MyOptionalParam is null
      union all
      select  *
      from    #data d 
      where   d.thisField = 'test'
      and     d.myField = '5D25E9F8-EA23-47EE-A954-9D290908EE3E';
      go
      
      if object_id('tempdb..#data') > 0
          drop table #data
      go
      

      【讨论】:

      • 第一个查询读取整个表。这不是最小化 IO 的好方法。
      • 这比问题中描述的 SQL 语句更昂贵。
      • 对不起伙计们,但优化器肯定不会在第一个查询中扫描整个表,它足够聪明地排除基于空值“AND”与变量的查询。一个简单的例子IO stat 输出将演示、在本地运行并自行查看(请注意,如果您在 ThisField 上没有可搜索索引,则由于对其进行查询,您将始终得到扫描,因此这是假设的) - 我已经用示例编辑答案以演示 - 将一些数据选择顶部 1000000 cast(a.name as varchar(100)) as thisField, cast(newid() as varchar(50)) as myField into #data from s
      • 你有一个无条件的 SARG 过滤标准。所以是的,你确实避开了表扫描。原始问题提问者不了解 SARG,如果他遵循您的建议,最终会进行表格扫描。
      • 不确定我是否在关注你,大卫,“最初的提问者不知道 SARG”,你能澄清一下吗?你是说最初的提问者不知道什么是 SARG?或者您是说查询不是 SARGable(这是重写它的目的)?
      【解决方案5】:

      从使用“或”语法更改为两个查询方法,您将看到 2 个不同的计划,这些计划应该使您的逻辑读取计数尽可能低:

      IF @MyOptionalParam is null
      BEGIN
      
        SELECT *
        FROM dbo.MyTableName t1
      
      END
      ELSE
      BEGIN
      
        SELECT *
        FROM dbo.MyTableName t1
        WHERE t1.MyField = @MyOptionalParam
      
      END
      

      您需要与程序员减少重复的冲动作斗争。意识到您要求两个根本不同的执行计划,并且需要两个查询来生成两个计划。

      【讨论】:

      • 但是如果您有多个要过滤的可选参数怎么办?我想我不明白为什么它是“两个根本不同的执行计划”。如果我是解析器,我会查看变量,然后说“嘿,它是空的,永远不会是这样。我可以停止过滤它。”但我想它不会那样工作,至少在 SQL 2005 中是这样。
      • 如果您有多个可选参数,很可能只有少数参数对查询计划很重要......只需在这些参数上进行分支。至于参数嗅探:sqlblog.com/blogs/ben_nevarez/archive/2009/08/27/… 不过这种方法祝你好运。
      • 致无评论的反对者——我明白了。你相信你是对的,你也是大多数。但是,你错了。
      猜你喜欢
      • 2016-07-02
      • 2014-09-03
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 2012-06-24
      • 2016-08-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多