为什么SQL评估WHERE子句是否为False?

时间:2014-05-19 15:58:57

标签: sql sql-server sql-server-2008

我在SQL(2008)中得到了一个查询,我无法理解为什么花了这么长时间来评估我是否在WHERE语句中包含一个不应该包含的条款。影响结果。以下是查询示例:

declare @includeAll bit = 0;

    SELECT
        Id
        ,Name
        ,Total
    FROM
        MyTable
    WHERE
        @includeAll = 1 OR Id = 3926

显然,在这种情况下,@ includeAll = 1将评估为false;但是,包括这会增加查询的时间,就像它总是如此。无论有没有该子句,我得到的结果都是正确的:我只获得Id = 3926的1条目,但是(在我的真实世界查询中)包括该行增加了来自< 0秒到大约7分钟......所以它似乎在运行查询,好像该语句是真的,即使它不是,但仍然返回正确的结果。

任何可以解释的光都会有所帮助。另外,如果您有解决方法的建议,我会接受它。我希望有一个像这样的子句,这样我就可以在存储过程中包含一个参数,使其忽略它所具有的Id,如果设置为true则返回所有结果,但我不能允许只需尝试获取一条记录就会影响性能。

6 个答案:

答案 0 :(得分:6)

您需要查看查询计划以确定,但使用OR通常会使其在某些DBMS中进行扫描。

另外,请阅读@Bogdan Sahlean对一些重要细节的回应,了解为何会发生这种情况。

这可能不起作用,但你可以试试像你需要坚持使用直接SQL:

SELECT
    Id
   ,Name
   ,Total
FROM
    MyTable
WHERE Id = 3926
UNION ALL
SELECT
    Id
   ,Name
   ,Total
FROM
    MyTable
WHERE Id <> 3926
AND   @includeAll = 1

如果您正在使用存储过程,则可以有条件地以任何方式运行SQL,这可能更有效。

类似的东西:

if @includeAll = 0 then
    SELECT
        Id
       ,Name
       ,Total
    FROM
        MyTable
    WHERE Id = 3926
else
    SELECT
        Id
       ,Name
       ,Total
    FROM
        MyTable

答案 1 :(得分:3)

  

显然,在这种情况下,@ includeAll = 1将评估为false;   但是,包括这会增加查询的时间,就好像它一样   总是如此。

这是因为这两个谓词强制SQL Server选择Index|Table Scan运算符。为什么?

@includeAll变量/参数的所有可能值生成执行计划。因此,在@includeAll = 0@includeAll = 1时使用相同的执行计划。如果@includeAll = 0为真并且Id列上有索引,则SQL Server 可以使用 Index SeekIndex Seek + {{1}找到行。但如果Key|RID Lookup为真,则最佳数据访问运算符为@includeAll = 1。因此,如果执行计划必须可用所有Index|Table Scan变量值,SQL Server使用的数据访问运算符是什么:Seek或Scan?答案在下面你可以找到类似的查询:

@includeAll

这些查询具有以下执行计划:

enter image description here

如您所见,第一个执行计划包含DECLARE @includeAll BIT = 0; -- Initial solution SELECT p.ProductID, p.Name, p.Color FROM Production.Product p WHERE @includeAll = 1 OR p.ProductID = 345 -- My solution DECLARE @SqlStatement NVARCHAR(MAX); SET @SqlStatement = N' SELECT p.ProductID, p.Name, p.Color FROM Production.Product p ' + CASE WHEN @includeAll = 1 THEN '' ELSE 'WHERE p.ProductID = @ProductID' END; EXEC sp_executesql @SqlStatement, N'@ProductID INT', @ProductID = 345; Clustered Index Scan个谓词。

我的解决方案基于动态查询,并根据not optimized变量的值生成两个不同的查询:

[1] @includeAll生成的查询(@includeAll = 0

@SqlStatement

并且执行计划包括SELECT p.ProductID, p.Name, p.Color FROM Production.Product p WHERE p.ProductID = @ProductID (如上图所示)和

[2] Index Seek生成的查询(@includeAll = 1

@SqlStatement

并且执行计划包括SELECT p.ProductID, p.Name, p.Color FROM Production.Product p 。这两个生成的查询具有不同的最佳执行计划。

注意:我使用了Adventure Works 2012示例数据库

答案 2 :(得分:2)

我的猜测是参数嗅探 - @includeAll为1时编译的过程,这是已缓存的查询计划。这意味着当它为false时,你仍然可以进行全表扫描,并且索引搜索和键查找会更快。

我认为最好的方法是:

declare @includeAll bit = 0;

if @includeAll = 1
    BEGIN
        SELECT Id, Name,Total
        FROM MyTable;

    END
ELSE
    BEGIN
        SELECT Id, Name,Total
        FROM MyTable
        WHERE Id = 3926;
    END

或者你可以在每次运行时强制重新编译:

SELECT Id, Name,Total
FROM MyTable
WHERE Id = 3926
OR @IncludeAll = 1
OPTION (RECOMPILE);

为了进一步说明这一点,我设置了一个非常简单的表格,并用无意义的数据填充:

CREATE TABLE dbo.T (ID INT, Filler CHAR(1000));
INSERT dbo.T (ID)
SELECT TOP 100000 a.Number
FROM    master..spt_values a, master..spt_values b
WHERE   a.type = 'P'
AND     b.Type = 'P'
AND     b.Number BETWEEN 1 AND 100;

CREATE NONCLUSTERED INDEX IX_T_ID ON dbo.T (ID);

然后我运行了4次相同的查询。

  1. @IncludeAll设置为1,查询计划使用表扫描并计划缓存
  2. @IncludeAll设置为false的同一查询,仍然会缓存带有表扫描的计划,以便使用该计划。
  3. 清除计划的缓存,然后使用@IncludeAll false再次运行查询,以便现在使用索引查找和书签查找编译和存储计划。
  4. @IncludeAll设置为true运行。索引查找和查找再次使用。

  5. DECLARE @SQL NVARCHAR(MAX) = 'SELECT COUNT(Filler) FROM dbo.T WHERE @IncludeAll = 1 OR ID = 2;',
            @ParamDefinition NVARCHAR(MAX) = '@IncludeAll BIT',
            @PlanHandle VARBINARY(64);
    
    EXECUTE sp_executesql @SQL, @ParamDefinition, 1;
    EXECUTE sp_executesql @SQL, @ParamDefinition, 0;
    
    SELECT  @PlanHandle = cp.Plan_Handle
    FROM    sys.dm_exec_cached_plans cp
            CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS st
    WHERE   st.text LIKE '%' + @SQL;
    
    DBCC FREEPROCCACHE (@PlanHandle); -- CLEAR THE CACHE
    
    EXECUTE sp_executesql @SQL, @ParamDefinition, 0;
    EXECUTE sp_executesql @SQL, @ParamDefinition, 1;
    
    DBCC FREEPROCCACHE (@PlanHandle); -- CLEAR THE CACHE
    

    检查执行计划表明,一旦编译了查询,它将重用相同的计划而不管参数值如何,并且它将缓存适用于首先运行时传递的值的计划,而不是大多数灵活的基础。

    enter image description here

答案 3 :(得分:0)

在您的SQL中放置OR语句会导致扫描。它可能会扫描整个表或索引,这在非常大的表中非常低效。

如果查看没有@includeAll部分的查询的查询计划,您可能会看到索引查找操作。只要添加该OR,您就很可能将查询计划更改为表/索引扫描。您应该查看查询计划,看看它到底在做什么。

答案 4 :(得分:0)

在Sql Server生成查询计划的情况下,它必须创建一个适用于任何嵌入变量的任何可能值的计划。在你的情况下,索引搜索会在@IncludeAll = 0时给你最好的结果但是在@IncludeAll = 1的事件中不能使用索引搜索。所以查询优化器别无选择,只能使用适用于任何一个的查询计划@IncludeAll的值。这会导致表格扫描。

答案 5 :(得分:0)

这是因为当你在where子句中使用OR时,sql server不会短路。

通常在其他编程语言中,如果我们有像

这样的条件
IF (@Param1 == 1 || @Param2 == 2)

如果Param1 = 1,它甚至不会打扰评估另一个表达式。

在Sql Server中,如果你的where子句中的OR与查询中的OR相似

WHERE @includeAll = 1 OR Id = 3926

即使@includeAll = 1的计算结果为true,它也可以继续检查第二个条件。

更改where子句中的Order,哪个表达式被评估为1st并没有任何区别,因为这是Tunning优化器将在运行时决定的。你无法控制sql server的这种行为。短路表达式评估或评估表达式的顺序。