Database
 sql >> Datenbank >  >> RDS >> Database

Ein Anwendungsfall für sp_prepare / sp_prepexec

Es gibt Funktionen, vor denen viele von uns zurückschrecken, wie Cursor, Trigger und dynamisches SQL. Es steht außer Frage, dass sie jeweils ihre Anwendungsfälle haben, aber wenn wir einen Trigger mit einem Cursor in dynamischem SQL sehen, kann uns das erschrecken (dreifacher Schlag).

Plan Guides und sp_prepare sitzen in einem ähnlichen Boot:Wenn Sie sehen würden, wie ich einen von ihnen verwende, würden Sie eine Augenbraue hochziehen; Wenn Sie sehen würden, wie ich sie zusammen benutze, würden Sie wahrscheinlich meine Temperatur messen. Aber wie bei Cursorn, Triggern und dynamischem SQL haben sie ihre Anwendungsfälle. Und ich bin kürzlich auf ein Szenario gestoßen, in dem es vorteilhaft war, sie zusammen zu verwenden.

Hintergrund

Wir haben viele Daten. Und viele Anwendungen, die mit diesen Daten laufen. Einige dieser Anwendungen lassen sich nur schwer oder gar nicht ändern, insbesondere Standardanwendungen von Drittanbietern. Wenn also ihre kompilierte Anwendung Ad-hoc-Abfragen an SQL Server sendet, insbesondere als vorbereitete Anweisung, und wenn wir nicht die Freiheit haben, Indizes hinzuzufügen oder zu ändern, sind mehrere Tuning-Möglichkeiten sofort vom Tisch.

In diesem Fall hatten wir eine Tabelle mit ein paar Millionen Zeilen. Eine vereinfachte und bereinigte Version:

CREATE TABLE dbo.TheThings
(
  ThingID    bigint NOT NULL,
  TypeID     uniqueidentifier NOT NULL,
  dt1        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt2        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt3        datetime NOT NULL DEFAULT sysutcdatetime(),
  CONSTRAINT PK_TheThings PRIMARY KEY (ThingID)
);
 
CREATE INDEX ix_type ON dbo.TheThings(TypeID);
 
SET NOCOUNT ON;
GO
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1) 2500, @guid2
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;

Die vorbereitete Anweisung aus der Anwendung sah folgendermaßen aus (wie im Plan-Cache zu sehen):

(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0

Das Problem ist, dass für einige Werte von TypeID , gäbe es viele tausend Zeilen. Bei anderen Werten wären es weniger als 10. Wenn basierend auf einem Parametertyp der falsche Plan ausgewählt (und wiederverwendet) wird, kann dies für die anderen zu Problemen führen. Für die Abfrage, die eine Handvoll Zeilen abruft, möchten wir eine Indexsuche mit Lookups, um die zusätzlichen nicht abgedeckten Spalten abzurufen, aber für die Abfrage, die 700.000 Zeilen zurückgibt, möchten wir nur einen Clustered-Index-Scan. (Idealerweise würde der Index abdecken, aber diese Option war diesmal nicht vorgesehen.)

In der Praxis erhielt die Anwendung immer die Scan-Variation, obwohl dies diejenige war, die in etwa 1 % der Zeit benötigt wurde. 99 % der Abfragen verwendeten einen 2-Millionen-Zeilen-Scan, obwohl sie eine Suche + 4 oder 5 Suchen hätten verwenden können.

Wir könnten dies leicht in Management Studio reproduzieren, indem wir diese Abfrage ausführen:

DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO
 
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO

Die Pläne kamen so zurück:

Die Schätzung betrug in beiden Fällen 1.000 Zeilen; die Warnungen auf der rechten Seite sind auf Rest-E/A zurückzuführen.

Wie können wir sicherstellen, dass die Abfrage je nach Parameter die richtige Wahl trifft? Wir müssten es neu kompilieren, ohne der Abfrage Hinweise hinzuzufügen, Trace-Flags zu aktivieren oder Datenbankeinstellungen zu ändern.

Wenn ich die Abfragen unabhängig voneinander mit OPTION (RECOMPILE) ausgeführt habe , würde ich bei Bedarf die Suche erhalten:

DBCC FREEPROCCACHE;
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE);
SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);

Mit RECOMPILE erhalten wir genauere Schätzungen und eine Suche, wenn wir eine brauchen.

Aber auch hier konnten wir den Hinweis nicht direkt zur Abfrage hinzufügen.

Versuchen wir es mit einer Plananleitung

Viele Leute warnen vor Planführern, aber wir waren hier irgendwie in einer Ecke. Wir würden es auf jeden Fall vorziehen, die Abfrage oder die Indizes zu ändern, wenn wir könnten. Aber das könnte das Nächstbeste sein.

EXEC sys.sp_create_plan_guide   
  @name   = N'TheThingGuide',
  @stmt   = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0',
  @type   = N'SQL',
  @params = N'@P0 varchar(8000)',
  @hints  = N'OPTION (RECOMPILE)';

Scheint einfach zu sein; testen ist das Problem. Wie simulieren wir eine vorbereitete Anweisung in Management Studio? Wie können wir sicher sein, dass die Anwendung den geführten Plan erhält und dass dies ausdrücklich auf den Plan-Guide zurückzuführen ist?

Wenn wir versuchen, diese Abfrage in SSMS zu simulieren, wird dies als Ad-hoc-Anweisung behandelt, nicht als vorbereitete Anweisung, und ich konnte dies nicht dazu bringen, den Planleitfaden aufzunehmen:

DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier
SELECT * FROM dbo.TheThings WHERE TypeID = @P0

Dynamisches SQL funktionierte auch nicht (dies wurde auch als Ad-hoc-Anweisung behandelt):

DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', 
        @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier
        @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
EXEC sys.sp_executesql @sql, @params, @P0;

Und ich konnte dies nicht tun, weil es auch die Plananleitung nicht aufnehmen würde (die Parametrisierung übernimmt hier, und ich hatte nicht die Freiheit, Datenbankeinstellungen zu ändern, selbst wenn dies wie eine vorbereitete Anweisung behandelt werden sollte). :

SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';

Ich kann den Plancache nicht auf die Abfragen überprüfen, die von der App ausgeführt werden, da der zwischengespeicherte Plan nichts über die Verwendung der Planhinweisliste aussagt (SSMS fügt diese Informationen für Sie in das XML ein, wenn Sie einen tatsächlichen Plan generieren). Und wenn die Abfrage wirklich den RECOMPILE-Hinweis beachtet, den ich an die Plan-Anleitung weitergebe, wie könnte ich dann überhaupt irgendwelche Beweise im Plan-Cache sehen?

Versuchen wir es mit sp_prepare

Ich habe sp_prepare in meiner Karriere weniger verwendet als Planleitfäden, und ich würde es nicht für Anwendungscode empfehlen. (Wie Erik Darling betont, kann die Schätzung aus dem Dichtevektor gezogen werden, nicht aus dem Sniffing des Parameters.)

In meinem Fall möchte ich es aus Leistungsgründen nicht verwenden, ich möchte es (zusammen mit sp_execute) verwenden, um die vorbereitete Anweisung zu simulieren, die von der App kommt.

DECLARE @o int;
EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)',
     N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0';
 
EXEC sys.sp_execute @o,  'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan
EXEC sys.sp_execute @o,  'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup

SSMS zeigt uns, dass die Planhinweisliste in beiden Fällen verwendet wurde.

Aufgrund der Neukompilierung können Sie den Plan-Cache nicht auf diese Ergebnisse überprüfen. Aber in einem Szenario wie dem meinen sollten Sie in der Lage sein, die Auswirkungen in der Überwachung zu sehen, explizit über erweiterte Ereignisse zu prüfen oder die Linderung des Symptoms zu beobachten, das Sie dazu veranlasst hat, diese Abfrage überhaupt zu untersuchen (beachten Sie nur, dass die durchschnittliche Laufzeit, Abfrage Statistiken usw. können durch zusätzliche Kompilierung beeinträchtigt werden).

Schlussfolgerung

Dies war ein Fall, in dem eine Planhinweisliste von Vorteil war, und sp_prepare war nützlich, um zu validieren, dass sie für die Anwendung funktionieren würde. Diese sind nicht oft nützlich und seltener zusammen, aber für mich war es eine interessante Kombination. Auch ohne den Planleitfaden ist sp_prepare Ihr Freund, wenn Sie SSMS verwenden möchten, um eine App zu simulieren, die vorbereitete Anweisungen sendet. (Siehe auch sp_prepexec, das eine Abkürzung sein kann, wenn Sie nicht versuchen, zwei verschiedene Pläne für dieselbe Abfrage zu validieren.)

Beachten Sie, dass diese Übung nicht unbedingt dazu diente, ständig eine bessere Leistung zu erzielen – es sollte die Leistungsvarianz glätten. Neukompilierungen sind natürlich nicht kostenlos, aber ich zahle eine kleine Strafe dafür, dass 99 % meiner Abfragen in 250 ms und 1 % in 5 Sekunden ausgeführt werden, anstatt mit einem Plan festzustecken, der für 99 % der Abfragen absolut schrecklich ist oder 1 % der Suchanfragen.