web-dev-qa-db-ja.com

クエリのパフォーマンス調整

このクエリのパフォーマンスを改善するための支援を求めています。

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.

実行計画: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f

9
Wendy

結合順序を検討することから始めましょう。クエリに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値があることがわかります。そのテーブルへの結合後の実際の行数に基づいて、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 BYHAVING、古代スタイルの結合など)は、結果セットが11行に削減された後に発生します。現在、クエリのパフォーマンスの観点からは問題ではありませんが、テーブル内の変更されたデータによって明らかになる可能性がある他の懸念がある可能性があります。

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

before plan

私のマシンでは、そのクエリの実行に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);

no spool

スプールは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の結果セットを使用してこれを行うことはできません。

11
Joe Obbish

条件を書き換えるとどうなるか見てみましょう

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つの要素がゼロでないことだけですか?

0
Evan Carroll