嵌套循环在Where语句中查杀性能

时间:2016-01-19 00:07:57

标签: sql tsql

在WHERE子句中使用嵌套循环时,我遇到了严重的性能问题。

当我按原样运行以下代码时,需要几分钟时间。诀窍是如果report_id为NULL,我正在使用WHERE子句来提取 ALL 数据,但如果我在参数字符串中设置它们,则仅使用某些report_id

函数[fn_Parse_List]将VARCHAR字符串(例如'123,456,789')转换为表格,其中每一行都是整数形式的数字,然后在IN子句中使用。

当我使用report_id = '456'(虚线部分)运行下面的代码时,代码需要几秒钟,但传递临时表并使用WHERE子句中的SELECT语句会将其杀死。

alter procedure dbo.p_revenue

(@report_id varchar(max) = NULL)

as

select cast(value as int) Report_ID 
into #report_ID_Temp 
from [fn_Parse_List] (@report_id)

SELECT * 
FROM BIGTABLE
where @report_id is null 
   or a.report_id in (select Report_ID from #report_ID_Temp)
--Where @report_id is null or a.report_id in (456)


exec p_revenue @report_id = '456'

有没有办法优化这个?我尝试使用表#report_ID_Temp进行JOIN,但是当report_id为NULL时,它仍然需要很长时间并且不起作用。

4 个答案:

答案 0 :(得分:2)

你违反了三条不同的规则。

  1. 如果您需要两个查询计划,则需要两个查询:OR不会为您提供两个查询计划。 IF确实如此。
  2. 如果您有临时表,请确保它具有主键和任何适当的索引。在您的情况下,您需要ALTER TABLE语句来添加主键聚簇索引。或者您可以CREATE TABLE首先声明结构。
  3. 如果您认为fn_Parse_List是个好主意,那么您的阅读时间不够Sommarskog

答案 1 :(得分:1)

如果我要为您的案例编写存储过程,我会使用Table Valued Parameter(TVP)而不是将多个值作为逗号分隔的字符串传递。

如下所示:

-- Create a type for the TVP
CREATE TYPE REPORT_IDS_PAR AS TABLE(
    report_id INT
);
GO

-- Use the TVP type instead of VARCHAR
CREATE PROCEDURE dbo.revenue
    @report_ids REPORT_IDS_PAR READONLY
AS
BEGIN
    SET NOCOUNT ON;

    IF NOT EXISTS(SELECT 1 FROM @report_ids)
        SELECT 
            *
        FROM 
            BIGTABLE;
    ELSE
        SELECT 
            *
        FROM 
            @report_ids AS ids
            INNER JOIN BIGTABLE AS bt ON
                bt.report_id=ids.report_id;
        -- OPTION(RECOMPILE) -- see remark below
END
GO

-- Execute the Stored Procedure 
DECLARE @ids REPORT_IDS_PAR;

-- Empty table for all rows:
EXEC dbo.revenue @ids;

-- Specific report_id's for specific rows:
INSERT INTO @ids(report_id)VALUES(123),(456),(789);
EXEC dbo.revenue @ids;
GO

如果您使用包含大量行或行数变化很大的TVP运行此过程,建议您在查询中添加选项OPTION(RECOMPILE)

答案 2 :(得分:0)

我看到有两件可能有助于提高绩效的事情。取决于哪个部分花费的时间最长。首先,SELECT INTO是SQL Server 2014之前的单线程操作。如果这需要很长时间,请使用CREATE TABLE创建显式定义的临时表。其次,根据插入临时表的记录数,您可能需要Report_ID列的索引。这一切都可以在存储过程的主体中完成。如果你最终使用显式定义的临时表,我会在加载数据后创建索引。

如果这没有帮助,请首先检查BIGTABLE上的report_id列是否已编入索引。然后尝试将select分成2并与UNION ALL组合,如下所示:

ALTER PROCEDURE dbo.p_revenue
(
  @report_id VARCHAR(MAX) = NULL
)
AS
    SELECT  CAST(value AS INT) Report_ID
    INTO    #report_ID_Temp
    FROM    fn_Parse_List(@report_id);

    SELECT  *
    FROM    BIGTABLE
    WHERE   @report_id IS NULL

    UNION ALL

    SELECT  *
    FROM    BIGTABLE
    WHERE a.report_id IN ( SELECT  Report_ID
                                FROM    #report_ID_Temp );

GO

EXEC p_revenue @report_id = '456';

答案 3 :(得分:0)

  
      
  1. 您是说我应该有两个查询,一个是在report_id不存在的情况下提取的,另一个是有一个report_ids列表?
  2.   

是的,是的,是的。事实上,当你直接输入数字时,它会以某种方式运作,从而分散你的核心问题。当@report_id为null时需要进行表扫描,而当不是@report_id时需要进行索引查找,并且在一个执行计划中不能同时进行。表演不可避免地会受到各种各样的影响。

  

我不愿意,因为我从中抽出的桌子实际上是一个   查看800行,其他参数未在上面显示。

我看不出问题出在哪里,SELECT * FROM BIGTABLESELECT * FROM BIGVIEW似乎相同。如果需要参数您可以使用内联表值函数。如果你有更多具有变量选择性的参数,例如@report_id,我想你最迟还是会以动态sql结束。

@db_brad提出的

UNION ALL会有所帮助,但即使没有必要,也会执行其中一个子查询。

作为快速补丁您可以将OPTION(RECOMPILE)附加到SELECT并进行一次表扫描,另一次搜索索引,但每次重新编译会产生非常重要的开销。