クエリのパフォーマンスチューニング


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の間にあるこのクエリの問題については、あなたは正しいです。どのようにJOINし、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,200万行を返します。これは、最大2億1,200万の入れ子になったループであり、2億1,200万行の数がコストの83%であることがわかります。クエリを書き直したり、データ
Tony Hinkle

私は理解していません、なぜエヴァンの提案が最初からあなたを助けなかったのか、両方の答えは説明を除いて同じです。また、これら両方の人があなたに示唆したことを完全に実装したこともわかりません。ジョーはこの質問を興味深いものにしました。
KumarHarsh 2017

回答:


11

結合順序を検討することから始めましょう。クエリに3つのテーブル参照があります。最高のパフォーマンスが得られる結合順序はどれですか。クエリオプティマイザーは、結合元DsJobStatto DsAvgによってほとんどすべての行が削除されると考えています(カーディナリティの推定値は212195000から1行に下がります)。実際の計画では、推定値が現実にかなり近いことが示されています(11行は結合に耐えます)。ただし、結合は正しいアンチセミマージ結合として実装されているため、DsJobStatテーブルから2億1100万行すべてがスキャンされ、11行が生成されます。それは確かに長いクエリ実行時間に寄与している可能性がありますが、私はその結合のためのより良い物理的または論理的演算子を考えることはできません。きっとDJS_Dashboard_2インデックスは他のクエリで使用されますが、追加のキーと含まれている列はすべて、このクエリでより多くのIOが必要になり、速度が低下します。したがって、テーブルのインデックススキャンでテーブルアクセスの問題が発生する可能性がありDsJobStatます。

結合先AJFはあまり選択的ではないと仮定します。現在、クエリに表示されるパフォーマンスの問題とは関係がないため、この回答の残りの部分では無視します。テーブルのデータが変更されると、それは変わる可能性があります。

計画から明らかなもう1つの問題は、行カウントスプールオペレーターです。これは非常に軽量なオペレーターですが、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の一意のJobName値があることがわかりDsAvgます。そのテーブルへの結合後の実際の行数に基づいて、ほとんどすべての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 BYHAVING結果セットが11行に減少された後、古代のスタイルが参加する、など)が起こります。現在、クエリのパフォーマンスの観点からは問題ありませんが、テーブル内のデータの変更によって明らかになる可能性のある他の懸念事項が存在する可能性があります。

私はSQL Server 2017でテストしていますが、基本的な計画の形はあなたと同じです:

計画前

私のマシンでは、そのクエリの実行に62219ミリ秒のCPU時間と65576ミリ秒の経過時間がかかります。使用するようにクエリを書き換えた場合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億1200万回実行されなくなったため、おそらくベンダーの意図した動作をしています。これで、クエリは34516ミリ秒のCPU時間と41132ミリ秒の経過時間で実行されます。ほとんどの時間は、インデックスから2億1200万行をスキャンするのに費やされます。

そのインデックススキャンは、そのクエリにとって非常に残念です。の一意の値ごとに平均して1000行JobNameありますが、最初の行を読んだ後、先行する1000行が必要かどうかがわかります。これらの行はほとんど必要ありませんが、とにかくそれらをスキャンする必要があります。テーブルの行の密度がそれほど高くなく、結合によってほとんどすべての行が削除されることがわかっている場合は、インデックスでのより効率的なIOパターンを想像できます。SQL Serverがの一意の値ごとに最初の行を読み取り、JobNameその値がにあるかどうかを確認DsAvgし、次の値にスキップした場合はJobNameどうなりますか?2億1,200万行をスキャンする代わりに、約20万回の実行を必要とするシークプランを代わりに実行できます。

これは主に、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);

このクエリは多くのことを検討する必要があるため、実際の計画を注意深く検討することをお勧めします。最初DsJobStatに、すべての一意のJobName値を取得するために、200002インデックスをインデックスに対して検索します。次にDsAvg、1つを除くすべての行に結合して削除します。残りの行については、結合しDsJobStatて必要な列をすべて取得します。

IOパターンは完全に変化します。これを入手する前に:

テーブル 'DsJobStat'。スキャンカウント1、論理読み取り1091651、物理読み取り13836、先読み読み取り181966

再帰クエリでは、次のようになります。

テーブル 'DsJobStat'。スキャン数200003、論理読み取り1398000、物理読み取り1、先読み読み取り7345

私のマシンでは、新しいクエリは6891ミリ秒のCPU時間と7107ミリ秒の経過時間で実行されます。この方法で再帰を使用する必要があることは、データモデルに何かが欠落していることを示唆していることに注意してください(または投稿された質問に単に記載されていなかった可能性があります)。可能なすべてを含む比較的小さなテーブルがある場合JobNames、大きなテーブルでの再帰とは対照的に、そのテーブルを使用する方がはるかに良いでしょう。つまり、結果セットにJobNames必要なものがすべて含まれている場合、インデックスシークを使用して、欠落している残りの列を取得できます。ただし、必要のない結果セットを使用しJobNamesてそれを行うことはできません。


提案したNOT EXISTS。彼らはすでに「私は質問を投稿する前に、私はすでに参加し、存在しない両方を試しました。あまり大きな違いはありませんでした」と答えました。
エヴァンキャロル

1
再帰的なアイデアが機能するかどうか知りたいのですが、それは恐ろしいことです。
エヴァンキャロル

where句では、「ElapsedSecはnullではない」と考えています。また、再帰CTEは必要ないと考えています。存在しない場合は、row_number()over(partition by jobname order by name)rnを使用できますquery)私のアイデアについて何を言わなければなりませんか?
KumarHarsh 2017

@ジョーオブビッシュ、私は私の投稿を更新しました。どうもありがとう。
ウェンディ、

はい、再帰CTEアウトは、row_number()over(part by jobname order by name)rn by 1 minutesを実行しますが、同時に、サンプルデータを使用した再帰CTEに余分な利益は見られませんでした。
KumarHarsh 2017

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;

平均がゼロではないこと、またはグループの1つの要素がゼロでないことを本当に知っている必要がありますか?


@EvanCarroll。質問を投稿する前に、私はすでに参加していて、存在していないことを両方試しました。大した違いはありません。
ウェンディ、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.