查询性能调优


9

寻求帮助来改善此查询性能。

SQL Server 2008 R2 Enterprise,最大RAM 16 GB,CPU 40,最大并行度4。

SELECT DsJobStat.JobName AS JobName
    , AJF.ApplGroup AS GroupName
    , DsJobStat.JobStatus AS JobStatus
    , AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
    , AVG(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 
AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         
GROUP BY DsJobStat.JobName
, AJF.ApplGroup
, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

执行消息,

(0 row(s) affected)
Table 'AJF'. Scan count 11, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 2, logical reads 1926, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 1, logical reads 3831235, physical reads 85, read-ahead reads 3724396, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

SQL Server Execution Times:
      CPU time = 67268 ms,  elapsed time = 90206 ms.

表的结构:

-- 212271023 rows
CREATE TABLE [dbo].[DsJobStat](
    [OrderID] [nvarchar](8) NOT NULL,
    [JobNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [TaskType] [nvarchar](255) NULL,
    [JobName] [nvarchar](255) NOT NULL,
    [StartTime] [datetime] NULL,
    [EndTime] [datetime] NULL,
    [NodeID] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [CompStat] [int] NULL,
    [RerunCounter] [int] NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
    [CpuMSec] [int] NULL,
    [ElapsedSec] [int] NULL,
    [StatusReason] [nvarchar](255) NULL,
    [NumericOrderNo] [int] NULL,
CONSTRAINT [PK_DsJobStat] PRIMARY KEY CLUSTERED 
(   [OrderID] ASC,
    [JobNo] ASC,
    [Odate] ASC,
    [JobName] ASC,
    [RerunCounter] ASC
));

-- 48992126 rows
CREATE TABLE [dbo].[AJF](  
    [JobName] [nvarchar](255) NOT NULL,
    [JobNo] [int] NOT NULL,
    [OrderNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [SchedTab] [nvarchar](255) NULL,
    [Application] [nvarchar](255) NULL,
    [ApplGroup] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [NodeID] [nvarchar](255) NULL,
    [Memlib] [nvarchar](255) NULL,
    [Memname] [nvarchar](255) NULL,
    [CreationTime] [datetime] NULL,
CONSTRAINT [AJF$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC,
    [JobNo] ASC,
    [OrderNo] ASC,
    [Odate] ASC
));

-- 413176 rows
CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [JobStatus] [nvarchar](255) NULL,
    [ElapsedSecAVG] [float] NULL,
    [CpuMSecAVG] [float] NULL
);

CREATE NONCLUSTERED INDEX [DJS_Dashboard_2] ON [dbo].[DsJobStat] 
(   [JobName] ASC,
    [Odate] ASC,
    [StartTime] ASC,
    [EndTime] ASC
)
INCLUDE ( [OrderID],
[JobNo],
[NodeID],
[GroupName],
[JobStatus],
[CpuMSec],
[ElapsedSec],
[NumericOrderNo]) ;

CREATE NONCLUSTERED INDEX [Idx_Dashboard_AJF] ON [dbo].[AJF] 
(   [OrderNo] ASC,
[Odate] ASC
)
INCLUDE ( [SchedTab],
[Application],
[ApplGroup]) ;

CREATE NONCLUSTERED INDEX [DsAvg$JobName] ON [dbo].[DsAvg] 
(   [JobName] ASC
)

执行计划:

https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM


得到答复后更新

非常感谢@Joe Obbish

您对这个查询的问题是正确的,该问题在DsJobStat和DsAvg之间。与如何加入和不使用NOT IN无关。

正如您所猜测的,确实有一张桌子。

CREATE TABLE [dbo].[DSJobNames](
    [JobName] [nvarchar](255) NOT NULL,
 CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC
) ); 

我尝试了你的建议

SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, Avg(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat
INNER JOIN DSJobNames jn
    ON jn.[JobName]= DsJobStat.[JobName]
INNER JOIN AJF 
    ON DsJobStat.Odate=AJF.Odate 
    AND DsJobStat.NumericOrderNo=AJF.OrderNo 
WHERE NOT EXISTS ( SELECT 1 FROM [DsAvg] WHERE jn.JobName =  [DsAvg].JobName )      
GROUP BY DsJobStat.JobName, AJF.ApplGroup, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;   

执行信息:

(0 row(s) affected)
Table 'DSJobNames'. Scan count 5, logical reads 1244, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 5, logical reads 2129, physical reads 0, read-ahead reads 24, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 8, logical reads 84, physical reads 0, read-ahead reads 83, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AJF'. Scan count 5, logical reads 757999, physical reads 944, read-ahead reads 757311, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 21776 ms,  elapsed time = 33984 ms.

执行计划:https//www.brentozar.com/pastetheplan/?id = rJVkLSZ7f


如果您无法更改供应商代码,那么最好的办法就是与供应商一起发起支持事件,这可能会很痛苦,并且会因需要大量读取才能完成的查询而打败他们。引用具有41.3万行的表中的值的NOT IN子句是次优的。DSJobStat上的索引扫描将返回2.12亿行,其中最多冒出了2.12亿个嵌套循环,您可以看到2.12亿行的计数是成本的83%。我认为如果不重写查询或清除数据,您将无济于事...
Tony Hinkle

我不明白,埃文(Evan)的建议为什么没有首先对您有所帮助,除了解释,这两个答案都是相同的。此外,我看不到您完全实现了这两个家伙的建议。乔使这个问题很有趣。
KumarHarsh

Answers:


11

让我们从考虑加入顺序开始。您在查询中有三个表引用。哪种加入顺序可能会为您带来最佳效果?查询优化器认为,从DsJobStat到的DsAvg联接将消除几乎所有行(基数估计从212195000降至1行)。实际计划向我们显示,估计值与实际情况非常接近(在连接中保留了11行)。但是,该连接是作为正确的反半合并连接实现的,因此DsJobStat表中的所有2.12亿行仅被扫描以产生11行。这肯定会导致较长的查询执行时间,但是我无法想到一个更好的物理或逻辑运算符来实现更好的连接。我确定DJS_Dashboard_2index用于其他查询,但是所有额外的键和包含的列将仅需要更多IO用于此查询,并降低速度。因此,表的索引扫描可能会导致表访问问题DsJobStat

我将假设对join的AJF选择性不是很高。当前,它与您在查询中看到的性能问题无关,因此在本答案的其余部分中,我将忽略它。如果表中的数据更改,那可能会更改。

从计划中可以明显看出的另一个问题是行数假脱机运算符。这是一个非常轻量级的运算符,但是它执行了2亿多次。运算符在那里,因为查询是用编写的NOT IN。如果其中只有一个NULL行,DsAvg则必须消除所有行。假脱机是该检查的实现。那可能不是您想要的逻辑,因此最好编写该部分以供使用NOT EXISTS。重写的实际好处将取决于您的系统和数据。

我根据查询计划模拟了一些数据,以测试一些查询重写。我的表定义与您的表定义显着不同,因为要为每个单独的列模拟数据会花费很多精力。即使使用了简化的数据结构,我也能够重现您遇到的性能问题。

CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL
);

CREATE CLUSTERED INDEX CI_DsAvg ON [DsAvg] (JobName);

INSERT INTO [DsAvg] WITH (TABLOCK)
SELECT TOP (200000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE [dbo].[DsJobStat](
    [JobName] [nvarchar](255) NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
);

CREATE CLUSTERED INDEX CI_JobStat ON DsJobStat (JobName)

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT [JobName], 'ACTIVE'
FROM [DsAvg] ds
CROSS JOIN (
SELECT TOP (1000) 1
FROM master..spt_values t1
) c (t);

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT TOP (1000) '200001', 'ACTIVE'
FROM master..spt_values t1;

根据查询计划,我们可以看到表中大约有200000个唯一JobNameDsAvg。根据联接到该表之后的实际行数,我们可以看到表中的几乎所有JobNameDsJobStat也都在DsAvg表中。因此,该DsJobStat表的JobName列具有200001个唯一值,每个值具有1000行。

我相信此查询代表性能问题:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] );

查询计划中的所有其他内容(GROUP BY,,HAVING古代风格的连接等)都在结果集减少到11行之后发生。从查询性能的角度来看,目前这无关紧要,但是表中的数据更改可能会揭示其他问题。

我正在SQL Server 2017中进行测试,但是得到的基本计划形状与您相同:

计划前

在我的机器上,该查询需要62219 ms的CPU时间和65576 ms的执行时间。如果我重写查询以使用NOT EXISTS

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE DsJobStat.JobName = [DsAvg].JobName);

无线轴

后台打印程序不再执行2.12亿次,它可能具有供应商的预期行为。现在,查询将在34516 ms的CPU时间和41132 ms的经过时间中执行。大多数时间都花在从索引扫描2.12亿行上。

对于该查询而言,索引扫描是非常不幸的。平均而言,每个的唯一值有1000行JobName,但是在读取了第一行之后我们知道是否需要前面的1000行。我们几乎永远不需要这些行,但是无论如何我们仍然需要对其进行扫描。如果我们知道表中的行不是很密集,并且几乎所有行都将通过联接消除,那么我们可以想象索引上可能存在更有效的IO模式。如果SQL Server读取每个唯一值的第一行JobName,检查该值是否在中DsAvg,然后直接跳到下一个值,JobName该怎么办?代替扫描2.12亿行,可以执行需要大约20万次执行的查找计划。

这可以主要通过使用递归与技术沿着保罗·怀特率先所描述的来完成。我们可以使用递归来执行我上面描述的IO模式:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        [JobName]
    FROM dbo.DsJobStat AS T
    ORDER BY
        T.[JobName]

    UNION ALL

    -- Recursive
    SELECT R.[JobName]
    FROM
    (
        -- Number the rows
        SELECT 
            T.[JobName],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[JobName])
        FROM dbo.DsJobStat AS T
        JOIN RecursiveCTE AS R
            ON R.[JobName] < T.[JobName]
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT js.*
FROM RecursiveCTE
INNER JOIN dbo.DsJobStat js ON RecursiveCTE.[JobName]= js.[JobName]
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE RecursiveCTE.JobName = [DsAvg].JobName)
OPTION (MAXRECURSION 0);

该查询很多,因此我建议仔细检查实际计划。首先,我们进行200002索引搜索,DsJobStat以获取所有唯一JobName值。然后,我们加入DsAvg并消除除第一行外的所有行。对于剩余的行,返回DsJobStat并获得所有必需的列。

IO模式完全改变。在我们得到这个之前:

表“ DsJobStat”。扫描计数1,逻辑读1091651,物理读13836,预读181966

通过递归查询,我们得到以下信息:

表“ DsJobStat”。扫描计数200003,逻辑读取1398000,物理读取1,预读读取7345

在我的机器上,新查询仅在6891毫秒的CPU时间和7107毫秒的经过时间中执行。请注意,需要以这种方式使用递归表明数据模型中缺少某些内容(或者在发布的问题中只是未声明)。如果存在一个包含所有可能的较小表,JobNames则最好使用该表,而不是在大表上进行递归。归结为如果您的结果集包含所需的所有内容,JobNames则可以使用索引查找来获取其余缺少的列。但是,您无法获得不需要的结果集JobNames


我建议了NOT EXISTS。他们已经回答“在发布问题之前,我已经尝试了两种方法,加入并且不存在。差异不大。”
埃文·卡罗尔

1
我很想知道递归的想法是否可行,这真是可怕。
埃文·卡罗尔

我认为不要求具有该子句。where子句中将使用“ ElapsedSec不为空”。此外,我认为不需要递归CTE。查询)。关于我的想法您要说些什么?
KumarHarsh

@Joe Obbish,我更新了我的帖子。非常感谢。
温迪

是的,递归CTE输出执行row_number()over(按作业名称按名称分区)按了1分钟。但是与此同时,我没有看到使用示例数据在递归CTE中获得任何额外收益。
KumarHarsh

0

看看如果重写条件会发生什么,

AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         

AND NOT EXISTS ( SELECT 1 FROM [DsAvg] AS d WHERE d.JobName = DsJobStat.JobName )

还考虑重写您的SQL89联接,因为这种样式很糟糕。

代替

FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 

尝试

FROM DsJobStat
INNER JOIN AJF ON (
  DsJobStat.NumericOrderNo=AJF.OrderNo 
  AND DsJobStat.Odate=AJF.Odate
)

我还怀疑可以更好地写出这种情况,但我们必须更多地了解正在发生的事情

HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

您是否真的必须知道平均值不为零,或者只是该组中的一个元素不为零?


@EvanCarroll。在发布问题之前,我已经尝试了两种方法,即加入但不存在。没有太大的区别。
温迪
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.