Die Abfrage wird bei der ersten Ausführung am Teststandort langsam ausgeführt. Warum?


7

Ich habe diese Abfrage gefunden, indem ich eine Testsite mit SQL-Profiler für alles angesehen habe, was länger als 10 Sekunden dauerte. Ich habe den Code direkt aus dem SQL Profiler in SQL Studio kopiert, wo er schnell ausgeführt werden konnte. Das langsame Verhalten beim ersten Ausführen kann mithilfe von DBCC DROPCLEANBUFFERS zurückgesetzt werden.

Hier ist die langsame Abfrage:

exec sp_executesql N'SELECT [t0].*
FROM [dbo].[MyTable] AS [t0]
WHERE [t0].[ParentID] IN (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26, @p27, @p28, @p29, @p30, @p31, @p32, @p33, @p34, @p35, @p36, @p37, @p38, @p39, @p40, @p41, @p42, @p43, @p44, @p45, @p46, @p47, @p48, @p49, @p50, @p51, @p52, @p53, @p54, @p55, @p56, @p57, @p58, @p59, @p60, @p61, @p62, @p63, @p64, @p65, @p66, @p67, @p68, @p69, @p70, @p71, @p72, @p73, @p74)',N'@p0 int,@p1 int,@p2 int,@p3 int,@p4 int,@p5 int,@p6 int,@p7 int,@p8 int,@p9 int,@p10 int,@p11 int,@p12 int,@p13 int,@p14 int,@p15 int,@p16 int,@p17 int,@p18 int,@p19 int,@p20 int,@p21 int,@p22 int,@p23 int,@p24 int,@p25 int,@p26 int,@p27 int,@p28 int,@p29 int,@p30 int,@p31 int,@p32 int,@p33 int,@p34 int,@p35 int,@p36 int,@p37 int,@p38 int,@p39 int,@p40 int,@p41 int,@p42 int,@p43 int,@p44 int,@p45 int,@p46 int,@p47 int,@p48 int,@p49 int,@p50 int,@p51 int,@p52 int,@p53 int,@p54 int,@p55 int,@p56 int,@p57 int,@p58 int,@p59 int,@p60 int,@p61 int,@p62 int,@p63 int,@p64 int,@p65 int,@p66 int,@p67 int,@p68 int,@p69 int,@p70 int,@p71 int,@p72 int,@p73 int,@p74 int',@p0=121888,@p1=317624,@p2=278130,@p3=299426,@p4=128786,@p5=553917,@p6=169682,@p7=316993,@p8=319430,@p9=321347,@p10=377276,@p11=388570,@p12=233344,@p13=304376,@p14=318493,@p15=318190,@p16=144455,@p17=342559,@p18=309867,@p19=258251,@p20=139296,@p21=530970,@p22=288191,@p23=127107,@p24=547572,@p25=617531,@p26=238898,@p27=606923,@p28=267113,@p29=140833,@p30=122554,@p31=298846,@p32=562964,@p33=554626,@p34=414874,@p35=534996,@p36=614977,@p37=230423,@p38=261899,@p39=149666,@p40=179537,@p41=148420,@p42=262955,@p43=298094,@p44=575449,@p45=246861,@p46=572334,@p47=172152,@p48=529420,@p49=129074,@p50=266589,@p51=194619,@p52=376201,@p53=608389,@p54=162335,@p55=405965,@p56=125671,@p57=146195,@p58=538850,@p59=575254,@p60=129485,@p61=243677,@p62=615828,@p63=236197,@p64=343015,@p65=294449,@p66=562013,@p67=138933,@p68=614729,@p69=561779,@p70=-1,@p71=-1,@p72=-1,@p73=-1,@p74=-1

Hier ist die Tabellendefinition mit Indizes:

CREATE TABLE [dbo].[MyTable] (
[MyID] [int] IDENTITY (1, 1) NOT NULL ,
[GrandParentID] [int] NOT NULL ,
[ParentID] [int] NOT NULL ,
[Col1] [nvarchar] (200) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Col2] [nvarchar] (20) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Col3] [nvarchar] (200) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Col4] [nvarchar] (200) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Col5] [decimal](18, 2) NULL ,
[Col6] [char] (2) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[Col7] [char] (4) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[OtherKey] [int] NULL ,
[Col8] [nvarchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[Col9] [datetime] NULL ,
[Col10] [nvarchar] (150) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
 CONSTRAINT [PK__7E8439BE] PRIMARY KEY  CLUSTERED 
(
    [MyID]
)  ON [PRIMARY] 
) ON [PRIMARY]

CREATE  INDEX [MyTable_ParentID] ON [dbo].[MyTable]([ParentID]) WITH  FILLFACTOR = 90 ON [PRIMARY]
CREATE  INDEX [MyTable_OtherKey] ON [dbo].[MyTable]([OtherKey] DESC ) WITH  FILLFACTOR = 90 ON [PRIMARY]
CREATE  INDEX [MyTable_GrandParentID] ON [dbo].[MyTable]([GrandParentID]) ON [PRIMARY]

Hier sind die Timings und IO:

-- Test site - first run:
(7064 row(s) affected)
Table 'MyTable'. Scan count 71, logical reads 49255, physical reads 3, read-ahead reads 13160, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 140 ms,  elapsed time = 30400 ms.

-- Test site - second run:
(7064 row(s) affected)
Table 'MyTable'. Scan count 71, logical reads 29054, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 78 ms,  elapsed time = 169 ms.

-- Production site - first run:
(7064 row(s) affected)
Table 'MyTable'. Scan count 71, logical reads 50513, physical reads 3, read-ahead reads 13157, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 62 ms,  elapsed time = 276 ms.

-- Production site - second run:
(7064 row(s) affected)
Table 'MyTable'. Scan count 71, logical reads 29054, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 63 ms,  elapsed time = 262 ms.

Der Teststandort und der Produktionsstandort werden auf die gleiche Weise eingerichtet: Datenbankdateien befinden sich auf einer einzigen virtuellen Festplatte, die von einem SAN mit Lastenausgleich auf mehreren Festplatten unterstützt wird.

Ausführungsplan:

constantscan -> sort -> verschachtelte Schleife -> Indexsuche -> verschachtelte Schleife -> Schlüsselsuche


1
Leider macht "Select t. * .." die Verwendung eines Deckungsindex unbrauchbar. Auf diese Weise haben Sie möglicherweise diese kostspieligen Key Lookups entfernt, die mein erster Teil wären. Verwenden Sie Entity Framework (oder ähnliches)?
Marian

Dies ist eine von LinqToSql kompilierte Abfrage.
Amy B

Antworten:


12

Nun, wenn Sie die Abfrage zum ersten Mal ausführen, muss sie von der Festplatte geladen werden. Anschließend wird Speicher verwendet, der viel schneller als die Festplatte ist. Wenn Sie die Puffer löschen, werden im Wesentlichen alle diese Daten aus dem Speicher gelöscht. Wenn Sie die Daten das nächste Mal anfordern, müssen sie erneut von der Festplatte geladen werden. Stellen Sie sich Festplatte gegen Speicher als Schildkröte gegen Hase vor. Außerhalb von Testszenarien möchten Sie, dass Ihre Abfragen im Allgemeinen aus einem warmen Cache ausgeführt werden und den Pufferpool nicht absichtlich mit löschen DBCC DROPCLEANBUFFERS;.


Ja, ich verwende DROPCLEANBUFFERS nur, um den Fall der ersten Abfrageausführung neu zu erstellen. Die Produktionsstätte scheint sich nicht um die Ausführung der ersten Abfrage zu kümmern. Irgendwelche Gedanken dazu?
Amy B

2
Ich weiß nicht, was "scheint mich nicht zu interessieren" bedeutet. Wollen Sie damit sagen, dass es in der Produktion immer langsam ist oder immer schnell? Wie viele Daten befinden sich in der Produktion im Vergleich zum Test? Wie wäre es mit Speicher, Unterschieden im E / A-System usw.? Haben Sie Ausführungspläne in Produktion mit Test verglichen? Wir sind keine sehr guten Gedankenleser. :-)
Aaron Bertrand

1
Was ich mit E / A meine, ist nicht das, was Sie denken, denke ich. Ich meine, Ihre Testbox ist eine virtuelle Maschine mit einer langsamen iSCSI- oder DAS-Festplatte mit 7200 U / min und Daten / Protokoll / Tempdb, die alle dasselbe Laufwerk verwenden, und Ihre Produktionsmaschine verwendet 15 KB SAS oder SSD mit Daten / Protokoll / Tempdb, die entsprechend verteilt sind die E / A weniger bedeutsam? Der Vergleich von Äpfeln mit Äpfeln ist wichtig. Wenn Sie dies nicht tun und Eingaben wünschen, müssen Sie alle Details teilen.
Aaron Bertrand

1
Sprechen Sie auch über einen geschätzten Ausführungsplan oder einen tatsächlichen Ausführungsplan? Werfen Sie die geschätzten und tatsächlichen Werte für jedes der vier von Ihnen beschriebenen Szenarien. Verwenden Sie vorzugsweise den Plan-Explorer , der alle Arten nützlicher Laufzeitmetriken enthält, die Sie nicht direkt von SSMS erhalten. Wenn Sie die PRO-Version evaluieren, erhalten Sie sogar Wartestatistiken für Ihre Sitzung (erstellen Sie also Pläne für jedes der vier Szenarien auf separaten Registerkarten).
Aaron Bertrand

5
@DavidB Begreifen Sie , dass die Differenz muss die Folge einer Differenz zwischen den beiden Umgebungen sein? In Bezug auf diesen Unterschied können selbst die besten Experten (einschließlich Aaron) nur raten, während Sie in der Lage sein sollten, ihn aufzuspüren.
Dekso

6

Ich würde gerne einige Experten für Leistungsoptimierung hören, aber nach dem, was ich mit den präsentierten Informationen sehen kann, laden die 13160 Read-Ahead-Reads sie in den Speicher. Das 2. Mal liest es es aus dem Speicher und ist viel schneller. Ein virtuelles System zu sein, könnte ein gemeinsames IOPS bedeuten und die Situation beim Laden in den Cache verschlimmern. Sobald Sie den Cache geleert haben, muss er erneut von der Festplatte gelesen werden.

Wenn Sie die Wartestatistiken dafür sammeln könnten, würden wir wissen, ob dies die richtige Antwort ist. Ich würde erwarten, dass vielleicht 10 Sekunden PAGELATCHIO auftauchen. Verwenden Sie diesen Link , um erweiterte Ereignisse für eine einzelne Operation abzurufen. Führen Sie die erste Abfrage aus, rufen Sie die erweiterten Ereignisinformationen ab, führen Sie sie erneut aus, ohne den Speicher zu löschen, und rufen Sie die erweiterten Ereignisinformationen ab und veröffentlichen Sie sie. Es sollte beim ersten Mal anzeigen, dass Disk io das Problem ist.


3

Die Hauptauswirkung auf Ihre Anfrage ist auf die KEY LOOKUP zurückzuführen, deren Kosten 77% betragen . Sie können die Schlüsselsuche entfernen, indem Sie einen Deckungsindex oder einen eingeschlossenen Spaltenindex erstellen.

Covering Index deckt die Abfrage ab, indem alle erforderlichen Spalten eingeschlossen werden.

Mit dem enthaltenen Index können Sie zusätzliche Spalten einfügen, damit diese im Index gespeichert werden, aber nicht Teil des Indexbaums sind. Dies hat den Vorteil, dass weniger Speicherplatz benötigt wird (wenn Ihr Index sehr groß ist, in GB).

Wenn Sie also einen nicht gruppierten Index für MyTable mit eingeschlossenen Spalten erstellen, wird die Schlüsselsuche beseitigt und die Abfrageleistung verbessert.

Sehen Sie sich die Key Lookup-Eigenschaft an und sehen Sie, welche Spalten erforderlich sind.

Zum Beispiel, wenn die Key Lookup-Eigenschaft MyID in der Ausgabeliste anzeigt, dann unten

CREATE NONCLUSTERED INDEX [MyTable_ParentID_included] ON [dbo].[MyTable]
(
       [ParentID] 
)
INCLUDE ([MyID])  ON [PRIMARY]
GO

Einige gute Erklärungen finden Sie unter Reduzieren von Schlüsselsuchen

EDIT: Ich habe gerade bemerkt, dass Sie verwenden

SELECT [t0].*
FROM [dbo].[MyTable] AS [t0] .....

Benötigt die Anwendung alle Spalten aus der Basistabelle? Dadurch wird die Verwendung der enthaltenen Spalten negiert.


1
Obwohl dies offensichtlich nützlich ist, um die Leistung im Allgemeinen zu verbessern, war die Frage, warum die Abfrage beim ersten Durchlauf langsam und beim nachfolgenden Durchlauf schnell ist. Zumindest habe ich es so gelesen.
Aaron Bertrand

Während ein Deckungsindex das IO reduzieren würde, würde es im Raum teuer kosten. Ich bin derzeit nicht bereit, einen 8G-Index zu empfehlen. Meine Frage ist: Warum funktioniert der erste Durchlauf der Abfrage in der Produktion gut, im Test jedoch schlecht?
Amy B

Es hängt von vielen Faktoren ab, die Aaron und Ali erwähnt haben. Ist der Server virtuell, sind alle Konfigurationseinstellungen bei Test und PROD, RAM-Größe, SQL Server-Version und Service Pack bei Test und PROD gleich?
Kin Shah
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.