2015-07-03 85 views
3

我有以下表结构:的SQL Server 2014:慢的存储过程的执行时间

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

AuditUserMethods表中的AuditUserMethod_IdCreatedDate上都有两个非聚簇索引。

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

返回三个结果集。

  1. 包含的AuditUserMethods详细
  2. 包含AuditUserMethodErrorCodes详细
  3. 包含发现总行(即,如果页面大小为1000并且有匹配所有标准5000行,这将返回5000) 。

存储过程:

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表包含50万行数据和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%。在热膨胀系数,它是占用了95%的成本排序:

Actual Execution Plan

+0

是执行迅速在第一时间(在服务器重新启动后或在运行DBCC'FREEPROCCACHE'),然后当你改变参数时会慢下来吗?或者,它只是缓慢? – Jodrell

+0

即使释放proc缓存后,它似乎仍然相当一致。 – JBond

+0

我终于完成编辑我的答案http://stackoverflow.com/a/31202079/659190 – Jodrell

回答

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,再次运行相同的查询以仅返回ids页面。查询的主体将被服务器内部缓存。

此外,ID为”做寻呼与OFFSETFETCH扩展ORDER BY

有一个数字,我在下面概括逻辑简化,

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; 

如果不能改善的事情足够了,您需要查看查询计划并考虑哪些索引是最优的。

买者所有的代码写入即兴,所以,虽然想法是正确的语法可能不是完美的。

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 
) 

我也与实验通过对CTE改变createddate然后ROW_NUMBER之间,然后ROW_NUMBER第一和然后createddate的顺序的“命令”。

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

+2

这些技巧被称为“中间物化CTE”,并需要额外的排序。我个人更喜欢在(本地)临时表中“物化”。增加的好处是这些提供了统计信息,并且在某些情况下,您的查询可以从添加索引(或多个)中受益。 –

+0

@TT我有时会使用表变量路由,特别是当SQL给你延迟加载性能混淆时,其中表变量或临时表在你期望的时候完成。在这种情况下,我认为只需要一个“索引”,所以CTE可以很好地工作,并且只需很少的返工。感谢您的评论。 –

+0

'TOP 100 PERCENT'将会被优化出来并且不起作用。 –

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 
    ) 
+0

我喜欢这个想法。它在WHERE子句中看起来更清晰。至于OR语句是相同的。我不确定SQL是否支持短路操作。 – JBond

+0

问题在哪里,你可以得到一个大连接,然后过滤。这个想法是为了帮助查询优化器尽早过滤。它不是一个真正的短路 - 与null的比较无论如何都会返回一个错误。 – Paparazzi

相关问题