SELECT *: warum es in Produktivcode vermieden wird
AI generated
SELECT
JOIN
SQL / Best Practices
SELECT *
warum es in Produktivcode konsequent vermieden wird

Kaum eine SQL-Gewohnheit ist so bequem und gleichzeitig so umstritten wie SELECT *. Für eine schnelle Abfrage in der Konsole spart der Stern Tipparbeit und liefert sofort alle verfügbaren Informationen. Sobald dieselbe Abfrage aber in Anwendungscode landet, der wiederholt ausgeführt wird und über Jahre gepflegt werden muss, kippt die Rechnung: unnötig übertragene Daten, stillschweigend brechender Code bei Schema-Änderungen und verschenktes Optimierungspotenzial durch verhinderte Covering-Indizes. Dieser Artikel geht die konkreten Probleme durch, benennt die legitimen Ausnahmen und zeigt, wie sich das Muster im Code Review zuverlässig abfangen lässt.

10 Min. Lesezeit SELECT * Query-Performance Code Review

1. Unnötiger Netzwerk- und I/O-Overhead durch überflüssige Spalten

Der offensichtlichste Nachteil von SELECT * ist, dass die Datenbank jede einzelne Spalte einer Tabelle liest und über die Verbindung an den Client überträgt, unabhängig davon, ob die Anwendung sie tatsächlich benötigt. Bei einer Tabelle mit einer großen Textspalte, etwa einer Produktbeschreibung oder einem serialisierten JSON-Blob, kann eine Abfrage, die eigentlich nur die ID und den Namen braucht, ein Vielfaches der tatsächlich benötigten Datenmenge übertragen.

Dieser Overhead summiert sich bei Abfragen, die häufig ausgeführt werden, spürbar auf: mehr Netzwerkbandbreite, mehr Serialisierungsaufwand auf der Datenbankseite, mehr Deserialisierung auf der Anwendungsseite, und mehr Speicherverbrauch für Objekte, die nie verwendet werden. Bei einer einzelnen Abfrage auf einer kleinen Tabelle ist das kaum messbar, bei tausenden Abfragen pro Sekunde auf einer breiten Tabelle wird daraus ein echter Kostenfaktor.

Besonders ins Gewicht fällt das bei Datenbank-I/O selbst: Wenn eine Zeile Spalten enthält, die nicht Teil des angefragten Index sind, muss die Datenbank zusätzlich zur Indexstruktur auch die eigentliche Tabellenzeile lesen, ein sogenannter Heap-Zugriff. Diese zusätzliche I/O-Operation entfällt komplett, wenn nur die tatsächlich benötigten, im Index bereits enthaltenen Spalten angefragt werden.


-- Überträgt auch große, ungenutzte Spalten wie description und metadata
SELECT * FROM products WHERE category_id = 42;

-- Überträgt nur die tatsächlich benötigten Spalten
SELECT id, name, price FROM products WHERE category_id = 42;

2. Wie Schema-Änderungen bei SELECT * stillschweigend Anwendungscode brechen

Ein subtileres, aber gefährlicheres Problem zeigt sich erst bei einer späteren Schema-Änderung. Wird der Tabelle eine neue Spalte hinzugefügt, liefert SELECT * ab sofort ein zusätzliches Feld, ohne dass sich am SQL-Code etwas ändert. Anwendungscode, der die Ergebniszeile anhand ihrer Position statt ihres Spaltennamens verarbeitet, etwa in vielen älteren Datenzugriffsschichten oder bei manuellem Array-Zugriff, verschiebt sich dadurch komplett und liest plötzlich falsche Werte in falsche Felder ein, oft ohne sofortigen Fehler.

Noch tückischer ist der umgekehrte Fall: Wird eine Spalte entfernt oder umbenannt, die der Anwendungscode über ihren Namen erwartet, schlägt der Zugriff erst zur Laufzeit fehl, wenn der entsprechende Codepfad tatsächlich durchlaufen wird, unter Umständen erst Wochen nach dem eigentlichen Schema-Change und weit entfernt vom Ort der Änderung. Eine explizite Spaltenliste hätte diesen Fehler sofort und lokal beim nächsten Deployment sichtbar gemacht, weil die Abfrage selbst fehlschlägt, statt stillschweigend falsche Daten zu liefern.

Dieses Problem verschärft sich in Microservice-Architekturen, in denen mehrere Dienste dieselbe Datenbank oder Tabelle lesen. Ein Team, das eine Spalte hinzufügt oder entfernt, hat oft keinen vollständigen Überblick darüber, welche anderen Dienste implizit über SELECT * davon betroffen sind, was Schema-Änderungen unnötig riskant macht.


-- Anwendungscode liest Spalten per Index-Position:
-- row[0] = id, row[1] = name, row[2] = email
SELECT * FROM users WHERE id = 7;

-- Wird "phone" als neue Spalte VOR "email" eingefügt,
-- landet die Telefonnummer plötzlich in row[2] statt der E-Mail-Adresse

-- Explizite Liste bleibt stabil, unabhängig von der Spaltenreihenfolge
SELECT id, name, email FROM users WHERE id = 7;

3. Verhinderte Covering-Index-Nutzung als versteckter Performance-Verlust

Ein Covering Index enthält alle Spalten, die eine Abfrage benötigt, sodass die Datenbank das Ergebnis vollständig aus dem Index selbst liefern kann, ohne die eigentliche Tabellenzeile zusätzlich lesen zu müssen. Das spart genau jenen Heap-Zugriff, der oben beschrieben wurde, und kann eine Abfrage um eine Größenordnung beschleunigen, insbesondere bei Tabellen, deren Zeilen nicht mehr komplett im Speicher-Cache liegen.

SELECT * verhindert diese Optimierung praktisch immer, weil es implizit jede Spalte der Tabelle anfragt. Selbst wenn ein Index alle für die WHERE-Klausel relevanten Spalten enthält, muss die Datenbank trotzdem die vollständige Zeile nachladen, sobald auch nur eine einzige zusätzliche Spalte verlangt wird, die nicht Teil des Index ist, und bei SELECT * ist das praktisch immer der Fall.

Eine explizite Spaltenliste erlaubt es dagegen, gezielt Indizes zu entwerfen, die exakt die in einer häufig ausgeführten Abfrage benötigten Spalten abdecken. Dieser Zusammenhang zwischen Abfrage-Design und Index-Design wird bei SELECT * praktisch unmöglich, weil die Menge der benötigten Spalten für die Datenbank gar nicht sichtbar ist, sie muss immer von allen ausgehen.


-- Covering Index für eine häufige Abfrage
CREATE INDEX idx_orders_customer_covering
  ON orders(customer_id, status, total_cents);

-- Kann vollständig aus dem Index bedient werden, kein Heap-Zugriff
SELECT customer_id, status, total_cents
FROM orders WHERE customer_id = 501;

-- Erzwingt trotz passendem Index einen zusätzlichen Heap-Zugriff,
-- weil weitere Spalten wie shipping_address nachgeladen werden müssen
SELECT * FROM orders WHERE customer_id = 501;

4. Implizite Verträge und Typüberraschungen durch SELECT *

Ein weiteres, oft übersehenes Problem betrifft die Typsicherheit auf Anwendungsseite. In statisch typisierten Sprachen mit Code-Generierung aus dem Datenbankschema, etwa über ORM-Tools, erzeugt eine explizite Spaltenliste ein klares, überprüfbares Ergebnisobjekt. SELECT * erzeugt dagegen ein Ergebnisobjekt, dessen Form implizit vom aktuellen Tabellenschema abhängt, was Compile-Time-Prüfungen entwertet, wenn sich das Schema ändert, ohne dass der generierte Code neu erzeugt wird.

Auch bei UNION-Abfragen über mehrere Tabellen mit SELECT * entsteht ein zusätzliches Risiko: Beide Tabellen müssen exakt dieselbe Spaltenanzahl und kompatible Typen in derselben Reihenfolge liefern. Fügt eine der beiden Tabellen eine neue Spalte hinzu, während die andere unverändert bleibt, schlägt die UNION-Abfrage mit einem Fehler über eine falsche Spaltenanzahl fehl, ein Fehler, der mit einer expliziten, bewusst abgestimmten Spaltenliste von vornherein ausgeschlossen wäre.

Schließlich erschwert SELECT * auch das Lesen und Verstehen von Code im Review: Eine explizite Spaltenliste dokumentiert unmittelbar, welche Daten eine Funktion tatsächlich benötigt, während SELECT * diese Information verschleiert und einen Blick ins Schema erfordert, um zu verstehen, was die Abfrage eigentlich zurückliefert.

5. SELECT * bei JOINs: doppelte und mehrdeutige Spaltennamen

Bei einer Abfrage über mehrere Tabellen verschärft sich das Problem von SELECT * zusätzlich. Enthalten zwei gejointe Tabellen eine gleichnamige Spalte, etwa beide eine Spalte id oder created_at, liefert SELECT * beide Spalten unter demselben Namen zurück. Viele Datenzugriffsschichten, die das Ergebnis in ein assoziatives Array oder ein Objekt mit benannten Feldern abbilden, überschreiben dabei stillschweigend den Wert der ersten Spalte mit dem Wert der zweiten, ohne jede Fehlermeldung.

Dieses Verhalten ist besonders tückisch, weil es von der konkreten Reihenfolge der Tabellen im JOIN und der internen Verarbeitung des Datenbanktreibers abhängt und sich daher zwischen unterschiedlichen Datenbanksystemen oder sogar Treiberversionen unterscheiden kann. Ein Test, der lokal unauffällig grün bleibt, kann in einer anderen Umgebung plötzlich das falsche id-Feld liefern, ein Klassiker schwer reproduzierbarer Produktionsfehler.

Eine explizite Spaltenliste mit Tabellen-Alias und, wo nötig, individuellen Spalten-Aliassen löst dieses Problem grundsätzlich: order.id AS order_id und customer.id AS customer_id sind eindeutig benannt und können nie miteinander verwechselt werden, unabhängig von der Join-Reihenfolge oder dem verwendeten Datenbanktreiber.


-- Beide Tabellen haben eine Spalte "id": Ergebnis ist mehrdeutig
SELECT * FROM orders o
JOIN customers c ON c.id = o.customer_id;

-- Eindeutig benannt, unabhängig von Join-Reihenfolge oder Treiber
SELECT o.id AS order_id, c.id AS customer_id, o.total_cents
FROM orders o
JOIN customers c ON c.id = o.customer_id;

6. Legitime Ausnahmen, in denen SELECT * akzeptabel bleibt

Nicht jeder Einsatz von SELECT * ist ein Fehler. Beim Ad-hoc-Debugging in einer interaktiven Datenbank-Konsole, wenn ein Entwickler schnell den Inhalt einer unbekannten Tabelle sichten will, ist der Stern die pragmatische Wahl, weil hier weder Performance noch langfristige Wartbarkeit relevant sind, die Abfrage wird einmal ausgeführt und danach verworfen.

Auch bei EXISTS-Unterabfragen, bei denen der Datenbankoptimierer ohnehin erkennt, dass die konkrete Spaltenliste für das Ergebnis irrelevant ist, weil nur die Existenz einer Zeile geprüft wird, ist SELECT 1 oder SELECT * praktisch gleichwertig, da moderne Optimierer die Spaltenliste in diesem Kontext ignorieren und keine echten Daten übertragen.

Ein dritter legitimer Fall ist eine generische Backup-, Replikations- oder Migrationsroutine, deren gesamter Zweck darin besteht, wirklich jede Spalte einer Tabelle unverändert zu übernehmen. Hier ist SELECT * nicht nur akzeptabel, sondern sogar die korrektere Wahl, weil eine explizite Liste bei einer künftigen Schema-Änderung genau die neue Spalte übersehen würde, die eigentlich mitkopiert werden sollte.

7. Praktische Linting- und Review-Strategie gegen SELECT *

Statische SQL-Linter erkennen SELECT * zuverlässig als Muster und lassen sich in CI-Pipelines als Pflichtprüfung einbinden, die einen Merge-Request blockiert, solange die Regel verletzt wird. Viele SQL-Linter erlauben dabei eine gezielte Ausnahme-Kennzeichnung im Code, etwa über einen Kommentar, mit dem ein Entwickler bewusst begründet, warum an dieser Stelle SELECT * beabsichtigt ist, statt die Regel pauschal zu deaktivieren.

Für ORM-basierte Codebasen lohnt sich zusätzlich eine Konfigurationsprüfung, ob das ORM standardmäßig alle Spalten lädt, sogenanntes Eager Loading aller Felder, oder ob es projektionsbasiertes Laden unterstützt, bei dem nur explizit angeforderte Felder in die generierte Abfrage einfließen. Viele moderne ORMs bieten diese Option, sie ist aber häufig nicht die Standardeinstellung und muss aktiv konfiguriert werden.

Im Code Review selbst hilft eine einfache Faustregel: Jede neue oder geänderte Abfrage, die in wiederholt ausgeführtem Anwendungscode landet, sollte eine explizite Spaltenliste tragen, es sei denn, es handelt sich nachweislich um einen der oben genannten legitimen Ausnahmefälle. Diese Regel lässt sich in einem Team-Styleguide festhalten und im Review konsequent einfordern, statt sie von Fall zu Fall neu zu diskutieren.


-- Bewusste, dokumentierte Ausnahme statt pauschaler Regelverletzung
-- lint-disable-next-line select-star -- Backup-Routine, muss alle Spalten
-- inklusive künftiger Erweiterungen unverändert kopieren
SELECT * FROM archive_source;

8. Auswirkung auf Abfrageplan-Caching und Statistiken

Datenbanksysteme cachen häufig ausgeführte Abfragepläne, um wiederholte Optimierungsarbeit zu vermeiden. Bei SELECT * ist der resultierende Plan direkt an das aktuelle Schema gekoppelt: Ändert sich die Spaltenanzahl, muss der gecachte Plan verworfen und neu berechnet werden, was bei stark frequentierten Abfragen kurzzeitig zu erhöhter Last führen kann, unmittelbar nach jeder Schema-Änderung.

Bei einer expliziten Spaltenliste bleibt der Plan dagegen stabil, solange sich die referenzierten Spalten selbst nicht ändern, unabhängig davon, ob an anderer Stelle der Tabelle neue Spalten hinzukommen. Das reduziert die Zahl der Momente, in denen ein Schema-Deployment unerwartete Plan-Neuberechnungen und damit kurzzeitige Latenzspitzen auslöst.

Auch für die Statistikpflege spielt das eine Rolle: Ein Optimierer, der weiß, dass eine Abfrage nur drei bestimmte Spalten benötigt, kann gezielter entscheiden, für welche Spalten detaillierte Histogramme wichtig sind. SELECT * liefert hier keine solche Information und zwingt den Optimierer, konservativ von einem breiteren Zugriffsmuster auszugehen.

9. Migrationspfad für bestehenden Code mit vielen SELECT *-Stellen

In gewachsenen Codebasen mit hunderten SELECT *-Vorkommen lohnt sich kein abrupter Komplett-Umbau, sondern eine priorisierte Vorgehensweise. Zuerst werden Abfragen identifiziert, die in Hochlast-Pfaden liegen oder auf Tabellen mit großen, selten benötigten Spalten zugreifen, denn hier ist der Performance-Gewinn am größten. Diese werden zuerst umgeschrieben und mit einem Vorher-Nachher-Vergleich der übertragenen Datenmenge dokumentiert.

Für den restlichen Bestand hilft ein einfacher technischer Kunstgriff: Der Linter wird zunächst nur für neuen und geänderten Code aktiviert, nicht rückwirkend für die gesamte Codebasis. So entsteht kein Blockade-Aufwand für unveränderte Legacy-Abfragen, während jede zukünftige Änderung an einer Abfrage automatisch den Zwang zur expliziten Spaltenliste mitbringt und der Bestand organisch schrumpft.

Wichtig ist, diese Umstellung mit ausreichender Testabdeckung abzusichern, insbesondere wenn Anwendungscode bislang implizit von einer bestimmten Spaltenreihenfolge oder von zusätzlichen, ungenutzten Feldern abhängt. Ein automatisierter Vergleich der Antwortstruktur vor und nach der Umstellung verhindert, dass die Migration selbst neue, subtile Fehler einführt.

Kriterium SELECT * Explizite Spaltenliste
Übertragene Datenmenge immer alle Spalten nur benötigte Spalten
Verhalten bei Schema-Änderung stillschweigend andere Ergebnisform Fehler bei fehlender Spalte, sofort sichtbar
Covering-Index-Nutzung praktisch ausgeschlossen gezielt möglich
Lesbarkeit im Code Review Datenbedarf unklar Datenbedarf explizit dokumentiert
Plan-Cache-Stabilität bricht bei neuer Spalte bleibt bei neuer Spalte stabil

Mironsoft

Datenbank-Optimierung, Query-Tuning und Migrationen

SQL-Abfragen, die bei Wachstum immer langsamer werden?

Wir analysieren und optimieren SQL-Datenbanken unabhängig vom eingesetzten System, planen sichere Migrationen und Schema-Änderungen und bringen Teams Query-Optimierung praxisnah bei.

Query-Optimierung

Langsame Abfragen analysieren und mit Indizes und Explain-Plänen gezielt beschleunigen.

Migrations-Planung

Schema-Änderungen und Datenmigrationen sicher und ohne Downtime umsetzen.

Team-Schulung

SQL-Grundlagen und Performance-Denken praxisnah im Entwicklerteam verankern.

10. Zusammenfassung

SELECT * vermeiden: Das Wichtigste auf einen Blick

Kernproblem

SELECT * überträgt und dekodiert immer alle Spalten, unabhängig vom tatsächlichen Bedarf.

Größtes Risiko

Schema-Änderungen brechen positionsbasierten Anwendungscode stillschweigend.

Legitime Ausnahme

Ad-hoc-Debugging und generische Backup-/Replikationsroutinen.

Gegenmaßnahme

SQL-Linter in der CI-Pipeline mit dokumentierter Ausnahme-Kennzeichnung.

11. FAQ: SELECT * vermeiden: Das Wichtigste auf einen Blick

1Ist SELECT * bei kleinen Tabellen unproblematisch?
Die Performance-Auswirkung ist bei kleinen Tabellen gering, das Risiko durch Schema-Änderungen bleibt aber unverändert bestehen. Auch bei kleinen Tabellen kann eine neue oder umbenannte Spalte positionsbasierten Anwendungscode brechen.
2Warum verhindert SELECT * die Nutzung eines Covering Index?
Ein Covering Index deckt nur die tatsächlich benötigten Spalten ab. SELECT * fragt implizit jede Spalte der Tabelle an, sodass die Datenbank fast immer zusätzlich zur Indexstruktur die vollständige Tabellenzeile nachladen muss.
3Ist SELECT COUNT(*) genauso problematisch wie SELECT *?
Nein. COUNT(*) zählt Zeilen und überträgt keine Spaltendaten, der Optimierer kann dafür meist den kleinsten verfügbaren Index nutzen. Das Problem betrifft ausschließlich SELECT * als Projektion aller Spalten einer Zeile.
4Wie erkenne ich SELECT * in einer großen, gewachsenen Codebasis?
Statische SQL-Linter erkennen das Muster automatisiert und lassen sich in die CI-Pipeline integrieren. Für einen ersten Überblick genügt oft schon eine einfache textbasierte Suche nach dem Muster SELECT * im Quellcode.
5Sollte ich SELECT * in EXISTS-Unterabfragen ebenfalls vermeiden?
Nicht zwingend. Moderne Optimierer ignorieren die konkrete Spaltenliste in einer EXISTS-Bedingung ohnehin, weil nur die Existenz einer Zeile geprüft wird. SELECT 1 ist hier aus Lesbarkeitsgründen verbreitet, funktional aber gleichwertig zu SELECT *.
6Wie gehe ich mit hunderten bestehenden SELECT *-Stellen in Legacy-Code um?
Priorisiert nach Performance-Relevanz umschreiben, den Linter zunächst nur für neuen und geänderten Code aktivieren, und die Umstellung mit ausreichender Testabdeckung gegen unerwartete Verhaltensänderungen absichern.
7Verlangsamt SELECT * auch einfache Abfragen ohne Join spürbar?
Bei schmalen Tabellen mit wenigen, kleinen Spalten ist der Effekt gering. Bei breiten Tabellen mit großen Text- oder Blob-Spalten kann bereits eine einfache Abfrage ohne Join deutlich mehr Daten übertragen als nötig.
8Gilt das Problem auch für ORM-Frameworks, die SELECT * intern generieren?
Ja, das Problem ist identisch, nur eine Ebene weiter unten versteckt. Viele ORMs erlauben projektionsbasiertes Laden, bei dem nur explizit angeforderte Felder abgefragt werden, diese Option muss aber häufig aktiv konfiguriert werden.
9Was ist der Unterschied zwischen SELECT * für Backup-Zwecke und SELECT * in Anwendungscode?
Bei einer generischen Backup- oder Replikationsroutine ist die Absicht, wirklich jede Spalte unverändert zu kopieren, wodurch SELECT * korrekt ist. In Anwendungscode ist meist nur eine Teilmenge der Spalten tatsächlich relevant, wodurch der Stern unnötigen Overhead erzeugt.
10Kann ein SQL-Linter legitime Ausnahmen von der SELECT-*-Regel erlauben?
Ja, die meisten SQL-Linter unterstützen eine gezielte Ausnahme-Kennzeichnung direkt im Code, meist über einen speziellen Kommentar, mit dem ein Entwickler die bewusste Abweichung von der Regel dokumentiert und begründet.