Themenseite · Das Paradox der Abstraktion

Verschachtelte Views: Drei Zeilen SQL, ein riesiger Plan

Eine View für die Bestellungen, darauf eine mit den Kunden, darauf eine mit den Produkten, darauf die Reporting-View. Jede Schicht ist für sich verständlich, und die Abfrage am Ende passt in drei Zeilen:

Stand 04.11.2026 · ca. 9 Minuten

Thema Views · Compile · Join-EliminationLab SQL Server 2017–2025
SELECT …FROM vw_BerichtWHERE …vw_Berichtvw_…vw_…vw_…vw_…vw_…vw_…vw_…vw_…vw_…eine View, viele Tabellen im Plan
SELECT *FROM dbo.vw_SalesReportingWHERE CustomerID = 42;

Was der Optimizer bekommt, ist etwas anderes. Bevor er einen Plan sucht, ersetzt SQL Server jede View durch ihre Definition, Ebene für Ebene. Aus drei Zeilen wird eine Abfrage mit allen Joins, Unterabfragen und Ausdrücken aller Schichten. Diese Vertiefung zeigt, was dabei passiert, wann es gut geht und woran Sie erkennen, dass der Optimizer überfordert ist.

Eine View ist kein vorberechnetes Modul

Eine gewöhnliche View speichert nur ihre Definition, keine Daten und keinen Plan. Bei jeder Abfrage wird sie aufgelöst, und der Optimizer sieht nur noch die Tabellen dahinter. Das ist oft gut: Er kann Filter nach unten schieben, Joins umstellen und Teile weglassen, die die Abfrage nicht braucht. Es heißt aber auch, dass jede Schicht ihre Arbeit in jede Abfrage mitbringt, die auf ihr aufbaut.

Ergebnisse speichert nur eine indizierte View. Sie ist ein eigenes Werkzeug mit eigenen Regeln und Kosten beim Schreiben und nicht Thema dieser Seite.

Wann der Optimizer Joins wieder entfernt

Fragt die Abfrage nur wenige Spalten ab, kann der Optimizer Joins zu Tabellen weglassen, aus denen nichts gebraucht wird. Das nennt sich Join-Elimination und setzt voraus, dass er sicher weiß, dass der Join die Zeilenzahl nicht verändert:

  • Bei einem inneren Join braucht er dafür einen vertrauenswürdigen Fremdschlüssel (WITH CHECK angelegt oder geprüft) auf Spalten, die NOT NULL sind.
  • Bei einem äußeren Join (LEFT JOIN) genügt ein eindeutiger Schlüssel auf der rechten Seite.

Im Lab bestand eine Kette aus bis zu 16 Views. Jede Ebene baut auf der vorigen auf, fügt per innerem Join eine Dimensionstabelle hinzu und gibt deren Namen aus. Die Abfrage braucht davon nur die Bestellnummer und den Namen aus der ersten Dimension:

Eine Kette aus 16 Views und eine Abfrage, die fast nichts davon braucht

vw_L1 bis vw_L16
-- Ebene 1: Bestellungen und die erste DimensionCREATE VIEW dbo.vw_L1 ASSELECT o.OrderId, o.D1Id, /* … */ o.D16Id, o.Amount,       d1.Name AS D1NameFROM dbo.Orders AS oJOIN dbo.D1 AS d1 ON o.D1Id = d1.Id;GO -- Ebene 2: die Ebene darunter plus die zweite DimensionCREATE VIEW dbo.vw_L2 ASSELECT v.OrderId, v.D1Id, /* … */ v.Amount, v.D1Name,       d2.Name AS D2NameFROM dbo.vw_L1 AS v   -- baut auf der View darunter aufJOIN dbo.D2 AS d2 ON v.D2Id = d2.Id;GO -- … und so weiter bis Ebene 16CREATE VIEW dbo.vw_L16 ASSELECT v.OrderId, /* … */ v.D15Name,       d16.Name AS D16NameFROM dbo.vw_L15 AS vJOIN dbo.D16 AS d16 ON v.D16Id = d16.Id;
Was Join-Elimination möglich macht
-- Vertrauenswürdig: mit WITH CHECK angelegt, Spalte D1Id ist NOT NULLALTER TABLE dbo.Orders WITH CHECK    ADD CONSTRAINT FK_Orders_D1    FOREIGN KEY (D1Id) REFERENCES dbo.D1 (Id);   -- je Bestellung genau ein D1-- … ebenso für D2 bis D16 -- Prüfen: is_not_trusted muss 0 seinSELECT name, is_not_trusted, is_disabledFROM sys.foreign_keysWHERE parent_object_id = OBJECT_ID('dbo.Orders');
Braucht nur Bestellnummer und D1Name
SELECT OrderId, D1Name   -- zwei Spalten aus 16 EbenenFROM dbo.vw_L16WHERE OrderId BETWEEN 1000 AND 1100;

mit Fremdschlüsseln 2 Tabellen im Plan, 1,4 ms · ohne 17 Tabellen, 4,0 ms · Kompilierung 50 bzw. 62 ms

Dieselbe Abfrage direkt auf den Tabellen
SELECT o.OrderId, d1.Name AS D1NameFROM dbo.Orders AS oJOIN dbo.D1 AS d1 ON o.D1Id = d1.IdWHERE o.OrderId BETWEEN 1000 AND 1100;

2 Tabellen im Plan, 1,3 ms · Kompilierung 1 ms

SELECT * auf dieselbe View
SELECT *   -- jetzt wird jede Dimension gebraucht, nichts fällt wegFROM dbo.vw_L16WHERE OrderId BETWEEN 1000 AND 1100;

17 Tabellen im Plan, mit und ohne Fremdschlüssel · 6,1 ms wie flach · Kompilierung 63 statt 24 ms

Code wie im Lab; Spaltenlisten mit /* … */ gekürzt, Schema einheitlich dbo. Ergebnisse auf SQL Server 2022, Median.

Dieselbe Kette gab es zweimal: einmal mit vertrauenswürdigen Fremdschlüsseln von den Bestellungen zu allen Dimensionen, einmal ohne. Bei 16 Ebenen:

VersionTabellen im Plan mit / ohne FremdschlüsselKompilierzeit mit / ohne Fremdschlüssel / flachLaufzeit mit / ohne Fremdschlüssel
SQL Server 20172 / 1738 / 49 / 1 ms1,4 / 3,5 ms
SQL Server 20192 / 1748 / 58 / 1 ms1,3 / 3,7 ms
SQL Server 20222 / 1750 / 62 / 1 ms1,4 / 4,0 ms
SQL Server 20252 / 1756 / 82 / 2 ms1,5 / 4,2 ms

„Flach“ ist dieselbe Abfrage direkt auf den zwei Tabellen, die sie braucht.

Mit Fremdschlüsseln blieb von den 16 Joins keiner übrig. Der Plan las nur die Bestellungen und die erste Dimension, 9 Seiten wie die flache Abfrage. Ohne Fremdschlüssel las er alle 17 Tabellen und 3.237 Seiten. Bei 101 Zeilen sind das nur Millisekunden. Deutlicher wird es, wenn die Abfrage über alle Bestellungen gruppiert: Mit Fremdschlüsseln lief sie so schnell wie flach, 25 bis 64 ms. Ohne Fremdschlüssel brauchte sie auf SQL Server 2019 bis 2025 107 bis 155 ms, auf 2017 918 ms.

Auffällig ist die Kompilierzeit. Auch wenn am Ende alle überflüssigen Joins wegfallen, kompilierte die View-Abfrage 20- bis 50-mal so lange wie die flache. SQL Server löst zuerst alle Ebenen auf und entfernt danach, was nicht gebraucht wird. Die Elimination spart Arbeit bei der Ausführung, nicht bei der Kompilierung. Braucht die Abfrage alle Spalten, gibt es nichts zu entfernen: Dann lief die View genauso schnell wie die flache Abfrage, kompilierte aber rund doppelt so lange.

View-Kette mit FremdschlüsselnView-Kette ohne FremdschlüsselFlache Abfrage, nur nötige JoinsTabellen im Plan12222232425282921621720369121518Tabellenbezüge; links die Zahl der EbenenKompilierzeit1221234147828162021650621010203040506070Millisekunden; links die Zahl der Ebenen
View-Kette mit FremdschlüsselnView-Kette ohne FremdschlüsselFlache Abfrage, nur nötige JoinsTabellen im Plan1222223242528292162172061218Tabellenbezüge je EbenenzahlKompilierzeit122123414782816202165062103570Millisekunden je Ebenenzahl
Eine Abfrage, die aus einer View-Kette nur zwei Spalten braucht. Mit Fremdschlüsseln entfernt der Optimizer alle überflüssigen Joins, der Plan ist so klein wie bei der flachen Abfrage. Die Kompilierzeit wächst trotzdem mit jeder Ebene: Erst wird alles aufgelöst, dann entfernt. SQL Server 2022, Median, 8 vCPU, synthetische Daten; 2017, 2019 und 2025 zeigen dasselbe Muster.

Im Betrieb fällt die Kompilierung nur an, wenn kein passender Plan im Cache liegt. Für eine Abfrage, die tausendfach mit demselben Plan läuft, sind 50 ms einmalig. Spürbar wird es bei Abfragen, die oft neu kompiliert werden, etwa mit wechselnden Literalen im SQL-Text oder mit OPTION (RECOMPILE).

SQL Server 2025 kompilierte die meisten Abfragen im Lab länger als 2022, je nach Abfrage um das 1,1- bis 1,6-Fache. Nur die tiefsten Rauten ohne Fremdschlüssel (siehe unten) kompilierte 2025 schneller. Woran das liegt, haben wir nicht untersucht.

Wenn die Suche zu groß wird

Der Optimizer probiert nicht alle möglichen Pläne aus. Er arbeitet in Stufen und hat für jede Abfrage ein Budget, das sich nach ihrer Größe richtet. Ist das Budget aufgebraucht, nimmt er den besten bisher gefundenen Plan. Im tatsächlichen Plan steht dann:

  • StatementOptmLevel = FULL und StatementOptmEarlyAbortReason = TimeOut, im grafischen Plan „Reason For Early Termination: Time Out“.

Das ist kein Fehler und kein Abbruch der Abfrage. Der gefundene Plan kann gut sein. Es heißt nur, dass der Optimizer nicht alle Möglichkeiten geprüft hat, und dass kleine Änderungen an Daten oder Statistiken zu einem ganz anderen Plan führen können.

Im Lab kam TimeOut früh und unabhängig von Views. Schon die flach geschriebene Abfrage auf 9 Tabellen endete auf allen vier Versionen damit. In der View-Kette ohne Fremdschlüssel trat es ab 8 Ebenen auf, mit Fremdschlüsseln nie, weil nach der Elimination nur zwei Tabellen übrig blieben. Ausschlaggebend ist also, wie viele Tabellen der Optimizer am Ende anordnen muss, nicht wie die Abfrage geschrieben ist. Ein TimeOut allein ist deshalb noch kein Befund, sondern ein Hinweis, genauer hinzusehen.

Echte Fehler beschreibt Microsoft für sehr große aufgelöste Abfragen:

  • 8623: Der Optimizer findet gar keinen Plan („ran out of internal resources“).
  • 8632: Der aufgelöste Ausdruck überschreitet eine interne Grenze.

Im Lab trat keiner der beiden auf, auch nicht bei Abfragen, die nach dem Auflösen über 500 Tabellenbezüge enthielten und über drei Minuten kompilierten. Die Grenze war die Zeit, nicht ein Fehler. Wer auf eine Fehlermeldung wartet, um ein Problem mit verschachtelten Views zu erkennen, wartet also womöglich vergeblich.

Mehrfach verwendete Views vervielfachen die Arbeit

Besonders teuer wird es, wenn eine View eine andere mehr als einmal verwendet, zum Beispiel für einen Vergleich mit dem Vorjahr. Jede Verwendung wird einzeln aufgelöst. Baut darauf wieder eine View auf, die dasselbe tut, verdoppeln sich die Tabellenbezüge mit jeder Ebene. Im Quelltext sieht man das nicht; die Inventur auf der Übersichtsseite auch nicht. Erst der Ausführungsplan zeigt, wie oft dieselbe Tabelle gelesen wird.

Im Lab verband jede Ebene dieser Raute die darunterliegende View mit sich selbst über die Bestellnummer. Das Ergebnis bleibt dasselbe, eine Zeile je Bestellung:

Die Raute: jede Ebene nutzt die darunterliegende zweimal

vw_R1: Bestellungen und eine Dimension
CREATE VIEW dbo.vw_R1 ASSELECT o.OrderId, d1.Name AS D1Name, o.AmountFROM dbo.Orders AS oJOIN dbo.D1 AS d1 ON o.D1Id = d1.Id;
vw_R2: dieselbe View zweimal
CREATE VIEW dbo.vw_R2 ASSELECT a.OrderId, a.D1Name, a.Amount + b.Amount AS AmountFROM dbo.vw_R1 AS aJOIN dbo.vw_R1 AS b ON a.OrderId = b.OrderId;   -- dieselbe View ein zweites MalGO -- vw_R3 verbindet vw_R2 mit sich selbst, vw_R4 dann vw_R3, … bis vw_R9CREATE VIEW dbo.vw_R3 ASSELECT a.OrderId, a.D1Name, a.Amount + b.Amount AS AmountFROM dbo.vw_R2 AS aJOIN dbo.vw_R2 AS b ON a.OrderId = b.OrderId;

Ebene 2: Bestellungen 2-mal im Plan · Ebene 9: 256-mal, Kompilierung 219 s

So löst SQL Server SELECT … FROM dbo.vw_R3 vor der Optimierung auf
vw_R3
├── vw_R2 (a)
│   ├── vw_R1 (a)  →  Orders, D1
│   └── vw_R1 (b)  →  Orders, D1
└── vw_R2 (b)
    ├── vw_R1 (a)  →  Orders, D1
    └── vw_R1 (b)  →  Orders, D1

Ebene 4:     8 × Orders
Ebene 6:    32 × Orders
Ebene 9:   256 × Orders

Ebene 3: Bestellungen 4-mal im Plan · jede weitere Ebene verdoppelt

Dasselbe Ergebnis für Ebene 3, ohne Views
SELECT o.OrderId, d1.Name AS D1Name,       o.Amount * 4 AS Amount   -- 2 hoch (Ebene − 1), hier Ebene 3FROM dbo.Orders AS oJOIN dbo.D1 AS d1 ON o.D1Id = d1.IdWHERE o.OrderId BETWEEN 1000 AND 1100;

Bestellungen 1-mal im Plan · Kompilierung wenige Millisekunden auf jeder Ebene

Code wie im Lab, Schema einheitlich dbo. Ergebnisse auf SQL Server 2022 mit Fremdschlüsseln, schmale Abfrage, Median.

Der Optimizer hat die doppelten Bezüge nicht zusammengelegt, auch nicht mit Fremdschlüsseln. Für dieselbe schmale Abfrage wie oben auf SQL Server 2022:

EbenenBestellungen im PlanKompilierzeit mit / ohne FremdschlüsselLaufzeit mit Fremdschlüsseln
48-mal69 / 171 ms10 ms
632-mal0,8 / 3,6 s3 ms
764-mal5,5 / 54 s6 ms
8128-mal32 / 133 s10 ms
9256-mal219 s / nicht gemessen160 ms
Kompilierung mit FremdschlüsselnKompilierung ohne FremdschlüsselAusführung mit Fremdschlüsseln1 ms10 ms100 ms1 s10 s100 s1.000 sAbbruchgrenze im Lab: 300 s11×22×34×48×516×632×764×8128×9256×Ebenen der Raute; darunter: wie oft die Faktentabelle im Plan steht219 s133 s160 ms
Kompilierung mit FremdschlüsselnKompilierung ohne FremdschlüsselAusführung mit Fremdschlüsseln1 ms10 ms100 ms1 s10 s100 s1.000 sAbbruchgrenze im Lab: 300 s11×234×4516×6764×89256×Ebenen; darunter: Faktentabelle im Plan219 s133 s160 ms
Eine View, die die darunterliegende zweimal verwendet, Ebene auf Ebene: Mit jeder Ebene verdoppelt sich, wie oft die Faktentabelle im Plan steht. Die Kompilierung steigt von Millisekunden auf über drei Minuten, die Ausführung bleibt bis Ebene 8 unter 20 Millisekunden. Ohne Fremdschlüssel wurde Ebene 9 nicht mehr gemessen. Logarithmische Skala, SQL Server 2022, Median, 8 vCPU, synthetische Daten.

Mit Fremdschlüsseln kostete ab sechs Ebenen jede weitere Ebene rund das Sechs- bis Siebenfache an Kompilierzeit. Bei acht Ebenen belegte die Kompilierung ohne Fremdschlüssel rund 800 MB Arbeitsspeicher, bei neun Ebenen mit Fremdschlüsseln rund 1 GB. Die Ausführung der schmalen Abfrage blieb dagegen klein, weil sie nur 101 Bestellungen las. Muss die Abfrage alle Daten lesen, wächst auch die Ausführung: Gruppiert über alle Bestellungen lief sie bei acht Ebenen 70 Sekunden mit und 212 Sekunden ohne Fremdschlüssel. Die flache Abfrage mit demselben Ergebnis brauchte 28 ms.

Die Grenze lag je Version etwas anders. SQL Server 2019 und 2022 kompilierten mit Fremdschlüsseln noch neun Ebenen, 2025 acht. SQL Server 2017 lief mit der schmalen Abfrage schon bei sechs Ebenen mit Fremdschlüsseln in die 300 Sekunden, obwohl die neueren Versionen dieselbe Ebene in unter einer Sekunde kompilierten. Den Grund dafür haben wir nicht untersucht.

So gehen Sie vor

  1. Tiefe Views finden. Die Inventur zeigt Verschachtelungstiefe und Tabellen dahinter. Mehrfach verwendete Views erkennt sie nicht.
  2. Tatsächlichen Plan der langsamen Abfrage holen. Prüfen Sie CompileTime und CompileCPU, StatementOptmEarlyAbortReason und wie viele Tabellen der Plan liest. Steht dieselbe Tabelle viele Male im Plan, obwohl die Abfrage sie fachlich einmal braucht, ist eine View mehrfach aufgelöst worden.
  3. Fremdschlüssel prüfen. Nicht vertrauenswürdige Fremdschlüssel (sys.foreign_keys.is_not_trusted = 1) verhindern Join-Elimination. Mit ALTER TABLE … WITH CHECK CHECK CONSTRAINT … werden sie wieder geprüft; das liest die ganze Tabelle und gehört in ein Wartungsfenster.
  4. Schichten flacher machen. Eine Abfrage, die nur drei Tabellen braucht, sollte nicht über eine View mit zwölf gehen. Für Berichte lohnt oft eine eigene, flache View.
  5. Bewusst zerlegen, wo es nötig ist. Ein Zwischenergebnis in einer temporären Tabelle trennt die Optimierung in kleinere Aufgaben. Eine CTE oder Unterabfrage tut das nicht: Sie wird genauso aufgelöst wie eine View. Was das Zwischenspeichern in TempDB kostet, zeigt der TempDB-Hub.

Quellen

Weitere Themen zur Abstraktion: T-SQL-Funktionen · Zurück zur Übersicht: Das Paradox der Abstraktion

Weitere Themen zur Abstraktion