优化存储过程查询

Optimizing Stored Procedure Query

参见下面的存储过程。

一个应用程序正在使用它进行搜索,接受用户输入的订单并显示它们。大多数时候,它 returns 会在几秒钟内得到预期的结果。但是,当搜索特定供应商时,returns 会在 5-10 秒内得到答案。如果它包括@vendorsOnly = true,那么它非常快。如果@vendorsOnly = 0,那么它很慢。

我试过在 VendorId 上建立索引,这似乎有一段时间的帮助,但又 运行 变慢了。

我认为这里存在优化问题,但我不确定应该做什么。

ALTER PROCEDURE [dbo].[SearchOrderInfos_v21]
(
    @orderId int = null,
    @customerFileNo nvarchar(max) = null,
    @orderDate Date = null,
    @countyId nvarchar(max) = null,
    @clientName nvarchar(max) = null,
    @orderStatusId int = null,
    @vendorId int = null,
    @vendorETA nvarchar(max) = null,
    @dueDate DateTime = null,
    @guid nvarchar(max) = null,
    @orderStatusCsv nvarchar(max) = null,
    @researcherOrManagerId nvarchar(max) = null,
    @vendorsOnly bit = false,
    @researchersOnly bit = false,
    @includeDeletedRows bit = 0,
    @researcherId nvarchar(max) = null,
    @managerId nvarchar(max) = null,
    @allowViewArchive bit = 0
)
AS
BEGIN
    declare @timeZone varchar(25) = 'Eastern Standard Time' -- bake it in!
    SET NOCOUNT ON
        BEGIN
            Select top (Select ts.MaxRecords from TableSettings ts where ts.TableName = 'Order') 
                O.*,
                C.CountyName, 
                CU.DisplayName AS CustomerName, 
                OS.OrderStatus, 
                SR.Code AS SearchRequestCode, 
                IsNull(V.DisplayName, V.CompanyName) AS Vendor,
                V.EmailAddress, 
                O.CustomerDueDate AS DueDate,
                SP.StateProvinceName as StateProvince,
                U.UserName as Manager,
                M.UserName as Researcher
            FROM [dbo].[Order] O 
            left JOIN [dbo].[County] C ON O.CountyId = C.CountyId
            left JOIN [dbo].[Customer] CU ON O.CustomerId = CU.CustomerId
            left JOIN [dbo].[OrderStatus] OS ON O.OrderStatusId = OS.OrderStatusId
            left JOIN [dbo].[Vendor] V ON O.VendorId = V.VendorId
            left JOIN [dbo].[SearchRequest] SR ON O.SearchRequestId = SR.SearchRequestId
            left JOIN StateProvince SP ON SP.StateProvinceId = O.StateProvinceId
            left join AspNetUsers U on U.Id = O.ManagerId
            left join AspNetUsers M on M.Id = O.ResearcherId

            WHERE
            (@vendorsOnly = 0 or o.VendorId is not null) and
            (@researchersOnly = 0 or O.ResearcherId is not null) and
            (@orderId IS NULL OR O.OrderId  = @orderId) AND
            (@customerFileNo IS NULL OR O.CustomerFileNo LIKE '%' + trim(@customerFileNo) + '%') AND
            (@orderDate IS NULL OR Cast(O.OrderDateTime at time zone @timeZone as Date) = @orderDate) AND
            (@countyId IS NULL OR C.CountyId  = @countyId) AND
            (@clientName IS NULL OR CU.DisplayName LIKE '%' + trim(@clientName) + '%') AND
            (@orderStatusId IS NULL OR OS.OrderStatusId = @orderStatusId) AND
            (@vendorId IS NULL OR V.VendorId = @vendorId) AND
            (@vendorETA IS NULL OR O.VendorETA LIKE '%' + trim(@vendorETA) + '%') AND
            (@dueDate IS NULL OR O.CustomerDueDate = @dueDate) and
            (@guid IS NULL OR O.Guid = @guid) and
            (   --If @researcherOrManagerId is not null, then ensure that the order is assigned to the id as Manager or Researcher
                @researcherOrManagerId IS NULL or 
                (
                    O.ResearcherId = @researcherOrManagerId or 
                    O.ManagerId = @researcherOrManagerId or 
                    -- if @allowViewArchive is true, then allow to view if the Order in a status of Client Delivered.
                    (@allowViewArchive = 1 and O.OrderStatusId = 4) ) 
                
            ) and
            (@researcherId is null or O.ResearcherId = @researcherId) and
            (@managerId is null or O.ManagerId = @managerId) and
            (@orderStatusCsv IS NULL or O.OrderStatusId in (Select value from STRING_SPLIT(@orderStatusCsv, ','))) and
            (@includeDeletedRows = 1 or O.DeletedDate is null)
    
            --order by OrderId -- For testing only       
            ORDER BY OS.SortOrder, O.OrderId DESC
        end
END

如评论中所述,“修复”此问题的最简单方法是在语句末尾添加 OPTION (RECOMPILE),在这种情况下,这将在 ORDER BY 之后:

...
            ORDER BY OS.SortOrder, O.OrderId DESC
            OPTION (RECOMPILE);
        END
END

不过,也如前所述,如果此 SP 被 运行 频繁使用,或者生成计划本身成本很高,那么使用动态方法会更好,但这是一个复杂得多的解决方案。根据您的查询,我相信以下是正确的实现,但我无法对此进行测试。您将需要使用您的“最好的朋友”来帮助您:

ALTER PROCEDURE [dbo].[SearchOrderInfos_v21] (@orderId int = NULL,
                                              @customerFileNo nvarchar(MAX) = NULL,
                                              @orderDate date = NULL,
                                              @countyId nvarchar(MAX) = NULL,
                                              @clientName nvarchar(MAX) = NULL,
                                              @orderStatusId int = NULL,
                                              @vendorId int = NULL,
                                              @vendorETA nvarchar(MAX) = NULL,
                                              @dueDate datetime = NULL,
                                              @guid nvarchar(MAX) = NULL,
                                              @orderStatusCsv nvarchar(MAX) = NULL,
                                              @researcherOrManagerId nvarchar(MAX) = NULL,
                                              @vendorsOnly bit = false,
                                              @researchersOnly bit = false,
                                              @includeDeletedRows bit = 0,
                                              @researcherId nvarchar(MAX) = NULL,
                                              @managerId nvarchar(MAX) = NULL,
                                              @allowViewArchive bit = 0)
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @timeZone varchar(25) = 'Eastern Standard Time'; -- bake it in!

    DECLARE @SQL nvarchar(MAX),
            @CRLF nchar(2) = NCHAR(13) + NCHAR(10);

    SET @SQL = N'SELECT TOP (SELECT ts.MaxRecords FROM TableSettings ts WHERE ts.TableName = ''Order'')' + @CRLF +
               N'       O.*,' + @CRLF +
               N'       C.CountyName,' + @CRLF +
               N'       CU.DisplayName AS CustomerName,' + @CRLF +
               N'       OS.OrderStatus,' + @CRLF +
               N'       SR.Code AS SearchRequestCode,' + @CRLF +
               N'       ISNULL(V.DisplayName, V.CompanyName) AS Vendor,' + @CRLF +
               N'       V.EmailAddress,' + @CRLF +
               N'       O.CustomerDueDate AS DueDate,' + @CRLF +
               N'       SP.StateProvinceName AS StateProvince,' + @CRLF +
               N'       U.UserName AS Manager,' + @CRLF +
               N'       M.UserName AS Researcher' + @CRLF +
               N'FROM [dbo].[Order] O' + @CRLF +
               N'     LEFT JOIN [dbo].[County] C ON O.CountyId = C.CountyId' + @CRLF +
               N'     LEFT JOIN [dbo].[Customer] CU ON O.CustomerId = CU.CustomerId' + @CRLF +
               N'     LEFT JOIN [dbo].[OrderStatus] OS ON O.OrderStatusId = OS.OrderStatusId' + @CRLF +
               N'     LEFT JOIN [dbo].[Vendor] V ON O.VendorId = V.VendorId' + @CRLF +
               N'     LEFT JOIN [dbo].[SearchRequest] SR ON O.SearchRequestId = SR.SearchRequestId' + @CRLF +
               N'     LEFT JOIN StateProvince SP ON SP.StateProvinceId = O.StateProvinceId' + @CRLF +
               N'     LEFT JOIN AspNetUsers U ON U.Id = O.ManagerId' + @CRLF +
               N'     LEFT JOIN AspNetUsers M ON M.Id = O.ResearcherId' + @CRLF +
               N'WHERE O.OrderStatusId = 4' + @CRLF + --This appears to be the only clause that doesn't reference a parameter, so putting it at the start as this makes things far easier
               CONCAT(CASE WHEN @vendorsOnly = 1 THEN N'  AND O.VendorId IS NOT NULL' + @CRLF END,
                      CASE WHEN @researchersOnly = 1 THEN N'  AND O.ResearcherId IS NOT NULL' + @CRLF END,
                      CASE WHEN @orderId IS NOT NULL THEN N'  AND O.OrderId = @orderId' + @CRLF END,
                      CASE WHEN @customerFileNo IS NOT NULL THEN N'  AND O.CustomerFileNo LIKE ''%'' + trim(@customerFileNo) + ''%''' + @CRLF END,
                      CASE WHEN @orderDate IS NOT NULL THEN N'  AND CAST(O.OrderDateTime AT TIME ZONE @timeZone AS Date) = @orderDate' + @CRLF END, --Warning, this isn't going to be SARGable,
                                                                                                                                                     --you should do use AT TIME ZONE on @orderDate, not O.OrderDateTime
                      CASE WHEN @countyId IS NOT NULL THEN N'  AND C.CountyId = @countyId' + @CRLF END,
                      CASE WHEN @clientName IS NOT NULL THEN N'  AND CU.DisplayName LIKE ''%'' + TRIM(@clientName) + ''%''' + @CRLF END,
                      CASE WHEN @orderStatusId IS NOT NULL THEN N'  AND OS.OrderStatusId = @orderStatusId' + @CRLF END,
                      CASE WHEN @vendorId IS NOT NULL THEN N'  AND V.VendorId = @vendorId' + @CRLF END,
                      CASE WHEN @vendorETA IS NOT NULL THEN N'  AND O.VendorETA LIKE ''%'' + TRIM(@vendorETA) + ''%''' + @CRLF END,
                      CASE WHEN @dueDate IS NOT NULL THEN N'  AND O.CustomerDueDate = @dueDate' + @CRLF END,
                      CASE WHEN @guid IS NOT NULL THEN N'  AND O.Guid = @guid' + @CRLF END,
                      CASE WHEN @researcherOrManagerId IS NOT NULL AND @allowViewArchive = 0 THEN N'  AND (O.ResearcherId = @researcherOrManagerId OR O.ManagerId = @researcherOrManagerId)' + @CRLF END, --I think the WHEN is correct
                      CASE WHEN @researcherId IS NOT NULL THEN N'  AND O.ResearcherId = @researcherId' + @CRLF END,
                      CASE WHEN @managerId IS NOT NULL THEN N'  AND O.ManagerId = @managerId' + @CRLF END,
                      CASE WHEN @orderStatusCsv IS NOT NULL THEN N'  AND O.OrderStatusId IN (SELECT value FROM STRING_SPLIT(@orderStatusCsv, '',''))' + @CRLF END,
                      CASE WHEN @includeDeletedRows = 0 THEN N'  AND O.DeletedDate IS NULL' + @CRLF END) +
              N'ORDER BY OS.SortOrder,' + @CRLF +
              N'         O.OrderId DESC;';

    --PRINT @SQL; --Your debugging best friend

    DECLARE @Params nvarchar(MAX) = N'@orderId int,
                                      @customerFileNo nvarchar(MAX),
                                      @orderDate date,
                                      @countyId nvarchar(MAX),
                                      @clientName nvarchar(MAX),
                                      @orderStatusId int,
                                      @vendorId int,
                                      @vendorETA nvarchar(MAX),
                                      @dueDate datetime,
                                      @guid nvarchar(MAX),
                                      @orderStatusCsv nvarchar(MAX),
                                      @researcherOrManagerId nvarchar(MAX),
                                      @vendorsOnly bit,
                                      @researchersOnly bit,
                                      @includeDeletedRows bit,
                                      @researcherId nvarchar(MAX),
                                      @managerId nvarchar(MAX),
                                      @allowViewArchive bit';
    EXEC sys.sp_executesql @SQL,
                           @Params, 
                           @orderId,
                           @customerFileNo,
                           @orderDate,
                           @countyId,
                           @clientName,
                           @orderStatusId,
                           @vendorId,
                           @vendorETA,
                           @dueDate,
                           @guid,
                           @orderStatusCsv,
                           @researcherOrManagerId,
                           @vendorsOnly,
                           @researchersOnly,
                           @includeDeletedRows,
                           @researcherId,
                           @managerId,
                           @allowViewArchive;
                           
END;