进行扫描,尽管我希望进行搜索


9

我需要优化一条SELECT语句,但是SQL Server总是执行索引扫描而不是查找。这是查询,它当然在存储过程中:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

这是索引:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

计划:

平面图

为什么SQL Server选择扫描?我该如何解决?

列定义:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

状态参数可以是:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser可以是:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)

您可以将实际的执行计划发布到某处(不是照片,而是XML格式的.sqlplan文件)吗?我的猜测是您更改了过程,但实际上并未在语句级别获得新的编译。您是否可以更改查询的某些文本(例如将模式前缀添加到表名),然后为传入有效值@Status
亚伦·伯特兰

1
索引定义也引出了一个问题-为什么键在Status DESC?有多少个值Status,它们是什么(如果数量很小),并且每个值大致相等地表示吗?向我们显示SELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Answers:


11

我不认为扫描是由搜索空字符串引起的(尽管您可以为这种情况添加过滤索引,但这只会帮助非常具体的查询变体)。您很可能会成为参数嗅探的受害者,并且单个计划没有针对要提供给该查询的所有各种参数组合(和参数值)进行优化。

我将此称为“厨房水槽”过程,因为您希望通过一个查询来提供所有东西,包括厨房水槽。

在这里这里都有关于我的解决方案的视频,以及关于它的博客文章,但是从本质上讲,我对此类查询的最佳体验是:

  • 动态地构建语句 -这将使您省去提及没有提供任何参数的列的子句,并确保您将针对针对随值传递的实际参数进行精确优化的计划。
  • 使用OPTION (RECOMPILE) -这可以防止特定的参数值强制使用错误的计划类型,特别是在数据偏斜,统计数据错误或第一次执行语句使用非典型值(其导致的计划不同于以后的计划)时更有用处决。
  • 使用服务器选项optimize for ad hoc workloads -这可以防止仅使用一次的查询变体污染您的计划缓存。

启用针对临时工作负载的优化:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

更改程序:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

一旦基于可监视的查询集获得了工作负载,就可以分析执行并查看哪些最能从附加或不同的索引中受益-您可以从多种角度,从简单的“将哪些组合最常提供参数?” “哪些查询的运行时间最长?” 我们不能仅根据您的代码回答这些问题,我们只能建议任何索引仅对您尝试支持的所有可能参数组合的子集有用。例如,如果@Status如果为NULL,则无法针对该非聚集索引进行查找。因此,对于那些用户不关心状态的情况,将进行扫描,除非您有一个满足其他子句的索引(但鉴于当前的查询逻辑,这种索引也将无用) -空字符串或非空字符串都不完全是选择性的)。

在这种情况下,根据可能Status值的集合以及这些值的分布程度,OPTION (RECOMPILE)可能不需要这样做。但是,如果您确实有一些值会产生100行,而有些值会产生成千上万行,那么您可能希望在那儿使用它(即使以CPU成本为代价,考虑到此查询的复杂性,这应该是很小的),因此您可以在尽可能多的情况下进行搜索。如果值的范围足够有限,则您甚至可以使用动态SQL进行一些棘手的操作,在其中您说:“我具有的非常有选择性的值@Status,因此当传递该特定值时,请对查询文本进行此稍许更改,以便这被认为是不同的查询,并且针对该参数值进行了优化。”


3
我已经使用过这种方法很多次了,这是使优化程序按照您认为应该执行的方式进行操作的绝佳方法。Kim Tripp在这里谈论了类似的解决方案: sqlskills.com/blogs/kimberly/high-performance-procedures ,还有一段视频,介绍了她几年前在PASS上进行的一次会议,其中确实详细介绍了其工作原理。就是说,这确实不会使贝特朗先生在这里所说的增加很多。这是每个人都应该保留在工具带中的工具之一。对于那些笼统的查询,它确实可以省去很多麻烦。
mskinner

3

免责声明:此答案中的某些内容可能会使DBA退缩。我是从纯粹的性能角度来解决问题的-当您始终获得索引扫描时如何获得索引查找。

有了这些,就可以了。

您的查询就是所谓的“厨房水槽查询”-单个查询旨在满足一系列可能的搜索条件。如果用户设置@status为一个值,则要过滤该状态。如果@statusNULL,则返回所有状态,依此类推。

这会引入索引问题,但它们与可持久性无关,因为所有搜索条件都“等于”条件。

这是可确定的:

WHERE [status]=@status

不是可保留的,因为SQL Server需要评估ISNULL([status], 0)每一行,而不是在索引中查找单个值:

WHERE ISNULL([status], 0)=@status

我以一种更简单的形式重新创建了厨房水槽问题:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

如果尝试以下操作,即使A是索引的第一列,也将获得“索引扫描”:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

但是,这会产生索引查找:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

只要您使用数量可管理的参数(在您的情况下为两个),您就可能只是UNION一堆搜索查询-基本上是搜索条件的所有排列。如果您有三个条件,那么看起来会很混乱,而四个条件则完全无法管理。您已被警告。

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

对于这四个使用索引查找的索引中的第三个,您将需要在上第二个索引(B, A)。这是这些更改后查询的外观(包括我对查询的重构,使其更具可读性)。

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

...另外,您需要Employee在两个索引列颠倒的情况下打开附加索引。

为了完整起见,我应该提到,x=@x隐含表示x不能为,NULL因为NULL它从不等于NULL。这样可以简化查询。

而且,是的,在大多数情况下(即只要您可以使用重新编译器),Aaron Bertrand的动态SQL答案是一个更好的选择。


3

您的基本问题似乎是“为什么”,我想您可能会几年前TechEdAdam Machanic的精彩演讲中找到第55分钟左右的答案。

我在第55分钟提到了5分钟,但是整个演示值得花时间。如果您查看查询的查询计划,我相信您会发现它具有用于搜索的残留谓词。基本上,SQL无法“看到”索引的所有部分,因为其中的某些部分被不等式和其他条件所隐藏。结果是对基于谓词的超集进行索引扫描。将该结果假脱机,然后使用残留谓词重新扫描。

检查“扫描运算符”(F4)的属性,并查看属性列表中是否同时包含“搜索谓词”和“谓词”。

正如其他人指出的那样,查询很难按原样编制索引。我最近一直在从事许多类似的工作,每个人都需要不同的解决方案。:(


0

在我们质疑索引查找是否比索引扫描更可取之前,一条经验法则是检查返回的行数与基础表的总行数。例如,如果您希望查询返回100万行中的10行,则索引查找可能比索引扫描更受青睐。但是,如果要从查询中返回几千行(或更多),则索引查找不一定是首选。

您的查询并不复杂,因此,如果您可以发布执行计划,我们可能有更好的主意为您提供帮助。


从一百万张表中筛选出几千行,我还是很想找-与扫描整个表相比,这仍然是一个巨大的性能提升。
Daniel Hutmacher '16

-6

这只是原始格式

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

这是修订版本-并非100%肯定,但是(也许)试试看,
即使一次或可能也会成为问题,
这可能会在ActiveDirectoryUser上中断null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )

3
我不清楚此答案如何解决OP的问题。
埃里克

@Erik我们是否可以让OP尝试一下?两个OR消失了。您确定这不能帮助查询性能吗?
狗仔队

@ypercubeᵀᴹIsUserGotAnActiveDirectoryUser IS NOT NULL被删除。这两个不必要的删除OR并删除IsUserGotAnActiveDirectoryUser IS NULL。您确定此查询不会比OP快运行吗?
狗仔队

@ypercubeᵀᴹ本可以做很多事情。我不是在寻找更简单的方法。两个或不见了。或通常不利于查询计划。我到那里是一个俱乐部,但我不属于俱乐部。但是我这样做是为了谋生,然后发布我所知道的工作。我的回答不受投票否决的影响。
狗仔队
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.