存储过程执行时间超过 30 秒

Stored procedure takes more than 30 seconds to execute

我正在优化存储过程,其中我在一个 table 变量中获取超过 10K 用户 ID 的列表作为参数。如果 table 变量中有任何数据,我将设置标志 @ContainsUserIds。在主查询中,如果在 table 变量中找不到数据,则选择 table 变量中存在的所有用户或所有用户(where 子句中有更多条件)。

这里的问题是where子句中有OR条件的语句耗时超过30秒。如果我删除第一个条件以检查 @ContainsUserIds = 0,那么它将在一秒钟内执行。有人可以帮我优化这个查询吗?

此存储过程是从不同位置调用的,因此用户 ID 可能无法通过。

CREATE PROCEDURE [core].[spGetUsersByFilter]      
(      
  @ClientId                nvarchar(38)  = NULL,        
  @UserIds                 [UserIdList] readonly      
)      
AS      
BEGIN   
    SET NOCOUNT ON         
      
    DECLARE @ContainsUserIds BIT = 0,
    SET @ContainsUserIds = CASE 
                                WHEN EXISTS(SELECT TOP 1 1 FROM @UserIds) 
                                    THEN 1 
                                ELSE 0 
                            END;   

    SELECT DISTINCT ES.Id
    FROM [db].[Users] E
    JOIN [db].[UserDetails] ES ON ES.UserId = E.Id 
    WHERE E.[Active] = 1  
         AND (@ContainsUserIds = 0 OR E.UserId IN(SELECT Item FROM @UserIds)) 
         AND ES.ClientId = @ClientId
END 

SQL 众所周知不喜欢 OR,优化器有时会绕过它,但在大多数情况下,将事物转换为 UNION [ALL] 语法是个好主意。如果只有 1 个 OR 通常很容易做到,如果有多个则很快爆炸。

无论如何,您可以将存储过程转换为:

CREATE PROCEDURE [core].[spGetUsersByFilter]      
(      
  @ClientId                nvarchar(38)  = NULL,        
  @UserIds                 [UserIdList] readonly      
)      
AS      
BEGIN   
    SET NOCOUNT ON         
      
    DECLARE @ContainsUserIds BIT = 0,
    SET @ContainsUserIds = CASE 
                                WHEN EXISTS(SELECT * FROM @UserIds) 
                                    THEN 1 
                                ELSE 0 
                            END;   

    SELECT DISTINCT ES.Id
    FROM [db].[Users] E
    JOIN [db].[UserDetails] ES ON ES.UserId = E.Id 
    WHERE E.[Active] = 1  
         AND (@ContainsUserIds = 0) 
         AND ES.ClientId = @ClientId
         
         
    UNION ALL
    
    SELECT DISTINCT ES.Id
    FROM [db].[Users] E
    JOIN [db].[UserDetails] ES ON ES.UserId = E.Id 
    WHERE E.[Active] = 1  
         AND (@ContainsUserIds = 1)
         AND E.UserId IN (SELECT Item FROM @UserIds)) 
         AND ES.ClientId = @ClientId
END 

就是说,您也可以在此处拆分并执行 2 个单独的 SELECT。此外,如果 table-variable 包含多个记录,您可能更喜欢使用 temp-table,因为后者允许索引,最重要的是具有统计处理功能,这将产生更好的执行计划。我也更喜欢 JOIN 而不是 IN (),只要确保 JOINing 时没有加倍的值即可。

CREATE PROCEDURE [core].[spGetUsersByFilter]      
(      
  @ClientId                nvarchar(38)  = NULL,        
  @UserIds                 [UserIdList] readonly      
)      
AS      
BEGIN   
    SET NOCOUNT ON         
    
    SELECT DISTINCT Item 
      INTO #UserIds
      FROM @UserIds
      
    IF @@ROWCOUNT = 0
        BEGIN
            
            SELECT DISTINCT ES.Id
            FROM [db].[Users] E
            JOIN [db].[UserDetails] ES ON ES.UserId = E.Id 
            WHERE E.[Active] = 1                 
              AND ES.ClientId = @ClientId;
              
        END
    ELSE
        BEGIN
            CREATE UNIQUE INDEX uq0_UserIds ON #UserIds ( Item ) WITH (FILLFACTOR = 100);
            
            SELECT DISTINCT ES.Id
            FROM [db].[Users] E
            JOIN [db].[UserDetails] ES ON ES.UserId = E.Id 
            JOIN #UserIds T ON T.Item = E.UserId
            WHERE E.[Active] = 1  
                 AND ES.ClientId = @ClientId
        END
END 

PS: 以上都是用记事本写的,未经测试,可能需要一些汇编=)