SQL Execution Plan Caching

Bei einem TSQL-Server kann es von Zeit zu Zeit zu „seltsamen“ Performance-Einbrüchen kommen. Dieser Posting beschäftige sich mit einem der Gründe: SQL – Execution Plan Caching.

Bei modernen Datenbanken wird die Schicht der Datenabfrage von der Schicht der Datenbeschaffung getrennt. Hatte früher die Formulierung eines SQL-Befehls Einfluss auf die Art wie das DBMS die Daten beschafft, ist der gleicher Befehl heute eher als „Wunsch“ welche Daten zu selektieren sind. Wie diese schlussendlich beschafft werden, bleibt dem DBMS überlassen. Der Sprung von SQL-Statement auf tatsächliche I/O-Operationen wird gemeinhin als „Execution Plan“ bezeichnet.

Ein solcher ExecutionPlan wird für jede Abfrage erzeugt die an das DBMS gestellt wird. Bei der Erstellungen eines solchen Plans werden alle Informationen die dem DBMS zur Verfügung stehen (Statistiken, Datenbankstruktur, etc) für die Optimierung der Datenabfrage genutzt. Es werden Suchalgorithmen und Daten-Aggregations–Funktionen ausgewählt. Anschließend wird der ExecutionPlan ausgeführt und die Abfrage beantwortet. Um bei Wiederholung einer Abfrage Zeit zu sparen, wird ein einmal erzeugter ExecutionPlan gecached und später wiederverwendet.

Um zu sehen welche ExecutionPlans gerade im Cache vorgehalten werden, kann folgender Befehl genutzt werden.

SELECT [cp].[refcounts]
, [cp].[usecounts]
, [cp].[objtype]
, [st].[dbid]
, [st].[objectid]
, [st].[text]
, [qp].[query_plan]
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text ( cp.plan_handle ) st
CROSS APPLY sys.dm_exec_query_plan ( cp.plan_handle ) qp ;

Wie bei allen Cache und Statistik-Funktionen, gibt es eine Kehrseite der Medaille. Es kann passieren, dass all die schöne Optimierung ins Lehre läuft oder ein Plan verwendet wird, der für die aktuelle Abfrage völlig ungeeignet ist. Wie schon bei dem TOP-Problem dreht sich auch hier alles darum, dass das DBMS den „falschen“ ExecutionPlan benutzt.

„Falsch“ bedeutet in diesem Zusammenhang, dass I/O – Operatoren benutzt werden, die für die anfallenden Datenmengen nicht optimal sind. Dafür gibt es normalerweise nur drei Gründe:

  • Man weißt das DBMS dazu an: Es gibt mehrere Möglichkeiten das DBMS auf die falsche Fährte zu schicken. Entweder zwingt man es mittels QueryHints dazu oder gibt gleiche ganze ExecutionPlans vor.
  • Die Statistik ist falsch: der unwahrscheinlichste Fall, sei aber dennoch Aufgeführt. Ändert sich bei einer Tabelle mehr als 20%+500 Datensätze (Update, Delete, Insert) wird die Statistik der Tabelle neu erzeugt. Bei 10000 Datensätzen müssten sich also 2500 Datensätze ändern, bis es zu einem Update in der Statistik gibt. Im schlimmsten Fall muss sich nur ein Datensatz ändern um bei einem Folge-Join zu einer explodierenden Datenmenge zu sorgen. In-the-Wild ist mir ein solches „Szenario“ aber noch nie unter gekommen.
  • Parameter Sniffing/Spoofing + ExecutionPlan Caching: Für sich alleine genommen ist jede der beiden Technologien sinnvoll und hilfreich. In Kombination kann es jedoch zu erheblichen „Verklemmungen“ kommen. „Parameter Sniffing“ bezeichnet den Versuch des Query-Optimizers einen Execution Plan auf einen bestimmten Parameter-Tupel zu optimieren. Wird die Abfrage immer nur mit den gleichen Parametern ausgeführt oder nur einmal, beschleunigt das die Abfrage. Sorgt einer der Parameter z.B. dafür das eine große Tabelle auf wenige Datensätze reduziert wird, kann sich so die Lead-Tabelle ändern. Nachteilig wird das ganze wenn dieser Plan gecached wird. Bei der zweiten Ausführung der Abfrage wird der alte Execution Plan genutzt. Im schlimmsten Fall ist dieser falsch optimiert und führt zu unnötigen I/O – Operationen.

Gegen falsche Statistiken lässt sich recht einfach vorgehen. Man veranlasst einen Rebuild der entsprechenden Statistik oder erzeugt gleich alle Statistiken neu.

UPDATE STATISTICS  -- erneuert nur die Statistik der angegeben Tabelle

exec sp_updatestats -- baut alle Statistiken neu

Mit einem Statistik-Rebuild werden auch alle gecachten Execution-Pläne verworfen die von der Statistik abhängen.

Gegen Parameter-Sniffing hilft das leider nur temporär. Abfragen die mehrere „optimale“ Execution Pläne haben, sollten gesondert behandelt werden. Es gibt prinzipiell fünf Möglichkeiten das Problem endgültig zu beheben.

  • der „sicherste“ weg: Wenn eine Abfrage in unterschiedlichen Kontexten genutzt wird, dupliziert man die Abfrage und ändert die Signatur ein wenig. Somit werden mehrere Abfragen gestellt, die getrennt optimiert werden.
  • Kann man das Problem in eine StoredProcedure auslagern, kann man dort ein „ConditionalTree“ aufbauen. Also die Übergabeparameter analysieren und entsprechende Sub-StoredProcedures aufrufen. Die wiederum auf den speziellen Fall optimiert werden.
  • Man kann auch das Parameter Sniffing unterdrücken. Dazu muss man aber ebenfalls auf Stored Procedures zurückgreifen.
    CREATE PROCEDURE SelectSomething
    		@parameter1 int,
    		@parameter2 char
    DECLARE
    	@parameter1_save int,
    	@parameter2_save char
    AS
    	SET @parameter1_save = @parameter1
    	SET @parameter2_save = @parameter2
    	
    	SELECT * 
    	  FROM tabelle1 t1
    	  JOIN tabelle2 t2
    	 WHERE t1.s1 = @parameter1_save
    	   AND t2.s1 = @parameter2_save
    

    Die Parameterzuweisung kann vom Optimizer nicht vorhergesehen werden, deswegen wird der anschließende Select auf den allgemeinen Anwendungsfall optimiert.

  • Kann man den Anwendungsbereich eines SELECTs von vornherein einschränken oder eingrenzen, kann man QueryOptimizer anweisen auf diesen Bereich hin zu optimieren.
    SELECT * 
      FROM tabelle1 t1
      JOIN tabelle2 t2
     WHERE t1.s1 = @parameter1
       AND t2.s1 = @parameter2
    OPTION (OPTIMIZE FOR(@parameter1 = ‘somevalue’,@parameter2 = ‘somevalue’ ));
    
  • Es gibt fälle wo alle bisher genannten Lösungsvarianten nicht greifen, da die Abfragen nicht getrennt werden können und die einzelnen Abfrage-Ziele so unterschiedlich sind, dass eine allgemeine Optimierung keinen Vorteil bringt. In diesem Fall kann man das DBMS anweisen, den ExecutionPlan nicht zu cachen bzw keine gecachten Plane zu nutzen.

    SELECT * 
      FROM tabelle1 t1
      JOIN tabelle2 t2
     WHERE t1.s1 = @parameter1
       AND t2.s1 = @parameter2
    OPTION (RECOMPILE)
    

    In diesem Fall wird für diesen SELECT immer ein neuer ExecutionPlan erzeugt und nach der Ausführung sofort verworfen. So wird immer der „optimale“ ExecutionPlan genutzt. Dafür geht jetzt immer Zeit für das Erzeugen des Plans drauf.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.