Wissen

Praxis-Artikel und Buchkapitel zu SQL-Performance, Sicherheit und Hochverfügbarkeit – alle frei verfügbar.

Beratung

Festpreis-Analyse mit Bericht und Handlungsempfehlung – oder strategische Begleitung bei Architektur, Migration und Hochverfügbarkeit.

Fachbücher

Die fünfbändige Reihe „Ulis SQL-Bibliothek“ – Band 1 verfügbar. Leseprobe herunterladen!

Tools

UB.SimSQL: SQL-Server-Lastsimulator mit regelbasierten Konfigurationsempfehlungen. Lokal, ohne Cloud, ohne Abo.

Schulungen

Online-Workshops zu Performance, Sicherheit und Entwicklung – kompakt, hands-on, ohne MOC-Folienschlacht.

Result-Set-Design: – SQL Server Performance

von

Dieser Artikel ist ein Kapitel aus:
SQL Server Performance & Troubleshooting
Praxisleitfaden, ca. 600 Seiten

[ Hier bei Amazon bestellen ]
[ Mehr zum Buch ]

Result-Set-Design:

Hol nur was du brauchst — und schick nicht die ganze Datenbank durch die Leitung

Es gibt eine bestimmte Art von Performance-Problem, die sich erst im Netzwerk-Monitor zeigt: SQL Server verarbeitet die Abfrage in 12 ms — und dann schickt er 47 MB durch die Leitung zum Client. Alle 3 Sekunden. Von 200 gleichzeitigen Sessions. Das ist kein SQL Server Problem. Das ist ein Result-Set-Design-Problem.

Result-Set-Design bedeutet: Was gibst du zurück? Welche Spalten, wie viele Zeilen, in welchem Format, wann und wie viel davon? Diese Entscheidungen treffen Entwickler oft implizit — SELECT *, DataTable.Fill(), "wir zeigen dem Nutzer sowieso nur 20 Zeilen" — und zahlen den Preis später in Megabytes, Latenz und Memory Grants.

Bei Trendforge Digital haben wir genau dieses Muster in der Produktion gefunden: eine Abfrage auf die Bestelltabelle holt 50.000 Zeilen, übergibt sie an C#, das dann im Client filtert und aggregiert. SQL Server braucht 18 ms für die Abfrage. Das Netzwerk braucht 2,4 Sekunden für den Transfer. Kapitel 34 beschreibt den vollständigen Befund — hier schauen wir uns die Mechanismen dahinter an.

Projektion: Hol nur die Spalten, die du wirklich brauchst

SELECT * ist das erste und häufigste Anti-Pattern im Result-Set-Design — und gleichzeitig das, das am meisten normalisiert wurde. "Ist doch einfacher", "wir brauchen vielleicht mal alle Spalten", "das macht kein Problem". Macht es doch. Drei davon, die unabhängig voneinander schmerzen. Wir haben SELECT * bereits in Kapitel 23 als Abfrageoptimierungs-Anti-Pattern behandelt — hier betrachten wir die spezifischen Auswirkungen auf den Daten-Transfer und den Server-Speicher.

Problem 1: Netzwerk-Overhead

Bei einer Tabelle mit 47 Spalten holst du im Zweifelsfall 47 Spalten durchs Netzwerk, obwohl die UI nur 5 davon anzeigt. Das klingt harmlos — wird es aber nicht, wenn eine der Spalten NVARCHAR(MAX) für Beschreibungstexte ist, oder eine VARBINARY(MAX) für PDF-Anhänge. Bei Trendforge: eine Produkttabelle, 47 Spalten, davon zwei NVARCHAR(MAX)-Felder (Beschreibung + Freitext). Durchschnittliche Zeilengröße: 4,7 KB. Bei 500 angezeigten Produkten sind das 2,35 MB pro Request — obwohl die Artikelliste nur Name, Preis und Kategorie benötigt.

Problem 2: Buffer Pool Pollution

SQL Server liest Seiten in den Buffer Pool (Kapitel 11). Wenn deine Abfrage alle 47 Spalten abruft, werden alle Seiten dieser Tabelle in den Cache geladen — auch die mit den NVARCHAR(MAX)-Daten, die du gar nicht brauchst. Targeted Queries auf wenige Spalten können schmale Covering Indizes nutzen, die nur die benötigten Felder enthalten — und der Buffer Pool wird mit relevanten Daten gefüllt, nicht mit Ballast.

Problem 3: Memory Grant Explosion

Hier wird es richtig teuer. Der Query Optimizer schätzt den Memory Grant für Sort- und Hash-Operationen auf Basis der projizierten Spaltenbreite. Breite Zeilen bedeuten große Grants. Bei Trendforge: SELECT * auf der Bestelltabelle (23 Spalten, durchschnittlich 380 Bytes pro Zeile) führt bei einem Sort auf 200.000 Zeilen zu einem geschätzten Memory Grant von 2,3 GB — für eine einfache sortierte Liste. Mit einer Projektionsliste auf die tatsächlich benötigten 6 Spalten (42 Bytes pro Zeile) sinkt der Grant auf 260 MB. Faktor 8,8 — für denselben logischen Inhalt. Das ganze Thema Memory Grants und was passiert, wenn sie überlaufen steht in Kapitel 12.

 

Warnung: SELECT * in Stored Procedures

SELECT * in Stored Procedures ist besonders tückisch: Wenn die Tabelle später eine neue Spalte bekommt, ändert sich das Result Set der Stored Procedure — ohne, dass der Code angefasst wurde. Clients die auf feste Spaltenindizes bauen (DataReader.GetValue(5)) brechen dann auf mysteriöse Weise. Das ist kein theoretisches Problem — das passiert bei Schemamigrationen regelmäßig.

Regel: Explizite Spaltenliste in jeder SELECT-Anweisung. Immer.

 

Paging: Nicht alle auf einmal, bitte

Paging ist das offensichtlichste Result-Set-Design-Muster: Zeige dem Nutzer 20 Zeilen statt 50.000. Klingt trivial. Ist es nicht — denn wie du das implementierst, entscheidet darüber ob deine Abfrage O(log n) oder O(n) ist.

OFFSET/FETCH NEXT: Standard, aber mit Tücken

Seit SQL Server 2012 ist OFFSET/FETCH NEXT die offizielle Paging-Syntax. Korrekt, standardkonform, weit verbreitet. Und bei großen Offsets ein stilles Performance-Problem.

-- Standard-Paging mit OFFSET/FETCH (SQL Server 2012+)
-- Seite 1: kein Problem — liest Zeilen 1-20
SELECT BestellNr, KundenID, Betrag, Datum
FROM   dbo.Bestellung
ORDER BY Datum DESC
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

 

-- Seite 501: liest intern 10.020 Zeilen, wirft 10.000 davon weg
-- Das ist O(n) — bei 1 Million Zeilen und Seite 10.000 wird das schmerzhaft
SELECT BestellNr, KundenID, Betrag, Datum
FROM   dbo.Bestellung
ORDER BY Datum DESC
OFFSET 10000 ROWS FETCH NEXT 20 ROWS ONLY;

Das versteckte Problem: SQL Server muss intern die ersten 10.020 Zeilen lesen und sortieren, um die Zeilen 10.001–10.020 zu liefern. Bei einem Index auf Datum läuft er den Index rückwärts — aber er muss trotzdem 10.000 Einträge überspringen. Bei großen Tabellen und tiefen Seiten (Nutzer klickt auf "Seite 500") kann das schnell 200–400 ms kosten, obwohl der Nutzer nur 20 Zeilen sieht.

Keyset Pagination: Der skalierbare Weg

Keyset Pagination — auch Seek Method genannt — löst das Problem durch einen anderen Ansatz: statt dem Offset übergibst du den letzten bekannten Schlüsselwert der vorherigen Seite. SQL Server kann dann direkt an der richtigen Stelle im Index einsteigen — kein Überspringen, keine versteckte O(n)-Arbeit. Das Ergebnis ist immer O(log n), egal auf welcher Seite du bist.

-- Keyset Pagination: immer O(log n) — egal ob Seite 1 oder Seite 10.000

 

-- Erste Seite: kein vorheriger Schlüssel bekannt
SELECT   TOP (20) BestellNr, KundenID, Betrag, Datum
FROM     dbo.Bestellung
ORDER BY Datum DESC, BestellNr DESC;  -- BestellNr als Tiebreaker für Eindeutigkeit

 

-- Folgeseiten: letzten Datum- und BestellNr-Wert der Vorseite übergeben
-- @LetztesDatum und @LetzteBestellNr kommen vom Client (aus dem letzten Response)
SELECT   TOP (20) BestellNr, KundenID, Betrag, Datum
FROM     dbo.Bestellung
WHERE    Datum < @LetztesDatum
         OR (Datum = @LetztesDatum AND BestellNr < @LetzteBestellNr)
ORDER BY Datum DESC, BestellNr DESC;
-- SQL Server springt direkt per Index Seek auf die erste passende Zeile
-- Kein Scan, kein Überspringen von 10.000 Zeilen — direkt die Top 20

 

Tipp: Wann welches Paging?

OFFSET/FETCH ist in Ordnung für: kleine Tabellen (< 50.000 Zeilen), erste 10–20 Seiten, Fälle wo wahlfreier Seitenzugriff ("springe zu Seite 348") erforderlich ist.

Keyset Pagination ist Pflicht für: große Tabellen, tiefe Seiten, Infinite Scroll, APIs mit hohem Durchsatz. Der Nachteil: kein wahlfreier Seitenzugriff — du kannst nur vorwärts blättern (oder mit umgekehrtem Sortierkriterium rückwärts).

 

 

Methode

Komplexität

Wahlfreier Zugriff

Empfehlung

OFFSET/FETCH

O(n) bei tiefem Offset

Ja — Seite beliebig

Kleine Tabellen, frühe Seiten

Keyset Pagination

O(log n) immer

Nein — nur sequenziell

Große Tabellen, tiefe Seiten, APIs

Paging-Methoden im Vergleich

 

Top-N Queries: Wenn der Optimizer mitdenkt

TOP ist mehr als Syntaxzucker für "gib mir die ersten N Zeilen". Der Query Optimizer behandelt TOP-Queries anders — er kann einen Row Goal setzen, der die gesamte Planstrategie verändert.

Row Goal: Der Optimizer denkt in Wahrscheinlichkeiten

Wenn SQL Server sieht, dass du TOP (20) abfragst, kalkuliert er: Wie viele Zeilen muss ich lesen, um wahrscheinlich 20 zu finden? Das Ergebnis ist ein Row Goal — ein interner Schätzwert, auf den der Optimizer optimiert. Mit Row Goal wählt er unter Umständen Nested Loop statt Hash Join, weil Nested Loop die ersten Treffer sehr schnell liefert (gut für TOP 20) — auch, wenn er bei der Gesamtmenge langsamer wäre.

Das kann schiefgehen: Wenn die Selektivität des WHERE-Prädikats schlecht geschätzt wird (veraltete Statistiken, Parameter Sniffing — Kapitel 16 und 18), wählt der Optimizer einen auf "schnelle erste 20 Zeilen" optimierten Plan, der dann aber Millionen Zeilen lesen muss, weil fast alle das WHERE-Kriterium erfüllen. Row Goal-Probleme sehen im Ausführungsplan oft aus wie dramatische Unterschätzungen: Estimated 1 Row, Actual 500.000 Rows.

-- TOP mit ORDER BY: SQL Server nutzt Early Termination, wenn Index die Sortierung liefert
SELECT TOP (10) KundenName, Umsatz
FROM   dbo.Kunde
ORDER BY Umsatz DESC;
-- Mit Index auf Umsatz: Index Scan rückwärts, stoppt nach 10 Zeilen — sehr effizient
-- Ohne Index: Full Table Scan + Sort der gesamten Tabelle — teuer

 

-- TOP WITH TIES: gibt auch Zeilen mit dem gleichen Wert wie der letzte Platz zurück
-- Nützlich für Rankings wo Gleichstand möglich ist
SELECT TOP (3) WITH TIES KundenName, Umsatz
FROM   dbo.Kunde
ORDER BY Umsatz DESC;
-- Gibt ggf. mehr als 3 Zeilen zurück, wenn Platz 3 mehrfach belegt ist

 

Hinweis: TOP ohne ORDER BY ist non-deterministic

SELECT TOP (1) * FROM dbo.Tabelle ohne ORDER BY liefert irgendeine Zeile — welche, hängt vom Ausführungsplan ab, der sich ändern kann. Heute liefert es die neuste Zeile, morgen nach einem Index-Rebuild eine andere. Nur mit explizitem ORDER BY ist das Ergebnis reproduzierbar.

 

Streaming vs. Buffering: Was der Client damit macht

SQL Server liefert das Result Set — was dann auf Clientseite passiert, entscheidet über den Memory-Bedarf der Applikation. Hier gibt es zwei grundlegende Strategien, und die falsche Wahl bei großen Result Sets endet mit OutOfMemoryExceptions in Produktion.

SqlDataReader: Streaming

Der SqlDataReader in .NET ist ein Forward-Only, Read-Only Cursor auf das Result Set. Er holt die Daten nicht auf einmal — er liest sie in Chunks vom Server, während du mit Read() durch die Zeilen iterierst. Bei 500.000 Zeilen hält der Applikationsserver zu keinem Zeitpunkt mehr als einen kleinen Puffer im RAM. Für große Result Sets die richtige Wahl.

DataTable und DataSet: Buffering

DataTable.Load() und SqlDataAdapter.Fill() machen das Gegenteil: Sie materialisieren das gesamte Result Set im Arbeitsspeicher, bevor die erste Zeile verarbeitet werden kann. Bei 500.000 Zeilen à 380 Bytes sind das 190 MB im Heap des Applikationsservers — pro Request. Skaliert nicht. Entity Framework macht standardmäßig dasselbe: die LINQ-Query wird vollständig materialisiert (ToList(), ToArray()), nicht gestreamt.

Mehr dazu in Kapitel 30 (ORM & Applikationsdesign): EF Core bietet AsAsyncEnumerable() für echtes Streaming — wird aber selten genutzt, weil der Default (ToListAsync()) für die meisten Entwickler "einfach funktioniert" — bis die Tabelle 2 Millionen Zeilen hat.

Aggregation gehört auf den Server — nicht in den Client

Das klassische Client-Side-Filtering Anti-Pattern sieht so aus: Die Applikation holt alle Bestellungen des letzten Quartals (50.000 Zeilen), iteriert in einer foreach-Schleife darüber und berechnet die Summe. SQL Server liefert das in 18 ms. Der Transfer der 50.000 Zeilen dauert 2,4 Sekunden. Die foreach-Schleife braucht nochmal 80 ms. Das SQL-Äquivalent mit SUM() braucht insgesamt unter 5 ms — inklusive Netzwerk, weil nur eine einzelne Zeile zurückkommt.

-- Anti-Pattern: Client-Side-Aggregation
-- Alle Zeilen übertragen, Aggregation in der Applikation
-- Ergebnis: 50.000 Zeilen, 47 MB Netzwerk-Traffic, foreach-Schleife in C#
-- SELECT BestellNr, Betrag FROM dbo.Bestellung WHERE Quartal = 1

 

-- Richtig: Aggregation auf dem Server
-- Eine Zeile zurück, < 5 ms end-to-end
SELECT
    COUNT(*)          AS AnzahlBestellungen,
    SUM(Betrag)       AS Gesamtumsatz,
    AVG(Betrag)       AS DurchschnittsBestellung,
    MIN(Betrag)       AS MinBestellung,
    MAX(Betrag)       AS MaxBestellung
FROM   dbo.Bestellung
WHERE  YEAR(Bestelldatum) = @Jahr
       AND DATEPART(QUARTER, Bestelldatum) = @Quartal;
-- SQL Server aggregiert auf dem Index, liefert eine einzige Zeile zurück
-- Netzwerk-Traffic: ein paar Hundert Bytes statt 47 MB

Trendforge: Die Applikation holte täglich um 02:00 Uhr alle Bestellungen der letzten 30 Tage (ca. 150.000 Zeilen, 71 MB), um einen Report zu berechnen. Dieser Nightly Job lief 8 Minuten — davon 7 Minuten und 42 Sekunden Netzwerk-Transfer. Mit serverseitiger Aggregation und GROUP BY: 12 Sekunden für den gesamten Report-Aufbau.

SET NOCOUNT und Multiple Result Sets

Kleine Maßnahme, messbarer Effekt: SET NOCOUNT ON verhindert, dass SQL Server nach jedem INSERT, UPDATE, DELETE eine "X rows affected" Nachricht an den Client schickt. Das sind zusätzliche Netzwerk-Roundtrips — bei einer Stored Procedure die in einer Schleife 1.000 Updates macht, sind das 1.000 zusätzliche Nachrichten, die der Client verarbeiten muss.

-- Immer SET NOCOUNT ON am Anfang jeder Stored Procedure
-- Verhindert die "X rows affected" Nachrichten nach DML-Anweisungen
CREATE OR ALTER PROCEDURE dbo.BestellungVerarbeiten
    @BestellNr INT
AS
SET NOCOUNT ON;  -- Keine "1 row affected" Nachrichten durch die Leitung schicken

 

-- Für einzelne Rückgabewerte: OUTPUT-Parameter statt Result Set
-- Spart einen Roundtrip und macht die Intention klar
CREATE OR ALTER PROCEDURE dbo.NaechsteBestellNrHolen
    @NeueNr INT OUTPUT
AS
SET NOCOUNT ON;
SELECT @NeueNr = NEXT VALUE FOR dbo.BestellNrSequenz;
-- Client liest @NeueNr aus dem OUTPUT-Parameter — kein Result Set nötig

 

Tipp: Multiple Result Sets: sinnvoll, aber mit Bedacht

Eine Stored Procedure kann mehrere SELECT-Anweisungen enthalten und damit mehrere Result Sets zurückgeben — praktisch für Dashboard-Abfragen die Kopf- und Detaildaten in einem Roundtrip holen. Die Kehrseite: der Caller muss wissen in welcher Reihenfolge die Result Sets kommen. Das ist implizit und zerbrechlich — bei jeder Änderung der Prozedur muss auch der Caller angepasst werden. Für stark frequentierte APIs: lieber zwei Aufrufe mit klaren Verträgen als einen Aufruf mit versteckter Reihenfolge.

 

FOR JSON und FOR XML: Serialisierung auf dem Server

SQL Server kann Result Sets direkt als JSON oder XML serialisieren — eine bequeme Möglichkeit, um Roundtrips zu reduzieren oder hierarchische Datenstrukturen in einer Abfrage aufzubauen.

-- FOR JSON PATH: explizite Kontrolle über die JSON-Struktur
-- Praktisch für REST-APIs die direkt JSON zurückgeben sollen
SELECT
    k.KundenNr    AS [kunde.nr],
    k.Name        AS [kunde.name],
    b.BestellNr   AS [bestellung.nr],
    b.Betrag      AS [bestellung.betrag]
FROM   dbo.Kunde k
JOIN   dbo.Bestellung b ON b.KundenID = k.KundenID
WHERE  k.KundenNr = @KundenNr
FOR JSON PATH, ROOT('kunde');
-- Gibt einen einzelnen NVARCHAR(MAX)-String zurück
-- Direkt als HTTP-Response nutzbar — kein Serialisierungsschritt nötig

Der Vorteil: ein Roundtrip, kein Serialisierungsoverhead auf dem Applikationsserver. Der Nachteil: CPU-Last auf SQL Server — der Optimizer kann FOR JSON nicht parallelisieren, und bei komplexen Abfragen mit vielen Spalten wird die JSON-Serialisierung zum Bottleneck. Faustformel: Wenn die Applikation sowieso einen JSON-Serializer hat (was in .NET/Node.js/Python immer der Fall ist), ist FOR JSON nur dann sinnvoll, wenn die Reduktion von Roundtrips den CPU-Overhead aufwiegt — also bei vielen kleinen Abfragen, nicht bei wenigen großen.

 

Serialisierungsort

CPU-Last

Netzwerk

Flexibilität

Empfehlung

SQL Server (FOR JSON)

SQL Server

Gering (1 String)

Mittel

Viele kleine Requests, simple Strukturen

Applikationsserver

App-Server

Höher (Rohdaten)

Hoch

Komplexe Strukturen, hohe Query-Last

FOR JSON auf Server vs. Serialisierung im Applikationsserver

 

Diagnose: Zu große Result Sets erkennen und beheben

 

Warnung: Symptome

Netzwerk-Traffic zu SQL Server ist hoch, obwohl die Abfragen selbst schnell sind (< 50 ms). Im Netzwerk-Monitor: große Paketmengen von SQL Server zum Applikationsserver.

Memory Grants für scheinbar einfache SELECT-Abfragen liegen im GB-Bereich — Hinweis auf breite Projektion und Sort-Operationen.

Client-Applikation ist langsam, obwohl SQL Server-Queries im Profiler schnell aussehen. Der Engpass liegt im Transfer, nicht in der Verarbeitung.

Trendforge-Symptom: 2,3 GB Memory Grant für eine einfache Bestellungsabfrage. Ursache: SELECT * auf eine 23-spaltige Tabelle mit NVARCHAR(MAX)-Feldern.

Applikationsserver-Speicher wächst stetig bei Last — Zeichen für ungepufferte Materialisierung großer Result Sets (DataTable.Fill auf 100.000 Zeilen).

 

 

Hinweis: So misst du das

— Queries mit hohem IO pro Ausführung identifizieren

— Hohe logische Reads oft Zeichen für zu breite Projektionen oder fehlende Filter

SELECT TOP (20)

SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,

((CASE qs.statement_end_offset

WHEN -1 THEN DATALENGTH(qt.text)

ELSE qs.statement_end_offset

END – qs.statement_start_offset)/2)+1) AS query_text,

qs.execution_count,

qs.total_logical_reads / qs.execution_count AS avg_logical_reads,

qs.total_elapsed_time / qs.execution_count / 1000 AS avg_ms,

qs.total_rows / qs.execution_count AS avg_rows_returned

FROM sys.dm_exec_query_stats qs

CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt

ORDER BY avg_logical_reads DESC;

— avg_rows_returned > 10.000 bei avg_ms < 100: Result Set zu groß, nicht die Query

— Kandidaten für Paging, Projektion oder serverseitige Aggregation

 

 

Hintergrund: Typische Fehlinterpretationen

"Paging mit OFFSET ist Standard und performant." — Stimmt für die ersten Seiten. Bei OFFSET 50.000 liest SQL Server intern 50.020 Zeilen und wirft 50.000 davon weg. Das ist kein Bug, das ist by design — und der Grund warum Keyset Pagination für tiefe Seiten Pflicht ist.

"Aggregation im Client ist flexibler." — Flexibler, ja. Aber sie kostet Netzwerk (alle Rohdaten übertragen) und Client-Memory (alle Zeilen im Heap). SUM(Betrag) auf dem Server kostet einen Index Scan und liefert eine Zeile. SUM in C# kostet denselben Index Scan plus 50.000 Zeilen durch die Leitung.

"Result Sets sind nur ein Applikationsproblem." — Falsch. Breite Projektionen beeinflussen Memory Grants (Kapitel 12) und Buffer Pool Nutzung (Kapitel 11) direkt auf SQL Server. Das ist kein reines Netzwerk- oder Applikationsproblem.

"FOR JSON ist immer besser als separate Serialisierung." — FOR JSON reduziert Roundtrips, verlagert aber CPU-Last auf SQL Server — den teuersten und am schwersten skalierbaren Teil des Stacks. Bei hoher Query-Last lieber auf dem Applikationsserver serialisieren.

 

 

Tipp: Erste Gegenmaßnahmen

1. SELECT * durch explizite Spaltenliste ersetzen — in allen Stored Procedures, Views und ORM-Queries. Bei EF Core: explizite .Select()-Projektion statt ToList() auf dem vollständigen Entity.

2. OFFSET/FETCH durch Keyset Pagination ersetzen wo tiefe Seiten möglich sind (> Seite 50, Tabellen > 100.000 Zeilen).

3. SET NOCOUNT ON in alle Stored Procedures einfügen — sofort, ohne Analyse, kein Risiko.

4. Client-seitige Aggregation auf den Server verlagern: jede foreach-Schleife die SUM/COUNT/MIN/MAX berechnet, ist ein Kandidat für eine GROUP BY Abfrage.

5. SqlDataReader statt DataTable.Fill() für große Result Sets — oder EF Core AsAsyncEnumerable() statt ToListAsync().

 

Zusammenfassung

Result-Set-Design ist die stille Performance-Dimension, die in vielen Projekten komplett ignoriert wird — bis der Netzwerk-Traffic explodiert oder der Applikationsserver mit OutOfMemoryException aufgibt. Die Kernregel ist simpel: Hol nur was du brauchst, aggregiere auf dem Server, und, wenn du pagst, dann mit einer Methode die skaliert.

SELECT * ist das Anti-Pattern mit den meisten Nebenwirkungen: Netzwerk-Overhead, Buffer Pool Pollution und überdimensionierte Memory Grants — drei verschiedene Kostenstellen für eine einzige schlechte Entscheidung. OFFSET/FETCH ist praktisch, hat aber bei tiefen Seiten eine versteckte O(n)-Komplexität — Keyset Pagination löst das eleganter. Aggregation im Client ist das teuerste Netzwerk-Muster überhaupt: alle Rohdaten transferieren, um dann eine einzige Zahl zu berechnen.

Die gute Nachricht: Diese Muster sind leicht zu finden (sys.dm_exec_query_stats, avg_rows_returned) und meistens schnell zu beheben. Kein Schema-Change, kein großes Refactoring — oft reicht das Hinzufügen einer Spaltenliste oder das Ersetzen eines OFFSET durch eine WHERE-Bedingung.

 

Im nächsten Kapitel (25) geht es um Batch-Verarbeitung vs. Chatty Apps — das andere Ende des Spektrums. Wenn Result-Set-Design darüber entscheidet was du pro Abfrage zurückholst, entscheidet Batch-Design darüber wie viele Abfragen du überhaupt machst. Spoiler: N+1-Queries auf einer Tabelle mit 500 Produkten sind das Result-Set-Design-Problem auf maximaler Lautstärke.

 

Abb. 1: Paging-Strategien im Vergleich

 

Kapitel 25