このクエリのパフォーマンスを改善するための支援を求めています。
SQL Server2008 R2Enterprise、最大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.
結合順序を検討することから始めましょう。クエリに3つのテーブル参照があります。最高のパフォーマンスが得られる結合順序はどれですか。クエリオプティマイザーは、DsJobStat
からDsAvg
への結合によってほとんどすべての行が削除されると考えています(カーディナリティの推定値は212195000から1行に下がります)。実際の計画では、推定値が現実にかなり近いことが示されています(11行は結合に耐えます)。ただし、この結合は正しい反セミマージ結合として実装されているため、DsJobStat
テーブルの2億1100万行すべてがスキャンされ、11行が生成されます。それは確かに長いクエリ実行時間に寄与している可能性がありますが、私はその結合のためのより優れた物理的または論理的演算子を考えることができません。 DJS_Dashboard_2
インデックスが他のクエリで使用されていると確信していますが、追加のキーと含まれている列はすべて、このクエリに対してより多くのIO=が必要であり、速度が低下します。 DsJobStat
テーブルのインデックススキャンでテーブルアクセスの問題が発生する可能性があります。
AJF
への結合はあまり選択的ではないと仮定します。現在、クエリに表示されるパフォーマンスの問題とは関係がないため、この回答の残りの部分では無視します。テーブルのデータが変更されると、それは変わる可能性があります。
計画から明らかなもう1つの問題は、行カウントスプールオペレーターです。これは非常に軽量なオペレーターですが、2億回以上実行されています。クエリはNOT IN
で記述されているため、演算子はそこにあります。 DsAvg
に単一のNULL行がある場合、すべての行を削除する必要があります。スプールはそのチェックの実装です。それはおそらくあなたが望むロジックではないので、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
テーブルに約200000個の一意のDsAvg
値があることがわかります。そのテーブルへの結合後の実際の行数に基づいて、JobName
のDsJobStat
値のほとんどすべてが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ミリ秒の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万行をスキャンするのに費やされます。
そのクエリでは、インデックススキャンは非常に残念です。 JobName
の一意の値ごとに平均で1000行ありますが、最初の行を読んだ後、先行する1000行が必要かどうかがわかります。これらの行はほとんど必要ありませんが、それでもスキャンする必要があります。テーブルの行の密度がそれほど高くなく、それらのほとんどすべてが結合によって削除されることがわかっている場合、おそらくより効率的なIOパターンがインデックス上にあると想像できます。 SQL Serverは、JobName
の一意の値ごとに最初の行を読み取り、その値がDsAvg
にあるかどうかを確認し、JobName
の次の値にスキップしました(存在する場合)。代わりに2億1,200万行をスキャンする場合、約20万回の実行を必要とするシークプランを実行できます。
これは主に、Paul Whiteが先駆者として開拓したテクニック here とともに再帰を使用することで達成できます。再帰を使用して、上記で説明した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
のインデックスに対して200002インデックスシークを実行し、すべての一意のJobName
値を取得します。次に、DsAvg
に結合して、1つを除くすべての行を削除します。残りの行については、DsJobStat
に戻って結合し、必要な列をすべて取得します。
IOパターンは完全に変更されます。これを取得する前に:
テーブル 'DsJobStat'。スキャンカウント1、論理読み取り1091651、物理読み取り13836、先読み読み取り181966
再帰クエリでは、次のようになります。
テーブル 'DsJobStat'。スキャン数200003、論理読み取り1398000、物理読み取り1、先読み読み取り7345
私のマシンでは、新しいクエリは6891ミリ秒のCPU時間と7107ミリ秒の経過時間で実行されます。このように再帰を使用する必要があることは、データモデルに何かが欠落していることを示唆していることに注意してください(または、投稿された質問に単に記載されていなかった可能性があります)。可能なすべてのJobNames
を含む比較的小さなテーブルがある場合、大きなテーブルでの再帰ではなく、そのテーブルを使用するほうがはるかに適切です。つまり、必要なすべてのJobNames
を含む結果セットがある場合、インデックスシークを使用して、欠落している列の残りを取得できます。ただし、必要のないJobNames
の結果セットを使用してこれを行うことはできません。
条件を書き換えるとどうなるか見てみましょう
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つの要素がゼロでないことだけですか?