Dies ist eine dieser religiös-politischen Debatten, die seit Jahren toben:Soll ich gespeicherte Prozeduren verwenden oder Ad-hoc-Abfragen in meine Anwendung stellen? Ich war schon immer ein Befürworter gespeicherter Prozeduren, und zwar aus folgenden Gründen:
- Ich kann keinen SQL-Injection-Schutz implementieren, wenn die Abfrage im Anwendungscode erstellt wird. Die Entwickler kennen möglicherweise parametrisierte Abfragen, aber nichts zwingt sie, sie richtig zu verwenden.
- Ich kann weder eine Abfrage optimieren, die in den Quellcode einer Anwendung eingebettet ist, noch kann ich Best Practices durchsetzen.
- Wenn ich eine Möglichkeit zur Abfrageoptimierung finde, muss ich den Anwendungscode neu kompilieren und erneut bereitstellen, anstatt nur die gespeicherte Prozedur zu ändern, um sie bereitzustellen.
- Wenn die Abfrage an mehreren Stellen in der Anwendung oder in mehreren Anwendungen verwendet wird und eine Änderung erforderlich ist, muss ich sie an mehreren Stellen ändern, während ich sie bei einer gespeicherten Prozedur nur einmal ändern muss (Bereitstellungsprobleme beiseite).
Ich sehe auch, dass viele Leute gespeicherte Prozeduren zugunsten von ORMs aufgeben. Für einfache Anwendungen wird dies wahrscheinlich in Ordnung sein, aber wenn Ihre Anwendung komplexer wird, werden Sie wahrscheinlich feststellen, dass Ihr ORM Ihrer Wahl einfach nicht in der Lage ist, bestimmte Abfragemuster auszuführen, was Sie *zwingt*, eine gespeicherte Prozedur zu verwenden. Wenn es gespeicherte Prozeduren unterstützt, heißt das.
Obwohl ich all diese Argumente immer noch ziemlich überzeugend finde, sind sie nicht das, worüber ich heute sprechen möchte; Ich möchte über Leistung sprechen.
Viele Argumente da draußen werden einfach sagen:"Gespeicherte Prozeduren funktionieren besser!" Das mag zu einem gewissen Zeitpunkt ein wenig richtig gewesen sein, aber seit SQL Server die Möglichkeit zum Kompilieren auf Anweisungsebene statt auf Objektebene hinzugefügt hat und leistungsstarke Funktionen wie optimize for ad hoc workloads
erworben hat , ist dies kein sehr starkes Argument mehr. Die Indexoptimierung und sinnvolle Abfragemuster wirken sich viel stärker auf die Leistung aus als die Verwendung einer gespeicherten Prozedur. Ich bezweifle, dass Sie in modernen Versionen viele Fälle finden werden, in denen genau dieselbe Abfrage merkliche Leistungsunterschiede aufweist, es sei denn, Sie führen auch andere Variablen ein (z. B. das lokale Ausführen einer Prozedur im Vergleich zu einer Anwendung in einem anderen Rechenzentrum auf einem anderen Kontinent).
Allerdings gibt es einen Leistungsaspekt, der beim Umgang mit Ad-hoc-Abfragen oft übersehen wird:der Plan-Cache. Wir können optimize for ad hoc workloads
verwenden um zu verhindern, dass Single-Use-Pläne unseren Cache füllen (Kimberly Tripp (@KimberlyLTripp) von SQLskills.com hat hier einige großartige Informationen dazu), und das wirkt sich auf Single-Use-Pläne aus, unabhängig davon, ob die Abfragen aus einer gespeicherten Prozedur heraus ausgeführt werden oder werden ad hoc ausgeführt. Eine andere Auswirkung, die Sie unabhängig von dieser Einstellung möglicherweise nicht bemerken, ist, wenn identisch Pläne belegen aufgrund von Unterschieden in SET
mehrere Slots im Cache Optionen oder kleinere Deltas im eigentlichen Abfragetext. Das ganze „langsam in der Anwendung, schnell in SSMS“-Phänomen hat vielen Leuten geholfen, Probleme mit Einstellungen wie SET ARITHABORT
zu lösen . Heute wollte ich über Unterschiede in Abfragetexten sprechen und etwas demonstrieren, das die Leute jedes Mal überrascht, wenn ich es anspreche.
Cache zum Brennen
Nehmen wir an, wir haben ein sehr einfaches System, auf dem AdventureWorks2012 ausgeführt wird. Und nur um zu beweisen, dass es nicht hilft, haben wir optimize for ad hoc workloads
aktiviert :
EXEC sp_configure 'Erweiterte Optionen anzeigen', 1;GORECONFIGURE WITH OVERRIDE;GOEXEC sp_configure 'Für Ad-hoc-Workloads optimieren', 1;GORECONFIGURE WITH OVERRIDE;
Und dann den Plan-Cache freigeben:
DBCC FREEPROCCACHE;
Jetzt generieren wir ein paar einfache Variationen einer ansonsten identischen Abfrage. Diese Variationen können möglicherweise Codierungsstile für zwei verschiedene Entwickler darstellen – geringfügige Unterschiede in Leerzeichen, Groß-/Kleinschreibung usw.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID>=75120ORDER BY OrderDate DESC;GO -- ändern Sie>=75120 zu> 75119 (gleiche Logik, da es ein INT ist)GO SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID> 75119ORDER BY OrderDate DESC;GO – Ändern Sie die Abfrage in KleinbuchstabenGO select top (1) salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO – entfernen Sie die runden Klammern das Argument für topGO select top 1 salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- fügen Sie ein Leerzeichen nach top 1GO select top 1 salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- entfernen Sie die Leerzeichen zwischen den KommasGO wählen Sie Top 1 salesorderid,orderdate,subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GOWenn wir diesen Stapel einmal ausführen und dann den Plan-Cache überprüfen, sehen wir, dass wir 6 Kopien von im Wesentlichen genau demselben Ausführungsplan haben. Dies liegt daran, dass der Abfragetext binär gehasht ist, was bedeutet, dass Groß- und Kleinschreibung einen Unterschied machen und dazu führen können, dass ansonsten identische Abfragen für SQL Server eindeutig aussehen.
SELECT [text], size_in_bytes, usecounts, cacheobjtypeFROM sys.dm_exec_cached_plans AS pCROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS tWHERE LOWER(t.[text]) LIKE '%ales.sales'+'orderheader%';Ergebnisse:
Text | size_in_bytes | usecounts | cacheobjtype |
---|---|---|---|
Wählen Sie die Top-1-Verkaufsauftrags-ID aus, o… | 272 | 1 | Kompilierter Plan-Stub |
Wählen Sie die Top-1-Verkaufsauftrags-ID aus, … | 272 | 1 | Kompilierter Plan-Stub |
wählen Sie die oberste 1-Verkaufsauftrags-ID aus, o… | 272 | 1 | Kompilierter Plan-Stub |
Wählen Sie die oberste (1) Verkaufsauftrags-ID aus,… | 272 | 1 | Kompilierter Plan-Stub |
OBEN WÄHLEN (1) SalesOrderID,… | 272 | 1 | Kompilierter Plan-Stub |
OBEN WÄHLEN (1) SalesOrderID,… | 272 | 1 | Kompilierter Plan-Stub |
Ergebnisse nach erstmaliger Ausführung "identischer" Abfragen
Dies ist also nicht ganz verschwenderisch, da die Ad-hoc-Einstellung es SQL Server ermöglicht hat, bei der ersten Ausführung nur kleine Stubs zu speichern. Wenn wir den Stapel jedoch erneut ausführen (ohne den Prozedurcache freizugeben), sehen wir ein etwas alarmierenderes Ergebnis:
Text | size_in_bytes | usecounts | cacheobjtype |
---|---|---|---|
Wählen Sie die Top-1-Verkaufsauftrags-ID aus, o… | 49.152 | 1 | Kompilierter Plan |
Wählen Sie die Top-1-Verkaufsauftrags-ID aus, … | 49.152 | 1 | Kompilierter Plan |
wählen Sie die oberste 1-Verkaufsauftrags-ID aus, o… | 49.152 | 1 | Kompilierter Plan |
Wählen Sie die oberste (1) Verkaufsauftrags-ID aus,… | 49.152 | 1 | Kompilierter Plan |
OBEN WÄHLEN (1) SalesOrderID,… | 49.152 | 1 | Kompilierter Plan |
OBEN WÄHLEN (1) SalesOrderID,… | 49.152 | 1 | Kompilierter Plan |
Ergebnisse nach zweiter Ausführung "identischer" Abfragen
Das Gleiche passiert bei parametrisierten Abfragen, unabhängig davon, ob die Parametrierung einfach oder erzwungen ist. Und dasselbe passiert, wenn die Ad-hoc-Einstellung nicht aktiviert ist, außer dass es früher passiert.
Das Endergebnis ist, dass dies selbst bei identisch aussehenden Abfragen eine Menge Plan-Cache-Bloat erzeugen kann – bis hin zu zwei Abfragen, bei denen ein Entwickler mit einem Tabulator und der andere mit 4 Leerzeichen einrückt. Ich muss Ihnen nicht sagen, dass der Versuch, diese Art von Konsistenz in einem Team durchzusetzen, mühsam bis unmöglich sein kann. Meiner Meinung nach ist dies also ein starker Hinweis auf Modularisierung, DRY und Zentralisierung dieser Art von Abfragen in einer einzigen gespeicherten Prozedur.
Eine Einschränkung
Wenn Sie diese Abfrage in eine gespeicherte Prozedur stellen, haben Sie natürlich nur eine Kopie davon, sodass Sie die Möglichkeit vollständig vermeiden, mehrere Versionen der Abfrage mit leicht unterschiedlichem Abfragetext zu haben. Nun könnten Sie auch argumentieren, dass verschiedene Benutzer möglicherweise dieselbe gespeicherte Prozedur mit unterschiedlichen Namen erstellen und in jeder gespeicherten Prozedur eine geringfügige Variation des Abfragetexts vorhanden ist. Obwohl es möglich ist, denke ich, dass dies ein ganz anderes Problem darstellt. :-)