Angenommen, "Kosten" sind in Bezug auf die Zeit (obwohl Sie nicht sicher sind, was es sonst in Bezug auf ;-) sein könnte, sollten Sie zumindest in der Lage sein, sich ein Bild davon zu machen, indem Sie Folgendes tun:
DBCC FREEPROCCACHE WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
EXEC sp_help 'sys.databases'; -- replace with your proc
SET STATISTICS TIME OFF;
Der erste auf der Registerkarte "Nachrichten" gemeldete Artikel sollte sein:
SQL Server-Analyse- und Kompilierungszeit:
Ich würde dies mindestens 10 Mal ausführen und sowohl die "CPU" - als auch die "verstrichene" Millisekunde mitteln.
Im Idealfall würden Sie dies in der Produktion ausführen, um eine echte Zeitschätzung zu erhalten, aber nur selten dürfen Benutzer den Plan-Cache in der Produktion leeren. Glücklicherweise wurde es ab SQL Server 2008 möglich, einen bestimmten Plan aus dem Cache zu löschen. In diesem Fall können Sie Folgendes tun:
DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
SELECT DISTINCT stat.plan_handle
FROM sys.dm_exec_query_stats stat
CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
+ CONVERT(NVARCHAR(130), cte.plan_handle, 1)
+ N');'
+ NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);
SET STATISTICS TIME ON;
EXEC sp_help 'sys.databases' -- replace with your proc
SET STATISTICS TIME OFF;
Abhängig von der Variabilität der Werte, die für die Parameter übergeben werden, die den "fehlerhaften" zwischengespeicherten Plan verursachen, ist jedoch eine andere Methode zu berücksichtigen, die einen Mittelweg zwischen OPTION(RECOMPILE)
und darstellt OPTION(OPTIMIZE FOR UNKNOWN)
: Dynamic SQL. Ja, ich habe es gesagt. Und ich meine sogar nicht parametrisiertes dynamisches SQL. Hier ist warum.
Sie haben eindeutig Daten mit einer ungleichmäßigen Verteilung, zumindest in Bezug auf einen oder mehrere Eingabeparameterwerte. Die Nachteile der genannten Optionen sind:
OPTION(RECOMPILE)
Für jede Ausführung wird ein Plan erstellt, und Sie können niemals von einer Wiederverwendung des Plans profitieren , selbst wenn die erneut übergebenen Parameterwerte mit den vorherigen Läufen identisch sind. Bei Prozessen, die häufig aufgerufen werden - mindestens alle paar Sekunden -, werden Sie vor der gelegentlichen schrecklichen Situation bewahrt, bleiben jedoch in einer immer nicht allzu großartigen Situation.
OPTION(OPTIMIZE FOR (@Param = value))
wird einen Plan erstellen, der auf diesem bestimmten Wert basiert. Dies könnte in mehreren Fällen hilfreich sein, lässt Sie jedoch weiterhin für das aktuelle Problem offen.
OPTION(OPTIMIZE FOR UNKNOWN)
Es wird ein Plan erstellt, der auf einer durchschnittlichen Verteilung basiert, die einigen Abfragen hilft, anderen jedoch schadet. Dies sollte mit der Option zur Verwendung lokaler Variablen identisch sein.
Bei korrekter Ausführung von Dynamic SQL haben die verschiedenen übergebenen Werte jedoch ihre eigenen, idealen Abfragepläne (so gut sie auch sein werden). Die Hauptkosten hierbei sind, dass mit zunehmender Anzahl der übergebenen Werte die Anzahl der Ausführungspläne im Cache zunimmt und sie Speicher belegen. Die Nebenkosten sind:
String-Parameter müssen validiert werden, um SQL-Injections zu verhindern
Möglicherweise müssen Sie ein Zertifikat und einen zertifikatbasierten Benutzer einrichten, um die ideale Sicherheitsabstraktion aufrechtzuerhalten, da für Dynamic SQL direkte Tabellenberechtigungen erforderlich sind.
So habe ich diese Situation gemeistert, als ich Procs hatte, die mehr als einmal pro Sekunde aufgerufen wurden und mehrere Tabellen mit jeweils Millionen von Zeilen trafen. Ich hatte es versucht, OPTION(RECOMPILE)
aber dies erwies sich in 99% der Fälle, in denen der Parameter Sniffing / Bad-Cached-Plan-Problem nicht auftrat, als viel zu schädlich für den Prozess. Beachten Sie bitte, dass in einem dieser Prozesse ungefähr 15 Abfragen enthalten waren und nur 3 bis 5 davon wie hier beschrieben in dynamisches SQL konvertiert wurden. Dynamisches SQL wurde nur verwendet, wenn es für eine bestimmte Abfrage erforderlich war.
Wenn die gespeicherte Prozedur mehrere Eingabeparameter enthält, ermitteln Sie, welche mit Spalten mit sehr unterschiedlichen Datenverteilungen verwendet werden (und daher dieses Problem verursachen) und welche mit Spalten mit gleichmäßigeren Verteilungen verwendet werden (und nicht sollten) dieses Problem verursachen).
Erstellen Sie die Dynamic SQL-Zeichenfolge mit Parametern für die Proc-Eingabeparameter, die gleichmäßig verteilten Spalten zugeordnet sind. Diese Parametrisierung trägt dazu bei, die resultierende Zunahme der Ausführungspläne im Cache für diese Abfrage zu reduzieren.
Für die übrigen Parameter, die mit sehr unterschiedlichen Verteilungen verknüpft sind, sollten diese als Literalwerte in Dynamic SQL verkettet werden. Da eine eindeutige Abfrage durch Änderungen am Abfragetext bestimmt wird, WHERE StatusID = 1
ist having eine andere Abfrage und damit ein anderer Abfrageplan als having WHERE StatusID = 2
.
Handelt es sich bei einem der Proc-Eingabeparameter, die in den Text der Abfrage eingebunden werden sollen, um Zeichenfolgen, müssen diese zum Schutz vor SQL Injection validiert werden (dies ist jedoch weniger wahrscheinlich, wenn die übergebenen Zeichenfolgen vom generiert werden App und kein Benutzer, aber immer noch). Zumindest tun Sie dies, REPLACE(@Param, '''', '''''')
um sicherzustellen, dass einfache Anführungszeichen zu einfachen Anführungszeichen werden.
Erstellen Sie bei Bedarf ein Zertifikat, das zum Erstellen eines Benutzers verwendet wird, und signieren Sie die gespeicherte Prozedur so, dass direkte Tabellenberechtigungen nur dem neuen zertifikatbasierten Benutzer und nicht [public]
oder nur Benutzern gewährt werden, die ansonsten nicht über solche Berechtigungen verfügen sollten .
Beispiel proc:
CREATE PROCEDURE MySchema.MyProc
(
@Param1 INT,
@Param2 DATETIME,
@Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = N'
SELECT tab.Field1, tab.Field2, ...
FROM MySchema.SomeTable tab
WHERE tab.Field3 = @P1
AND tab.Field8 >= CONVERT(DATETIME, ''' +
CONVERT(NVARCHAR(50), @Param2, 121) +
N''')
AND tab.Field2 LIKE N''' +
REPLACE(@Param3, N'''', N'''''') +
N'%'';';
EXEC sp_executesql
@SQL,
N'@P1 INT',
@P1 = @Param1;