warum das Kämpfen gegen Lücken meist unnötiger Aufwand ist
Kaum ein Verhalten von Auto-Increment-Sequenzen verunsichert Entwickler so zuverlässig wie eine ID-Spalte mit fehlenden Werten. Der Reflex, das als Fehler zu behandeln und mit komplexer Zusatzlogik zu beheben, führt fast immer zu mehr Problemen als er löst: gesperrten Tabellen, blockierten Transaktionen und schlechterer Skalierbarkeit. Dieser Artikel erklärt, warum Lücken eine strukturell unvermeidbare Folge von Rollbacks, Sequence-Caching und paralleler Transaktionsausführung sind, warum das in der weit überwiegenden Zahl der Fälle vollkommen unproblematisch ist, und wo die seltenen, legitimen Ausnahmen liegen.
Inhaltsverzeichnis
- 1. Die typische Beobachtung: fehlende IDs in einer Auto-Increment-Spalte
- 2. Warum Sequenzen bewusst nicht-transaktional funktionieren
- 3. Rollbacks als häufigste Ursache für Lücken im Alltag
- 4. Sequence-Caching als zweite, oft unterschätzte Ursache
- 5. Parallele Transaktionen und die Reihenfolge von Commits
- 6. Einen echten Bug von einer harmlosen, normalen Lücke unterscheiden
- 7. Warum das Kämpfen gegen Lücken meist unnötiger Aufwand ist
- 8. Seltene, legitime Ausnahmen: wenn Lückenlosigkeit fachlich gefordert ist
- 9. Wie man lückenlose Nummern sauber getrennt vom Primärschlüssel löst
- 10. Zusammenfassung
- 11. FAQ
1. Die typische Beobachtung: fehlende IDs in einer Auto-Increment-Spalte
Ein Blick in eine Tabelle mit Auto-Increment-Primärschlüssel zeigt gelegentlich eine ID-Folge wie 1001, 1002, 1005, 1006, obwohl niemand aktiv Zeilen gelöscht hat. Die naheliegende Vermutung ist ein Bug in der Anwendung oder ein Fehler in der Datenbank. Tatsächlich handelt es sich um ein vollkommen normales, gut dokumentiertes Verhalten, das aus der Funktionsweise von Sequenzen und Transaktionen folgt, nicht aus einem Defekt.
Der zentrale Denkfehler liegt in der Annahme, eine Sequence sei eine transaktionale Ressource, die bei einem Rollback ebenfalls zurückgesetzt wird. Genau das Gegenteil ist der Fall, und dieser Unterschied erklärt praktisch jedes beobachtete Lücken-Phänomen.
2. Warum Sequenzen bewusst nicht-transaktional funktionieren
Eine Sequence vergibt Werte außerhalb des normalen Transaktionskontexts, damit mehrere parallele Transaktionen gleichzeitig neue, garantiert eindeutige Werte anfordern können, ohne sich gegenseitig zu blockieren. Würde die Vergabe eines Sequence-Werts erst mit dem Commit der aufrufenden Transaktion endgültig, müsste jede andere Transaktion, die ebenfalls einen neuen Wert braucht, warten, bis die erste Transaktion committet oder zurückrollt. Bei hoher Nebenläufigkeit würde das die Sequence zu einem harten Flaschenhals machen.
Weil die Vergabe stattdessen sofort und unabhängig vom weiteren Schicksal der Transaktion erfolgt, bleibt ein einmal vergebener Wert für immer verbraucht, selbst wenn die zugehörige Transaktion später zurückgerollt wird. Diese bewusste Design-Entscheidung tauscht garantierte Lückenlosigkeit gegen hohe Nebenläufigkeit, und für die überwältigende Mehrheit der Anwendungsfälle ist das ein sehr guter Tausch.
BEGIN;
INSERT INTO bestellung (kunde_id) VALUES (42); -- erhält z.B. id=1005
ROLLBACK; -- Transaktion wird verworfen
BEGIN;
INSERT INTO bestellung (kunde_id) VALUES (43); -- erhält id=1006, NICHT 1005
COMMIT;
3. Rollbacks als häufigste Ursache für Lücken im Alltag
Jede Transaktion, die einen neuen Sequence-Wert anfordert und anschließend aus fachlichen oder technischen Gründen zurückrollt, etwa weil eine nachgelagerte Validierung fehlschlägt, ein Constraint verletzt wird oder die Anwendung eine Exception wirft, hinterlässt eine dauerhafte Lücke. In produktiven Systemen mit Validierungslogik, optimistischem Sperren oder externen Abhängigkeiten innerhalb einer Transaktion ist ein gewisser Anteil fehlschlagender Transaktionen völlig normal.
Je höher die Rate fehlschlagender oder bewusst zurückgerollter Transaktionen in einem System ist, desto mehr Lücken entstehen. Das ist kein Qualitätsmerkmal der Datenbank, sondern eine direkte, erwartbare Konsequenz aus der Menge an Transaktionen, die aus gutem Grund nicht durchkommen.
4. Sequence-Caching als zweite, oft unterschätzte Ursache
Viele Datenbanksysteme cachen aus Performance-Gründen ganze Blöcke von Sequence-Werten pro Datenbankverbindung oder Session, statt bei jeder Anfrage einen einzelnen Wert mit entsprechendem Synchronisationsaufwand zu vergeben. Wird eine Verbindung beendet oder die Datenbank neu gestartet, bevor der gecachte Block vollständig verbraucht wurde, gehen die verbleibenden, bereits reservierten Werte dieses Blocks endgültig verloren.
Dieses Verhalten ist unabhängig von Transaktions-Rollbacks und tritt selbst bei Systemen mit einer Erfolgsquote von hundert Prozent auf, sobald Verbindungen regelmäßig neu aufgebaut werden, was in modernen Anwendungen mit Connection Pooling und Autoscaling der Regelfall ist. Wer Cache-Größe und Neustart-Häufigkeit kennt, kann die ungefähre Größenordnung der dadurch entstehenden Lücken grob abschätzen, sie aber nicht verhindern, ohne den Performance-Vorteil des Caching aufzugeben.
-- Sequence mit Caching von 20 Werten pro Session
CREATE SEQUENCE bestellung_id_seq
START WITH 1000
INCREMENT BY 1
CACHE 20;
-- Bei Verbindungsabbruch nach nur 3 verbrauchten Werten
-- gehen die restlichen 17 reservierten Werte verloren
5. Parallele Transaktionen und die Reihenfolge von Commits
Auch ohne einen einzigen Rollback erzeugen parallele Transaktionen ein weiteres, subtileres Phänomen: Die Reihenfolge, in der Sequence-Werte vergeben werden, muss nicht mit der Reihenfolge übereinstimmen, in der die zugehörigen Transaktionen tatsächlich committen. Eine Transaktion, die zuerst einen Sequence-Wert anfordert, kann durch längere Verarbeitungszeit später committen als eine zweite Transaktion, die einen höheren Wert erhalten, aber schneller abgeschlossen hat.
Für Anwendungscode bedeutet das: Eine ID-Spalte darf niemals als verlässlicher Indikator für die tatsächliche zeitliche Reihenfolge von Commits verwendet werden, auch wenn keine einzige Lücke vorliegt. Für eine belastbare zeitliche Sortierung ist ein dedizierter Zeitstempel mit ausreichender Auflösung die korrekte Lösung, nicht der Rückschluss aus der ID.
6. Einen echten Bug von einer harmlosen, normalen Lücke unterscheiden
Nicht jede Lücke ist automatisch harmlos, weshalb es sinnvoll ist, eine normale Lücke von einem tatsächlichen Bug unterscheiden zu können. Eine normale Lücke korreliert mit einer plausiblen Ursache: einem protokollierten Validierungsfehler, einem bekannten Rollback, einem neu gestarteten Connection Pool oder einem Schub paralleler Schreibvorgänge rund um den betroffenen ID-Bereich. Zeigen Anwendungs- oder Transaktionslogs ein passendes Ereignis zum ungefähr selben Zeitpunkt, handelt es sich um erwartetes Verhalten, nicht um einen Defekt.
Verdächtig wird eine Lücke, wenn sie ohne jeden zugehörigen Rollback, Fehlerlog-Eintrag oder Verbindungsereignis auftritt, insbesondere wenn sie sich in einem festen, unplausiblen Muster wiederholt. In diesem Fall ist die wahrscheinlichere Erklärung ein Anwendungsfehler, der IDs außerhalb der Datenbank erzeugt und verwirft, etwa weil die Anwendungsschicht selbst einen Block von Sequence-Werten vorab abruft und nur einen Teil davon tatsächlich verwendet, wodurch genau das oben beschriebene Caching-Problem unter anderer Kontrolle erneut entsteht.
7. Warum das Kämpfen gegen Lücken meist unnötiger Aufwand ist
Der Versuch, Lücken zu vermeiden, etwa durch das explizite Sperren der gesamten Tabelle beim Vergeben einer neuen ID, serialisiert alle Schreibvorgänge auf diese eine Tabelle. Was zuvor parallel ablaufen konnte, wird dadurch zu einer strikt sequentiellen Warteschlange, mit spürbaren Auswirkungen auf Durchsatz und Latenz, sobald die Schreiblast über ein triviales Niveau hinausgeht.
In der überwältigenden Mehrheit der Anwendungsfälle hat eine Lücke in der ID-Spalte keinerlei fachliche Bedeutung. Der Primärschlüssel dient als technischer, eindeutiger Identifikator, nicht als fortlaufende, lückenlose Zählung von Geschäftsvorfällen. Wird diese Unterscheidung sauber getroffen, verschwindet das vermeintliche Problem, ohne dass irgendeine zusätzliche Logik nötig wäre.
8. Seltene, legitime Ausnahmen: wenn Lückenlosigkeit fachlich gefordert ist
Es gibt Fälle, in denen Lückenlosigkeit keine technische Vorliebe, sondern eine fachliche oder regulatorische Anforderung ist. Das prominenteste Beispiel sind Rechnungsnummern, für die in vielen Rechtsordnungen eine fortlaufende, lückenlose Nummerierung ohne Sprünge gesetzlich vorgeschrieben ist, um Manipulationen an der Buchführung nachweisbar auszuschließen. Ähnliche Anforderungen gelten mitunter für bestimmte Vertragsnummern oder amtliche Belegnummern.
In diesen Fällen ist die Antwort nicht, die primäre Auto-Increment-Sequence zu manipulieren oder ihr Caching zu deaktivieren, sondern die lückenlose Nummer als separates, fachliches Attribut zu behandeln, das unabhängig vom technischen Primärschlüssel verwaltet wird.
9. Wie man lückenlose Nummern sauber getrennt vom Primärschlüssel löst
Die robuste Lösung ist eine dedizierte Zählertabelle mit genau einer Zeile pro fachlichem Kontext, deren Wert innerhalb derselben Transaktion, die auch die Rechnung erzeugt, per SELECT ... FOR UPDATE gelesen und erhöht wird. Weil diese Erhöhung innerhalb der Transaktion erfolgt, wird sie bei einem Rollback ebenfalls zurückgenommen, anders als bei einer echten Sequence, wodurch Lückenlosigkeit garantiert bleibt.
Der bewusste Nachteil dieser Lösung ist eine strikte Serialisierung aller Vorgänge, die eine neue Nummer für denselben Kontext benötigen, weil die Zeile bis zum Commit gesperrt bleibt. Das ist für Rechnungsnummern mit vergleichsweise geringem Volumen pro Zeiteinheit fast immer akzeptabel und ein bewusst in Kauf genommener, klar begrenzter Trade-off, ganz anders als das pauschale Sperren einer stark frequentierten Haupttabelle.
CREATE TABLE rechnungsnummer_zaehler (
kontext TEXT PRIMARY KEY,
naechster_wert BIGINT NOT NULL
);
BEGIN;
SELECT naechster_wert FROM rechnungsnummer_zaehler
WHERE kontext = 'DE-2026' FOR UPDATE;
UPDATE rechnungsnummer_zaehler
SET naechster_wert = naechster_wert + 1
WHERE kontext = 'DE-2026';
-- Rechnung mit dem gelesenen Wert innerhalb derselben Transaktion anlegen
COMMIT;
| Ursache | Lücke entsteht durch | Vermeidbar ohne Zusatzaufwand | Fachlich relevant |
|---|---|---|---|
| Transaktions-Rollback | Sequence bleibt vergeben, Zeile verschwindet | Nein | Meist nicht |
| Sequence-Caching | Verbindungsabbruch mit ungenutztem Block | Nein, nur durch Caching-Verzicht | Meist nicht |
| Parallele Transaktionen | Commit-Reihenfolge weicht von ID-Reihenfolge ab | Nein, strukturell bedingt | Nur bei falscher Nutzung als Zeitangabe |
| Gesetzliche Rechnungsnummer | Fachliche Anforderung an Lückenlosigkeit | Ja, über separate Zählertabelle | Ja, regulatorisch bindend |
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
Sequenz-Lücken verstehen
Kernursache
Sequenzen vergeben Werte bewusst außerhalb des Transaktionskontexts, damit parallele Transaktionen sich nicht gegenseitig blockieren. Ein Rollback nimmt einen vergebenen Wert deshalb nie zurück.
Weitere Ursachen
Sequence-Caching mit Verbindungsabbrüchen und die von der Commit-Reihenfolge unabhängige Wertvergabe erzeugen zusätzliche, ebenfalls normale Lücken.
Warum nicht bekämpfen
Das Erzwingen von Lückenlosigkeit serialisiert Schreibvorgänge und kostet Durchsatz, während eine Lücke in der ID-Spalte in den meisten Fällen keinerlei fachliche Bedeutung hat.
Legitime Ausnahme
Gesetzlich vorgeschriebene, lückenlose Nummern wie Rechnungsnummern gehören in eine separate, transaktional gesperrte Zählertabelle statt in den technischen Primärschlüssel.