Penyesuaian kinerja pada kueri

9

Mencari bantuan untuk meningkatkan kinerja kueri ini.

SQL Server 2008 R2 Enterprise , Max RAM 16 GB, CPU 40, Max Degree of Parallelism 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;

Pesan eksekusi,

(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.

Struktur tabel:

-- 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
)

Rencana eksekusi:

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


Perbarui setelah dijawab

Terima kasih banyak @ Jo Obbish

Anda benar tentang masalah permintaan ini yaitu tentang antara DsJobStat dan DsAvg. Ini tidak banyak tentang cara BERGABUNG dan tidak menggunakan TIDAK DI.

Memang ada meja seperti yang Anda duga.

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

Saya mencoba saran Anda,

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;   

Pesan eksekusi:

(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.

Rencana eksekusi: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f

Wendy
sumber
Jika itu kode vendor yang tidak dapat Anda ubah, hal terbaik yang harus dilakukan adalah membuka insiden dukungan dengan vendor, sesulit yang mungkin terjadi, dan mengalahkan mereka karena memiliki kueri yang mengharuskan banyak membaca untuk memenuhi. Klausa NOT IN yang merujuk ke nilai dalam tabel dengan 413 ribu baris, eh, kurang optimal. Pemindaian indeks pada DSJobStat menghasilkan 212 juta baris, yang menghasilkan hingga 212 juta loop bersarang, dan Anda dapat melihat 212 juta jumlah baris adalah 83% dari biaya. Saya tidak berpikir Anda dapat membantu ini tanpa menulis ulang kueri atau membersihkan data ...
Tony Hinkle
Saya tidak mengerti, mengapa saran Evan tidak membantu Anda di tempat pertama, kedua jawabannya sama kecuali penjelasan. Juga saya tidak melihat bahwa Anda sepenuhnya menerapkan apa yang disarankan kedua orang ini kepada Anda. Jo membuat pertanyaan ini menarik.
KumarHarsh

Jawaban:

11

Mari kita mulai dengan mempertimbangkan pesanan bergabung. Anda memiliki tiga referensi tabel dalam kueri. Pesanan bergabung mana yang mungkin memberi Anda kinerja terbaik? Pengoptimal kueri berpikir bahwa bergabung dari DsJobStatke DsAvgakan menghilangkan hampir semua baris (perkiraan kardinalitas turun dari 212195000 ke 1 baris). Rencana aktual menunjukkan kepada kita bahwa perkiraan tersebut cukup dekat dengan kenyataan (11 baris bertahan untuk bergabung). Namun, join diimplementasikan sebagai join anti semi-gabung yang tepat, sehingga semua 212 juta baris dari DsJobStattabel dipindai hanya untuk menghasilkan 11 baris. Itu tentu saja dapat berkontribusi pada waktu eksekusi permintaan yang panjang, tetapi saya tidak dapat memikirkan operator fisik atau logis yang lebih baik untuk sambungan yang akan lebih baik. Saya yakin ituDJS_Dashboard_2indeks digunakan untuk pertanyaan lain, tetapi semua kunci tambahan dan kolom yang disertakan hanya akan membutuhkan lebih banyak IO untuk permintaan ini dan memperlambat Anda. Jadi Anda berpotensi memiliki masalah akses tabel dengan pemindaian indeks di atas DsJobStatmeja.

Saya akan berasumsi bahwa bergabung AJFtidak terlalu selektif. Saat ini tidak relevan dengan masalah kinerja yang Anda lihat dalam kueri, jadi saya akan mengabaikannya untuk sisa jawaban ini. Itu bisa berubah jika data dalam tabel berubah.

Masalah lain yang jelas dari rencana tersebut adalah operator spool count row. Ini adalah operator yang sangat ringan tetapi menjalankan lebih dari 200 juta kali. Operator ada di sana karena permintaan ditulis dengan NOT IN. Jika ada satu baris NULL di DsAvgsemua baris harus dihilangkan. Kumparan adalah implementasi dari pemeriksaan itu. Itu mungkin bukan logika yang Anda inginkan, jadi sebaiknya Anda menulis bagian yang akan digunakan NOT EXISTS. Manfaat sebenarnya dari penulisan ulang itu tergantung pada sistem dan data Anda.

Saya membuat beberapa data berdasarkan rencana kueri untuk menguji beberapa penulisan ulang kueri. Definisi tabel saya sangat berbeda dengan definisi Anda karena terlalu banyak upaya untuk membuat data untuk setiap kolom. Bahkan dengan struktur data yang disingkat saya dapat mereproduksi masalah kinerja yang Anda alami.

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;

Berdasarkan rencana kueri, kita dapat melihat bahwa ada sekitar 200000 JobNamenilai unik dalam DsAvgtabel. Berdasarkan jumlah baris aktual setelah bergabung dengan tabel itu, kita dapat melihat bahwa hampir semua JobNamenilai di DsJobStatjuga dalam DsAvgtabel. Dengan demikian, DsJobStattabel memiliki 20.000 nilai unik untuk JobNamekolom dan 1000 baris per nilai.

Saya percaya bahwa kueri ini mewakili masalah kinerja:

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

Semua hal lain dalam rencana kueri Anda ( GROUP BY,, HAVINGgabung gaya kuno, dll) terjadi setelah set hasil dikurangi menjadi 11 baris. Saat ini tidak masalah dari sudut pandang kinerja permintaan, tetapi mungkin ada masalah lain di sana yang bisa diungkapkan oleh data yang berubah di tabel Anda.

Saya menguji di SQL Server 2017, tapi saya mendapatkan bentuk rencana dasar yang sama seperti Anda:

sebelum rencana

Di mesin saya, permintaan itu membutuhkan 62219 ms waktu CPU dan 65576 ms waktu berlalu untuk mengeksekusi. Jika saya menulis ulang kueri untuk digunakan 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);

tidak ada spool

Spool tidak lagi dieksekusi 212 juta kali dan mungkin memiliki perilaku yang diinginkan dari vendor. Sekarang kueri dieksekusi dalam 34516 ms waktu CPU dan 41132 ms waktu berlalu. Sebagian besar waktu dihabiskan memindai 212 juta baris dari indeks.

Pemindaian indeks itu sangat disayangkan untuk permintaan itu. Rata-rata kita memiliki 1000 baris per nilai unik JobName, tetapi kita tahu setelah membaca baris pertama jika kita membutuhkan 1000 baris sebelumnya. Kami hampir tidak pernah membutuhkan baris-baris itu, tetapi kami masih perlu memindai mereka. Jika kita tahu bahwa baris-barisnya tidak terlalu padat dalam tabel dan bahwa hampir semuanya akan dihilangkan dengan join, kita dapat membayangkan pola IO yang mungkin lebih efisien pada indeks. Bagaimana jika SQL Server membaca baris pertama per nilai unik JobName, memeriksa apakah nilai itu masuk DsAvg, dan hanya melompat ke depan ke nilai selanjutnya JobNamejika itu? Alih-alih memindai 212 juta baris, rencana pencarian yang membutuhkan sekitar 200 ribu eksekusi dapat dilakukan sebagai gantinya.

Ini sebagian besar dapat dicapai dengan menggunakan rekursi bersama dengan teknik yang dipelopori Paul White yang dijelaskan di sini . Kita dapat menggunakan rekursi untuk melakukan pola IO yang saya jelaskan di atas:

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);

Permintaan itu banyak untuk dilihat, jadi saya sarankan hati-hati memeriksa rencana yang sebenarnya . Pertama-tama kita melakukan 200002 indeks berusaha melawan indeks DsJobStatuntuk mendapatkan semua nilai unik JobName. Kemudian kita bergabung DsAvgdan menghilangkan semua baris kecuali satu. Untuk baris yang tersisa, gabung kembali ke DsJobStatdan dapatkan semua kolom yang diperlukan.

Pola IO benar-benar berubah. Sebelum kita dapatkan ini:

Tabel 'DsJobStat'. Pindai hitungan 1, bacaan logis 1091651, bacaan fisik 13836, baca-depan bacaan 181966

Dengan permintaan rekursif kami mendapatkan ini:

Tabel 'DsJobStat'. Pindai hitung 200003, bacaan logis 1398000, bacaan fisik 1, bacalah bacaan 7345

Di komputer saya, kueri baru dijalankan hanya dalam 6891 ms waktu CPU dan 7107 ms berlalu waktu. Perhatikan bahwa perlu menggunakan rekursi dengan cara ini menunjukkan bahwa ada sesuatu yang hilang dari model data (atau mungkin itu hanya tidak disebutkan dalam pertanyaan yang diposting). Jika ada tabel yang relatif kecil yang berisi semua kemungkinan JobNames, akan jauh lebih baik untuk menggunakan tabel itu daripada rekursi di meja besar. Intinya adalah jika Anda memiliki set hasil yang berisi semua JobNamesyang Anda butuhkan maka Anda dapat menggunakan indeks berusaha untuk mendapatkan sisa kolom yang hilang. Namun, Anda tidak dapat melakukannya dengan set hasil JobNamesyang TIDAK Anda perlukan.

Joe Obbish
sumber
Saya menyarankan NOT EXISTS. Mereka sudah menjawab dengan, "Saya sudah mencoba keduanya, bergabung dan tidak ada, sebelum saya memposting pertanyaan. Tidak banyak perbedaan."
Evan Carroll
1
Saya ingin tahu jika ide rekursif berhasil, itu menakutkan.
Evan Carroll
saya pikir memiliki klausa tidak diperlukan. "ElapsedSec bukan nol" di mana klausa akan melakukannya. Juga saya pikir CTE rekursif tidak memerlukan. Anda dapat menggunakan row_number () lebih dari (partisi dengan urutan nama pekerjaan berdasarkan nama) rn di mana tidak ada (pilih pertanyaan). apa yang harus Anda katakan tentang ide saya?
KumarHarsh
@ Jo Obbish, saya memperbarui posting saya. Terima kasih banyak.
Wendy
ya, CTE Rekursif keluar melakukan row_number () lebih dari (partisi berdasarkan urutan nama pekerjaan dengan nama) rn oleh 1 menit. Tetapi pada saat yang sama saya tidak melihat keuntungan tambahan dalam CTE Rekursif menggunakan data sampel Anda.
KumarHarsh
0

Lihat apa yang terjadi jika Anda menulis ulang kondisinya,

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

Untuk

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

Juga pertimbangkan untuk menulis ulang bergabung SQL89 Anda karena gaya itu mengerikan.

Dari pada

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

Mencoba

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

Saya juga curiga bahwa kondisi ini dapat ditulis lebih baik tetapi kita harus tahu lebih banyak tentang apa yang terjadi

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

Apakah Anda benar-benar harus tahu rata-rata bukan nol, atau hanya satu unsur dari kelompok itu bukan nol?

Evan Carroll
sumber
@EvanCarroll. Saya sudah mencoba keduanya, bergabung dan tidak ada, sebelum saya memposting pertanyaan. Tidak banyak perbedaan.
Wendy