使用复杂 where 子句对 10M 行进行查询的性能

Erw*_*ers 2 performance sql-server-2008 sql-server query-performance

我们有一个查询会在运行时破坏我们的生产服务器。

它是报告功能的一部分,不好的部分如下所示:

SELECT DISTINCT 
    mt.ID           AS ID
FROM 
    [dbo].[MyTable]    mt
WITH (NOLOCK)
WHERE           
    (@aVariable           IS NULL 
      OR (CONVERT(VARCHAR(22), mt.Date1, 112) >= CONVERT(VARCHAR(22), @date1, 112))
    AND (@status            IS NULL 
    OR @status <> 2 
    OR  (   @status = 2 
        AND (  SELECT COUNT(*) 
               FROM 
               MyTable mt2
               WITH (NOLOCK)
               WHERE 
                  mt2.CaseID = mt.CaseID 
                  AND mt2.Date1 > mt.Date1
             ) = 0
        )
    )
    AND (@aSecondVariable IS NULL 
           OR (CONVERT(VARCHAR(22), mt.Date1, 112) <= CONVERT(VARCHAR(22), @date1, 112)))
    AND (@aThirdVariable  IS NULL 
           OR (CONVERT(VARCHAR(22), mt.Date2, 112) >= CONVERT(VARCHAR(22), @date2, 112)))
    AND (@aFourtVariable  IS NULL 
          OR (CONVERT(VARCHAR(22), mt.Date2, 112) <= CONVERT(VARCHAR(22), @date2, 112)))
Run Code Online (Sandbox Code Playgroud)

此外,表和索引的创建如下:

CREATE INDEX [MyIndex] ON [dbo].[MyTable]
    ([AColumn], [AColumn2], [Date1], [Date2]) 
WITH (FILLFACTOR = 90)
Run Code Online (Sandbox Code Playgroud)

MyTable有大约 80 列和一个单列 Primary Key: (ID)

MyTable mt由大约10.000.000行。有在包含该列的索引aVariableaSecondVariableaThirdVariableaFourthVariable。日期列的大约一半值是空值。在索引中,它们位于第 3 和第 4 位。

当我们在一台服务器(没有用户)上运行查询时,它的表现非常好。当我们在生产中(与用户一起)运行它时,它花费的时间太长并且超时。

我们想知道这怎么可能。两台服务器上的执行计划相同。我们认为结果可能被缓存在某处或者空闲内存(2GB RAM)不足。

我们不是数据库性能方面的专家,希望有真正的 DBA 能给我们提供意见。谢谢。

ype*_*eᵀᴹ 14

几点建议:

  • 取出DISTINCT作为ID是主键。您不可能在结果中得到重复的行。
  • 不要转换datetime列。这使您的条件不可 sargable,并且查询将始终进行表扫描。如果变量被声明为日期,则它们也不需要转换,但这对于可存储性来说不是问题。
  • 正如@Aaron 在评论中所建议的那样,对日期和日期时间使用封闭式开放范围。请阅读他的博客文章:要戒掉的坏习惯:错误处理日期/范围查询
  • 使用NOT EXISTS而不是(SELECT COUNT(*) ...) = 0检查是否没有匹配某些条件的行。
  • 放下WITH (NOLOCK)提示。另一篇博客:坏习惯:把NOLOCK无处不在和问题,在这个网站与一些宝贵的意见:是否NOLOCK总是不好?.
  • 添加适当的索引。我猜想单独的索引(Date1)(Date2)或索引(Date1, Date2)就可以了,但这需要测试。索引(CaseId, Date1)对子查询很有用。
  • (小注)始终使用架构前缀。@Aaron Bertrand 的另一篇博文:要戒掉的坏习惯:避免使用模式前缀
  • 添加OPTION (RECOMPILE)(如@Mikael Eriksson 建议的那样)。这基本上告诉优化器不要依赖缓存计划,而是花一些时间为每个查询运行重新编译查询 - 即根据新参数值生成新计划。有 7 个变量可以显着改变查询,这似乎是一个非常好的选择。阅读来自@Paul White 的文章,了解“参数嵌入优化”、参数嗅探以及其他选项和优势的更详细说明:参数嗅探、嵌入和RECOMPILE选项

查询重写:

SELECT  
    mt.ID 
FROM 
    [dbo].[MyTable] AS mt
WHERE 
      (@aVariable       IS NULL OR mt.Date1 >= CAST(@date1 AS DATE))
  AND (@aSecondVariable IS NULL OR mt.Date1 < DATEADD(day, 1, CAST(@date1 AS DATE)))
  AND (@aThirdVariable  IS NULL OR mt.Date2 >= CAST(@date2 AS DATE))
  AND (@aFourtVariable  IS NULL OR mt.Date2 < DATEADD(day, 1, CAST(@date2 AS DATE)))
  AND ( @status IS NULL 
     OR @status <> 2 
     OR ( @status = 2 
          AND NOT EXISTS
              ( SELECT * 
                FROM dbo.MyTable AS mt2
                WHERE mt2.CaseID = mt.CaseID 
                  AND mt2.Date1 > mt.Date1
              )
        ) 
      ) 
OPTION (RECOMPILE) ;
Run Code Online (Sandbox Code Playgroud)
  • about 部分@status可以写得稍微紧凑一些。

我认为这不会对性能产生太大影响(如果有的话),它可能看起来很模糊,但为了完整起见,我也添加了这个版本:

SELECT  
    mt.ID 
FROM 
    [dbo].[MyTable] AS mt
WHERE 
      (@aVariable       IS NULL OR mt.Date1 >= CAST(@date1 AS DATE))
  AND (@aSecondVariable IS NULL OR mt.Date1 < DATEADD(day, 1, CAST(@date1 AS DATE)))
  AND (@aThirdVariable  IS NULL OR mt.Date2 >= CAST(@date2 AS DATE))
  AND (@aFourtVariable  IS NULL OR mt.Date2 < DATEADD(day, 1, CAST(@date2 AS DATE)))
  AND NOT EXISTS
      ( SELECT * 
        FROM dbo.MyTable AS mt2
        WHERE @status = 2 
          AND mt2.CaseID = mt.CaseID 
          AND mt2.Date1 > mt.Date1
      )
OPTION (RECOMPILE) ;
Run Code Online (Sandbox Code Playgroud)