쿼리에서 성능 조정


9

이 쿼리 성능을 향상시키는 데 도움을 요청하십시오.

SQL Server 2008 R2 Enterprise , 최대 RAM 16GB, 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


응답 후 업데이트

너무 감사합니다 @ 조 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


변경할 수없는 공급 업체 코드 인 경우 가장 좋은 방법은 공급 업체에 대한 지원 문제를 가능한 한 많이 열어 놓고 많은 읽기를 수행해야하는 쿼리가있는 경우이를 해결하는 것입니다. 413 천 개의 행이있는 테이블의 값을 참조하는 NOT IN 절은 최적이 아닙니다. DSJobStat의 인덱스 스캔은 2 억 1 천 2 백만 행을 반환하며 최대 2 억 2 천 2 백만의 중첩 루프를 버블 링하고 2 억 2 천 2 백만 행 수가 비용의 83 %임을 알 수 있습니다. 쿼리를 다시 작성하거나 데이터를 제거하지 않고서도이를 도울 수 있다고 생각하지 않습니다 ...
Tony Hinkle

이해가 안 돼요, 에반의 제안이 처음부터 당신을 도와주지 않았는데, 둘 다 설명을 제외하고는 모두 똑같습니다.
KumarHarsh

답변:


11

가입 순서를 고려하여 시작하겠습니다. 쿼리에 세 개의 테이블 참조가 있습니다. 어떤 조인 순서가 최고의 성능을 제공 할 수 있습니까? 쿼리 최적화에서 조인 생각 DsJobStatDsAvg(카디널리티 추정치가 1 행에 212,195,000에서 가을) 거의 모든 행을 제거합니다. 실제 계획은 추정치가 실제와 거의 비슷하다는 것을 보여줍니다 (11 행은 결합에서 살아 남음). 그러나 조인은 오른쪽 반반 병합 조인으로 구현되므로 DsJobStat테이블 에서 2 억 2 천 2 백만 행을 모두 스캔하여 11 개의 행을 생성합니다. 그것은 확실히 긴 쿼리 실행 시간에 기여할 수 있지만 더 나은 조인에 대해 더 나은 물리적 또는 논리적 연산자를 생각할 수는 없습니다. 나는 확신합니다DJS_Dashboard_2index는 다른 쿼리에 사용되지만 모든 추가 키 및 포함 된 열은이 쿼리에 더 많은 IO가 필요하므로 속도가 느려집니다. 따라서 테이블의 인덱스 스캔에 테이블 액세스 문제점이있을 수 있습니다 DsJobStat.

조인 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;

쿼리 계획 JobName에 따라 DsAvg테이블 에 약 200000 개의 고유 한 값 이 있음을 알 수 있습니다 . 해당 테이블에 조인 한 후 실제 행 수를 기반으로 거의 모든 JobNameDsJobStatDsAvg테이블 에도 있음을 알 수 있습니다 . 따라서 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에서 테스트하고 있지만 다음과 같은 기본 계획 형태를 갖습니다.

계획하기 전에

내 컴퓨터에서 해당 쿼리는 62219ms의 CPU 시간과 65576ms의 경과 시간이 실행됩니다. 사용할 쿼리를 다시 작성하면 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 억 2 천 2 백만 번 실행되지 않으며 공급 업체의 의도 된 동작을 가지고있을 것입니다. 이제 쿼리는 34516ms의 CPU 시간과 41132ms의 경과 시간으로 실행됩니다. 대부분의 시간은 인덱스에서 2 억 2 천 2 백만 행을 검색하는 데 소비됩니다.

해당 인덱스 스캔은 해당 쿼리에 매우 유감입니다. 평균 고유 JobName값당 1000 개의 행이 있지만 앞의 1000 개의 행이 필요한 경우 첫 번째 행을 읽은 후 알고 있습니다. 우리는 그 행이 거의 필요하지 않지만 어쨌든 스캔해야합니다. 행이 테이블에서 매우 조밀하지 않고 조인으로 거의 모든 행이 제거된다는 것을 알고 있다면 인덱스에서보다 효율적인 IO 패턴을 상상할 수 있습니다. SQL Server가 고유 한 값당 첫 번째 행을 읽고 JobName해당 값이 있는지 확인한 DsAvg다음 다음 값인 JobName경우 바로 건너 뛰면 어떻게됩니까? 2 억 1 천 2 백만 행을 스캔하는 대신 약 2 억 개의 실행을 요구하는 탐색 계획을 수행 할 수 있습니다.

이것은 대부분 Paul White가 여기에 설명 된 기술과 함께 재귀를 사용하여 수행 할 수 있습니다 . 재귀를 사용하여 위에서 설명한 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

내 컴퓨터에서 새 쿼리는 6891ms의 CPU 시간과 7107ms의 경과 시간으로 실행됩니다. 이런 식으로 재귀를 사용해야한다는 것은 데이터 모델에서 무언가가 누락되었음을 나타냅니다 (또는 아마도 게시 된 질문에서 설명되지 않았을 수도 있음). 가능한 모든 테이블이 포함 된 비교적 작은 테이블이있는 경우 JobNames큰 테이블의 재귀와는 반대로 해당 테이블을 사용하는 것이 훨씬 좋습니다. 요약 JobNames하면 필요한 모든 것을 포함하는 결과 집합이 있으면 인덱스 탐색을 사용하여 나머지 열을 얻을 수 있습니다. 그러나 JobNames필요하지 않은 결과 집합으로는 그렇게 할 수 없습니다.


나는 제안했다 NOT EXISTS. 그들은 이미 질문을 게시하기 전에 이미 참여했지만 존재하지 않는 두 가지를 모두 시도했습니다. 별 차이가 없습니다. "
Evan Carroll

1
재귀 아이디어가 작동하는지 알고 싶을 것입니다.
Evan Carroll

나는 절이 필요하지 않다고 생각합니다. "ElapsedSec is null not"where where do will. 또한 재귀 적 CTE가 필요하지 않다고 생각합니다. 당신은 row_number () over (이름으로 작업 이름 순서로 파티션)를 사용할 수 없습니다 (select 내 아이디어에 대해 무엇을 말해야합니까?
KumarHarsh

@ Joe Obbish, 내 게시물을 업데이트했습니다. 고마워
Wendy

예, 재귀 CTE는 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;

실제로 평균이 0이 아니거나 그룹의 한 요소가 0이 아닌 것을 알아야합니까?


@EvanCarroll. 질문을 게시하기 전에 이미 참여하고 존재하지 않는 두 가지 방법을 모두 시도했습니다. 큰 차이는 없습니다.
Wendy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.