SQL Server 2014:缓慢存储过程执行时间

时间:2015-07-03 07:56:55

标签: sql tsql stored-procedures sql-server-2014 sqlperformance

我有以下表结构:

AuditUserMethods

+---------------+---------------+----------+
|  ColumnName   |   DataType    | Nullable |
+---------------+---------------+----------+
| Id            | INT           | NOT NULL |
| CreatedDate   | DATETIME      | NOT NULL |
| ApiMethodName | NVARCHAR(MAX) | NOT NULL |
| Request       | NVARCHAR(MAX) | NOT NULL |
| Result        | NVARCHAR(MAX) | NOT NULL |
| Method_Id     | INT           | NOT NULL |
| User_Id       | INT           | NULL     |
+---------------+---------------+----------+

AuditUserMethodErrorCodes

+--------------------+----------+----------+
|     ColumnName     | DataType | Nullable |
+--------------------+----------+----------+
| Id                 | INT      | NOT NULL |
| AuditUserMethod_Id | INT      | NOT NULL |
| ErrorCode          | INT      | NOT NULL |
+--------------------+----------+----------+

ID是两个表中的PK。有一对多的关系。 AuditUserMethod可以包含多个AuditUserMethodErrorCodes。因此FK AuditUserMethod_Id

AuditUserMethod_Id表中的CreatedDateAuditUserMethods都有两个非聚簇索引。

该过程的目的是返回基于过滤器的分页结果集。 @PageSize确定要返回的行数,@PageIndex确定要返回的页面。所有其他变量都用于过滤。

返回三个结果集。

  1. 包含AuditUserMethods详细信息
  2. 包含AuditUserMethodErrorCodes详细信息
  3. 包含找到的总行数(例如,如果页面大小为1000且有5000行符合所有条件,则返回5000)。
  4. 存储过程:

    CREATE PROCEDURE [api].[Audit_V1_GetAuditDetails]
    (
        @Users XML = NULL,
        @Methods XML = NULL,
        @ErrorCodes XML = NULL,
        @FromDate DATETIME = NULL,
        @ToDate DATETIME = NULL,
        @PageSize INT = 5,
        @PageIndex INT = 0
    )
    AS
    BEGIN
        DECLARE @UserIds            TABLE   (Id INT)
        DECLARE @MethodNames        TABLE   (Name NVARCHAR(256))
        DECLARE @ErrorCodeIds       TABLE   (Id INT)
    
        DECLARE @FilterUsers        BIT = 0
        DECLARE @FilterMethods      BIT = 0
        DECLARE @FilterErrorCodes   BIT = 0
    
        INSERT @UserIds
           SELECT
               x.y.value('.', 'int')
           FROM 
               @Users.nodes('Ids/x/@i') AS x (y)
    
        INSERT @MethodNames
           SELECT
               x.y.value('.', 'NVARCHAR(256)')
           FROM 
               @Methods.nodes('ArrayOfString/string') AS x (y)
    
        INSERT @ErrorCodeIds
           SELECT
               x.y.value('.', 'int')
           FROM 
               @ErrorCodes.nodes('Ids/x/@i') AS x (y)
    
        IF EXISTS (SELECT TOP 1 0 FROM @UserIds)
           SET @FilterUsers = 1
    
        IF EXISTS (SELECT TOP 1 0 FROM @MethodNames)
           SET @FilterMethods = 1
    
        IF EXISTS (SELECT TOP 1 0 FROM @ErrorCodeIds)
           SET @FilterErrorCodes = 1
    
        DECLARE @StartRow INT = @PageIndex * @Pagesize
    
        DECLARE @PageDataResults TABLE (Id INT,
                                        CreatedDate DATETIME,
                                        ApiMethodName NVARCHAR(256), 
                                        Request NVARCHAR(MAX),
                                        Result NVARCHAR(MAX),
                                        MethodId INT,
                                        UserId INT,
                                        TotalRows INT);
    
        WITH PageData AS
        (
            SELECT
                id AS id
                , createddate AS createddate
                , apimethodname AS apimethodname
                , request AS request
                , result AS result
                , method_id AS method_id
                , user_id AS user_id
                , ROW_NUMBER() OVER (ORDER BY createddate DESC, id DESC) AS row_number
                , COUNT(*) OVER() as TotalRows
            FROM 
                dbo.AuditUserMethods AS aum
            WHERE 
               (@FromDate IS NULL OR 
                (@FromDate IS NOT NULL AND aum.createddate > @FromDate))
               AND (@ToDate IS NULL OR 
                    (@ToDate IS NOT NULL AND aum.createddate < @ToDate))
               AND (@FilterUsers = 0 OR 
                    (@FilterUsers = 1 AND aum.user_id IN (SELECT Id FROM @UserIds)))
               AND (@FilterMethods = 0 OR 
                    (@FilterMethods = 1 AND aum.ApiMethodName IN (SELECT Name FROM @MethodNames)))
               AND (@FiltererRorCodes = 0 OR 
                        (@FiltererRorCodes = 1 
                         AND EXISTS (SELECT 1
                                     FROM AuditUserMethodErrorCodes e
                                     WHERE e.AuditUserMethod_Id = aum.Id
                                       AND e.ErrorCode IN (SELECT Id FROM @ErrorCodeIds)
                                    )
                        )
                   )
        )
    
        INSERT @PageDataResults
            SELECT TOP (@Pagesize)
                PageData.id AS id
                , PageData.createddate AS createddate
                , PageData.apimethodname AS apimethodname
                , PageData.request AS request
                , PageData.result AS result
                , PageData.method_id AS method_id
                , PageData.user_id AS user_id
                , PageData.TotalRows AS totalrows
             FROM 
                 PageData
             WHERE 
                 PageData.row_number > @StartRow
             ORDER BY 
                 PageData.createddate DESC
    
        SELECT 
            Id, CreatedDate, ApiMethodName, Request, Result, MethodId, UserId
        FROM 
            @PageDataResults
    
        SELECT 
            aumec.AuditUserMethod_Id, aumec.ErrorCode
        FROM 
            @PageDataResults ps
        INNER JOIN 
            AuditUserMethodErrorCodes aumec ON ps.Id = aumec.AuditUserMethod_Id
    
        SELECT TOP 1 
            TotalRowsNumberOfReturnedAuditEntries 
        FROM @PageDataResults
    END
    

    AuditUserMethods表包含500,000行,AuditUserMethodErrorCodes包含67843行。

    我正在使用以下参数执行该过程:

    EXEC [api].[Audit_V1_GetAuditDetails]   @Users = N'<Ids><x i="1" /></Ids>'
                                            ,@Methods = NULL
                                            ,@ErrorCodes = N'<Ids />'
                                            ,@FromDate = '2015-02-15 07:18:59.613'
                                            ,@ToDate = '2015-07-02 08:18:59.613'
                                            ,@Pagesize = 5000
                                            ,@PageIndex = 0
    

    存储过程只需2秒钟即可执行并返回5000行。我需要这个存储过程运行得更快,而且我不确定如何改进它。

    根据实际执行计划。相对于批次,CTE占99%。在CTE中,排序占成本的95%:

    Actual Execution Plan

3 个答案:

答案 0 :(得分:1)

我首先声明一些表参数类型。

CREATE TYPE [api].[IdSet] AS TABLE
(
    [Id] INT NOT NULL
);

CREATE TYPE [api].[StringSet] AS TABLE
(
    [Value] NVARCHAR(256) NOT NULL
);

然后我会更改商店程序的签名以使用它们。

注意我还会将总计数作为输出参数而不是单独的结果集返回。

CREATE PROCEDURE [api].[Audit_V2_GetAuditDetails]
(
    @userIds [api].[IdSet] READONLY,
    @methodNames [api].[StringSet] READONLY,
    @errorCodeIds [api].[IdSet] READONLY,
    @fromDate DATETIME = NULL,
    @toDate DATETIME = NULL,
    @pageSize INT = 5,
    @pageIndex INT = 0,
    @totalCount BIGINT OUTPUT
)

我知道您可能仍需要进行XML提取,但如果您在SP之外执行此操作,它将有助于查询规划器。

现在,在SP中,我不会使用@PageDataResults我只得到页面的ID。我也不会使用CTE,这在这种情况下没有帮助。

我将简化查询并运行一次以聚合总计数,然后如果大于0,则再次运行相同的查询以仅返回ID页面。查询的主体将由服务器在内部缓存。

此外,Id'使用ORDER BYOFFSETFETCH扩展名进行分页,

我在下面概述了许多逻辑简化,

CREATE PROCEDURE [api].[Audit_V2_GetAuditDetails]
    (
        @userIds [api].[IdSet] READONLY,
        @methodNames [api].[StringSet] READONLY,
        @errorCodeIds [api].[IdSet] READONLY,
        @fromDate DATETIME = NULL,
        @toDate DATETIME = NULL,
        @pageSize INT = 5,
        @pageIndex INT = 0,
        @totalCount BIGINT OUTPUT
    )
AS

DECLARE @offset INT = @pageSize * @pageIndex; 
DECLARE @filterUsers BIT = 0;
DECLARE @filterMethods BIT = 0;
DECLARE @filterErrorCodes BIT = 0;

IF EXISTS (SELECT 0 FROM @userIds)
    SET @filterUsers = 1;
IF EXISTS (SELECT 0 FROM @methodNames)
    SET @filterMethods = 1;
IF EXISTS (SELECT 0 FROM @errorCodeIds)
    SET @filterErrorCodes = 1;

SELECT
            @totalCount = COUNT_BIG(*)
     FROM
            [dbo].[AuditUserMethods] [aum]
         LEFT JOIN
            @userIds [U]
                ON [U].[Id] = [aum].[user_id]
         LEFT JOIN
            @methodName [M]
                ON [M].[Value] = [aum].[ApiMethodName]
    WHERE
            (
               @fromDate IS NULL
            OR
               [aum].[createddate] > @fromDate
            )
        AND
            (
               @toDate IS NULL
            OR
               [aum].[createddate] < @toDate
            )
        AND
            (
                @filterUsers = 0
            OR
                [U].[Id] IS NOT NULL
            (
        AND
            (
                @filterMethods = 0
            OR
                [M].[Value] IS NOT NULL
            (
        AND
            (
                @filterErrorCodes = 0
            OR 
                (
                    EXISTS(
                        SELECT
                                    1
                            FROM
                                    [dbo].[AuditUserMethodErrorCodes] [e]
                                JOIN
                                    @errorCodeIds [ec]
                                       ON [ec].[Id] = [e].[ErrorCode]
                            WHERE
                                    [e].[AuditUserMethod_Id] = [aum].[Id])
                );

DECLARE @pageIds [api].[IdSet];

IF @totalCount > 0
INSERT @pageIds
SELECT
            [aum].[id]
     FROM
            [dbo].[AuditUserMethods] [aum]
         LEFT JOIN
            @userIds [U]
                ON [U].[Id] = [aum].[user_id]
         LEFT JOIN
            @methodName [M]
                ON [M].[Value] = [aum].[ApiMethodName]
    WHERE
            (
               @fromDate IS NULL
            OR
               [aum].[createddate] > @fromDate
            )
        AND
            (
               @toDate IS NULL
            OR
               [aum].[createddate] < @toDate
            )
        AND
            (
                @filterUsers = 0
            OR
                [U].[Id] IS NOT NULL
            (
        AND
            (
                @filterMethods = 0
            OR
                [M].[Value] IS NOT NULL
            (
        AND
            (
                @filterErrorCodes = 0
            OR 
                (
                    EXISTS(
                        SELECT
                                    1
                            FROM
                                    [dbo].[AuditUserMethodErrorCodes] [e]
                                JOIN
                                    @errorCodeIds [ec]
                                       ON [ec].[Id] = [e].[ErrorCode]
                            WHERE
                                    [e].[AuditUserMethod_Id] = [aum].[Id])
                )
    ORDER BY
            [aum].[createddate] DESC,
            [aum].[id] DESC
        OFFSET @offset ROWS
        FETCH NEXT @pageSize ROWS ONLY;

SELECT 
            [aum].[Id],
            [aum].[CreatedDate],
            [aum].[ApiMethodName],
            [aum].[Request],
            [aum].[Result],
            [aum].[MethodId],
            [aum].[UserId]
    FROM
            [dbo].[AuditUserMethods] [aum]
    JOIN
            @pageIds [i]
                ON [i].[Id] = [aum].[id] 
ORDER BY
            [aum].[createddate] DESC,
            [aum].[id] DESC;

SELECT 
            [aumec].[AuditUserMethod_Id],
            [aumec].[ErrorCode]
    FROM
            [dbo].[AuditUserMethodErrorCodes] [aumec]
        JOIN
            @pageIds [i]
                ON [i].[Id] = [aumec].[AuditUserMethod_Id];

/* The total count is an output parameter */
RETURN 0;

如果这不足以改善,你需要查看查询计划并考虑哪些指数是最优的。

警告 所有代码都是从袖口上写下的,因此,虽然思路正确,但语法可能并不完美。

答案 1 :(得分:1)

(@FromDate IS NULL OR 
            (@FromDate IS NOT NULL AND aum.createddate > @FromDate))

相同
(@FromDate IS NULL OR  aum.createddate > @FromDate)

尝试这样的事情

CREATE PROCEDURE [api].[Audit_V1_GetAuditDetails]
(
    @Users XML = NULL,
    @Methods XML = NULL,
    @ErrorCodes XML = NULL,
    @FromDate DATETIME = NULL,
    @ToDate DATETIME = NULL,
    @PageSize INT = 5,
    @PageIndex INT = 0
)
AS
BEGIN
    DECLARE @UserIds            TABLE   (Id INT)
    DECLARE @MethodNames        TABLE   (Name NVARCHAR(256))
    DECLARE @ErrorCodeIds       TABLE   (Id INT)

    INSERT @UserIds
       SELECT
           x.y.value('.', 'int')
       FROM 
           @Users.nodes('Ids/x/@i') AS x (y)

    INSERT @MethodNames
       SELECT
           x.y.value('.', 'NVARCHAR(256)')
       FROM 
           @Methods.nodes('ArrayOfString/string') AS x (y)

    INSERT @ErrorCodeIds
       SELECT
           x.y.value('.', 'int')
       FROM 
           @ErrorCodes.nodes('Ids/x/@i') AS x (y)

    IF NOT EXISTS (SELECT TOP 1 0 FROM @UserIds)
       INSERT INTO @UserIds values (-1)

    IF NOT EXISTS (SELECT TOP 1 0 FROM @MethodNames)
       INSERT INTO @MethodNames values ('empty')

    IF NOT EXISTS (SELECT TOP 1 0 FROM @ErrorCodeIds)
       INSERT INTO @ErrorCodeIds values (-1)

    IF @FromDate is null 
       @FromDate = '1/1/1900'

    IF @ToDate is null 
       @ToDate = '1/1/2079'

    DECLARE @StartRow INT = @PageIndex * @Pagesize

    DECLARE @PageDataResults TABLE (Id INT,
                                    CreatedDate DATETIME,
                                    ApiMethodName NVARCHAR(256), 
                                    Request NVARCHAR(MAX),
                                    Result NVARCHAR(MAX),
                                    MethodId INT,
                                    UserId INT,
                                    TotalRows INT);

    WITH PageData AS
    (
        SELECT
            id AS id
            , createddate AS createddate
            , apimethodname AS apimethodname
            , request AS request
            , result AS result
            , method_id AS method_id
            , user_id AS user_id
            , ROW_NUMBER() OVER (ORDER BY createddate DESC, id DESC) AS row_number
            , COUNT(*) OVER() as TotalRows
        FROM 
            dbo.AuditUserMethods AS aum
        JOIN @UserIds
          ON (aum.user_id = @UserIds.ID OR @UserIds.ID = -1)
         AND aum.createddate > @FromDate
         AND aum.createddate < @ToDate
        JOIN @MethodNames 
          ON aum.ApiMethodName = @MethodNames.Name 
          OR @MethodNames.Name = 'empty'
        JOIN AuditUserMethodErrorCodes e
          on e.AuditUserMethod_Id = aum.Id 
        JOIN @ErrorCodeIds 
          ON e.ErrorCode = @ErrorCodeIds.ID 
          OR @ErrorCodeIds.ID = -1
    )

答案 2 :(得分:0)

您可以先为您的CTE提供某种索引,这可以通过以下方式完成 - 请参阅/ ** /获取“更改的行”:

WITH PageData AS
(
    SELECT
/**/    TOP 100 PERCENT                                                                 
        id                                              AS id
        ,createddate                                    AS createddate
        ,apimethodname                                  AS apimethodname
        ,request                                        AS request
        ,result                                         AS result
        ,method_id                                      AS method_id
        ,user_id                                        AS user_id
        ,ROW_NUMBER() OVER (ORDER BY createddate DESC, id DESC) AS row_number
        ,COUNT(*) OVER() as TotalRows
    FROM dbo.AuditUserMethods AS aum
    WHERE (@FromDate IS NULL OR (@FromDate IS NOT NULL AND aum.createddate > @FromDate))
    AND (@ToDate IS NULL OR (@ToDate IS NOT NULL AND aum.createddate < @ToDate))
    AND (@FilterUsers = 0 OR (@FilterUsers = 1 AND aum.user_id IN (SELECT Id FROM @UserIds)))
    AND (@FilterMethods = 0 OR (@FilterMethods = 1 AND aum.ApiMethodName IN (SELECT Name FROM @MethodNames)))
    AND 
        (
            @FiltererRorCodes = 0 OR 
                (
                    @FiltererRorCodes = 1 AND EXISTS 
                        (
                            SELECT 1
                            FROM AuditUserMethodErrorCodes e
                            WHERE e.AuditUserMethod_Id = aum.Id
                            AND e.ErrorCode IN (SELECT Id FROM @ErrorCodeIds)
                        )
                )
        )
/**/ORDER BY 
/**/    PageData.createddate 
/**/    ,PageData.row_number
)

我还会尝试在createddate和row_number之间的CTE上更改订单的“顺序”,然后首先是row_number然后是createddate。

然后您将CTE交给下一个流程,因为它已经按照预期的顺序。它可能加快速度。 ORDER BY需要TOP 100 PERCENT。