MVCC: Multi-Version Concurrency Control verstehen
AI generated
SELECT
JOIN
SQL · Nebenläufigkeit · Storage Engine
MVCC: Multi-Version Concurrency Control verstehen
wie Leser Schreiber nie blockieren

MVCC erlaubt es einer Datenbank, jeder Transaktion eine konsistente Momentaufnahme der Daten zu zeigen, ohne dass Lesevorgänge Schreibvorgänge blockieren oder umgekehrt. Statt Zeilen zu sperren, verwaltet die Datenbank mehrere Versionen jeder Zeile gleichzeitig, PostgreSQL und InnoDB lösen das auf unterschiedliche Weise, mit direkten Folgen für Vacuum, Purge und die Auswahl des Isolation Levels.

16 Min. Lesezeit MVCC · Snapshot Isolation · Row-Versionierung PostgreSQL · MySQL/InnoDB

1. Was MVCC ist und welches Problem es löst

MVCC, Multi-Version Concurrency Control, ist eine Technik, mit der eine Datenbank mehreren Transaktionen gleichzeitigen Zugriff auf dieselben Daten erlaubt, ohne dass Lesevorgänge auf Schreibvorgänge warten müssen und umgekehrt. Statt eine Zeile beim Lesen zu sperren, wie es klassische lock-basierte Nebenläufigkeitskontrolle tun würde, hält die Datenbank mehrere Versionen jeder Zeile gleichzeitig vor. Eine lesende Transaktion sieht die Version, die zum Zeitpunkt ihres Starts gültig war, unabhängig davon, was andere Transaktionen parallel dazu schreiben.

Das eigentliche Problem, das MVCC löst, ist der klassische Zielkonflikt zwischen Konsistenz und Durchsatz: Ohne Versionierung müsste eine Datenbank entweder Lesesperren einsetzen, die Schreiber blockieren, oder Lesevorgänge riskieren, inkonsistente Zwischenstände zu sehen, während eine andere Transaktion mitten in einer Änderung steckt. MVCC löst diesen Konflikt strukturell auf: Ein Leser bekommt immer eine konsistente, abgeschlossene Sicht auf die Daten, ohne dass ein Schreiber jemals auf ihn warten muss und ohne dass er selbst auf einen Schreiber wartet.

Nahezu alle modernen relationalen Datenbanken implementieren eine Form von MVCC, PostgreSQL, MySQL/InnoDB, Oracle und SQL Server mit seiner Snapshot-Isolation-Option nutzen alle dieselbe Grundidee, unterscheiden sich aber erheblich in den Implementierungsdetails. Diese Unterschiede haben direkte praktische Konsequenzen, von der Notwendigkeit regelmäßiger Wartungsjobs bis hin zu subtilen Unterschieden im Verhalten unter hoher Schreiblast.

2. Row-Versionierung: der PostgreSQL-Ansatz

PostgreSQL implementiert MVCC durch physische Duplizierung der Zeile bei jeder Änderung. Ein UPDATE erzeugt keine In-Place-Modifikation, sondern eine komplett neue physische Zeilenversion mit eigenen Sichtbarkeits-Metadaten, den Systemspalten xmin und xmax. xmin enthält die Transaktions-ID, die diese Version erzeugt hat, xmax die Transaktions-ID, die sie ungültig gemacht hat, falls es eine gibt. Die alte Version bleibt physisch in der Tabelle liegen, als sogenannte tote Zeile, bis ein separater Prozess sie entfernt.

Diese Architektur bedeutet, dass eine PostgreSQL-Tabelle nach vielen UPDATE-Operationen physisch mehr Speicherplatz belegt, als die aktuell sichtbaren Daten erfordern würden, weil alte Zeilenversionen weiter existieren, bis sie aufgeräumt werden. Der Vorteil dieses Ansatzes: Jede Transaktion kann anhand der xmin/xmax-Werte und ihres eigenen Snapshots exakt bestimmen, welche Version einer Zeile für sie sichtbar ist, ohne jemals eine Sperre anzufragen. Das macht Lesevorgänge in PostgreSQL vollständig sperrfrei gegenüber gleichzeitigen Schreibvorgängen.


-- PostgreSQL: system columns reveal MVCC row versions directly
SELECT xmin, xmax, ctid, id, balance FROM accounts WHERE id = 42;

-- Example output after two UPDATEs on the same row:
--  xmin | xmax | ctid   | id | balance
-- ------+------+--------+----+--------
--  1042 |    0 | (0,3)  | 42 | 850.00   -- current, visible version (xmax = 0 means not deleted)

-- The two older physical versions still exist on disk until VACUUM removes them,
-- they are simply no longer visible to any active transaction's snapshot

-- Inspect dead row ratio to judge how urgently a table needs vacuuming
SELECT relname, n_live_tup, n_dead_tup,
       round(n_dead_tup::numeric / GREATEST(n_live_tup, 1), 3) AS dead_ratio
FROM pg_stat_user_tables
ORDER BY dead_ratio DESC
LIMIT 5;
-- A high dead_ratio on a hot table signals autovacuum is falling behind

3. Row-Versionierung: der InnoDB-Ansatz

InnoDB, die Standard-Storage-Engine von MySQL, verfolgt einen anderen Weg zur Umsetzung von MVCC. Statt mehrere physische Kopien der Zeile in der Haupttabelle zu halten, modifiziert InnoDB die Zeile in-place und schreibt die vorherige Version in ein separates Undo-Log-Segment. Jede Zeile trägt intern einen Roll-Pointer, der auf den zugehörigen Undo-Log-Eintrag verweist, und dieser wiederum kann auf eine noch ältere Version verweisen, wodurch eine verkettete Liste historischer Versionen entsteht.

Für eine lesende Transaktion, die eine ältere Version einer Zeile sehen muss, weil ihr Snapshot vor einer zwischenzeitlichen Änderung liegt, rekonstruiert InnoDB die benötigte Version zur Laufzeit, indem es die aktuelle Zeile nimmt und die Undo-Log-Einträge rückwärts anwendet, bis die passende Version erreicht ist. Dieser Ansatz hält die Haupttabelle kompakter als der PostgreSQL-Ansatz, verlagert aber die Kosten in die Undo-Log-Verwaltung: Lange laufende Transaktionen, die alte Snapshots offen halten, verhindern, dass InnoDB die zugehörigen Undo-Log-Einträge freigibt, was die sogenannte History List wachsen lässt.


-- InnoDB: history list length grows with long-running transactions
-- Check current undo log pressure
SHOW ENGINE INNODB STATUS\G
-- Look for "History list length" in the TRANSACTIONS section

-- A long-running read transaction forces InnoDB to keep old undo records around
-- so its snapshot can still be reconstructed on demand
START TRANSACTION WITH CONSISTENT SNAPSHOT;
SELECT balance FROM accounts WHERE id = 42;  -- reads the snapshot as of transaction start
-- ... transaction stays open for a long time while other sessions keep updating accounts ...
COMMIT;  -- only now can InnoDB purge the undo records this transaction depended on

-- Identify the oldest active transaction, the usual root cause of a growing history list
SELECT trx_id, trx_started, trx_isolation_level, trx_rows_locked
FROM information_schema.innodb_trx
ORDER BY trx_started ASC
LIMIT 5;
-- The oldest trx_started value is the one preventing purge from making progress

4. Snapshot Isolation und Read Consistency

Der zentrale Begriff, um MVCC zu verstehen, ist der Snapshot: Beim Start einer Transaktion, oder je nach Isolation Level bei jedem einzelnen Statement, erfasst die Datenbank, welche Transaktionen zu diesem Zeitpunkt bereits committet und welche noch offen sind. Anhand dieser Information kann sie für jede Zeilenversion entscheiden, ob sie für die aktuelle Transaktion sichtbar ist oder nicht, ohne jemals eine Sperre auf die gelesene Zeile zu benötigen.

Unter READ COMMITTED erfasst die Datenbank einen neuen Snapshot bei jedem Statement, wodurch eine Transaktion innerhalb ihres Verlaufs unterschiedliche Datenstände sehen kann, je nachdem, was zwischenzeitlich committet wurde. Unter REPEATABLE READ, in PostgreSQL wie auch in InnoDB, wird der Snapshot einmal beim Beginn der Transaktion erfasst und bleibt für die gesamte Transaktionsdauer stabil, sodass alle Leseoperationen innerhalb dieser Transaktion konsistent denselben Datenstand sehen. Diese Konsistenz ist ein direktes Ergebnis von MVCC und würde ohne Versionierung erhebliche Sperraufwände erfordern.


-- READ COMMITTED: a new snapshot is taken on every statement
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 42;  -- sees committed state at this moment
-- ... another session commits an UPDATE on account 42 here ...
SELECT balance FROM accounts WHERE id = 42;  -- may now return a different value
COMMIT;

-- REPEATABLE READ: one snapshot for the entire transaction
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 42;  -- snapshot taken here
-- ... another session commits an UPDATE on account 42 here ...
SELECT balance FROM accounts WHERE id = 42;  -- still returns the original value
COMMIT;

5. Wie MVCC Reader von Writern entkoppelt

Der praktische Kernvorteil von MVCC zeigt sich am deutlichsten im direkten Vergleich zu lock-basierten Systemen: In einem rein lock-basierten Modell muss ein SELECT, das eine konsistente Sicht garantieren will, entweder eine Lesesperre auf die betroffenen Zeilen setzen oder auf die Freigabe einer bestehenden Schreibsperre warten. Unter MVCC entfällt beides: Der Leser bekommt seine Version der Daten aus dem Snapshot, unabhängig davon, ob gerade ein Schreiber dieselbe Zeile ändert.

Diese Entkopplung ist der Grund, warum lang laufende analytische Abfragen in einem MVCC-System keine Schreiboperationen blockieren, selbst wenn sie Millionen Zeilen über mehrere Sekunden hinweg lesen. Ein Reporting-Job und ein gleichzeitig laufender Order-Prozess konkurrieren unter MVCC nicht um dieselbe Sperre, weil der Reporting-Job seine eigene, in sich konsistente Momentaufnahme sieht, während der Order-Prozess ungehindert neue Versionen schreibt. Der Preis dafür ist, dass alte Versionen aufbewahrt werden müssen, solange irgendeine aktive Transaktion sie potenziell noch braucht, was direkt zu den Themen Vacuum und Purge führt.

6. Vacuum in PostgreSQL: Aufräumen alter Versionen

Weil PostgreSQL alte Zeilenversionen physisch in der Tabelle belässt, braucht es einen dedizierten Prozess, der diese toten Zeilen entfernt, sobald keine aktive Transaktion mehr auf sie angewiesen ist. Dieser Prozess heißt Vacuum, und der Autovacuum-Daemon führt ihn standardmäßig automatisch im Hintergrund aus, sobald der Anteil toter Zeilen einer Tabelle einen konfigurierbaren Schwellenwert überschreitet. Vacuum markiert den belegten Speicherplatz als wiederverwendbar für zukünftige INSERT- und UPDATE-Operationen, gibt ihn aber in der Regel nicht sofort an das Betriebssystem zurück.

Ohne funktionierendes Vacuum wächst eine Tabelle in PostgreSQL kontinuierlich, ein Phänomen, das als Tabellen-Bloat bekannt ist, mit direkten Folgen für Sequential-Scan-Performance und Indexgröße. Besonders kritisch wird es bei Transaction-ID-Wraparound: PostgreSQL nutzt eine begrenzte Anzahl von Transaktions-IDs, und wenn Vacuum über lange Zeit nicht laufen kann, etwa weil eine sehr alte Transaktion offen bleibt und den Freeze-Prozess blockiert, kann die Datenbank im Extremfall in einen Nur-Lese-Notmodus wechseln, um Datenverlust durch ID-Wraparound zu verhindern. Diese direkte Kopplung zwischen MVCC und Vacuum macht deutlich, dass Wartung in PostgreSQL kein optionales Detail, sondern integraler Bestandteil des Konsistenzmodells ist.


-- Manually trigger vacuum on a specific table, with verbose progress output
VACUUM (VERBOSE, ANALYZE) accounts;

-- Check how close a database is to transaction id wraparound
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
-- A rapidly growing xid_age with no corresponding freeze activity is a warning sign

7. Purge in InnoDB: Undo-Logs und History List

InnoDB löst das Aufräumen alter Versionen über einen separaten Purge-Thread, der Undo-Log-Einträge entfernt, sobald keine aktive Transaktion mehr die zugehörige alte Zeilenversion benötigt. Anders als bei PostgreSQL, wo tote Zeilen in der Haupttabelle verbleiben, betrifft das Aufräumen bei InnoDB primär das Undo-Tablespace, kann aber auch dazu führen, dass als gelöscht markierte Zeilen erst verzögert physisch entfernt werden.

Die praktische Konsequenz von MVCC in InnoDB ist die bereits erwähnte History List Length: Je länger eine Transaktion offen bleibt, während andere Sitzungen dieselben Zeilen mehrfach ändern, desto mehr Undo-Log-Einträge muss InnoDB vorhalten, um den Snapshot dieser langen Transaktion weiterhin rekonstruieren zu können. Eine stark wachsende History List führt zu spürbar sinkender Schreibperformance und zunehmendem Speicherverbrauch im Undo-Tablespace, weshalb lang laufende Transaktionen in InnoDB genauso vermieden werden sollten wie in PostgreSQL, wenn auch aus einem leicht anderen technischen Grund.

8. Anomalien trotz MVCC

MVCC löst viele, aber nicht alle Nebenläufigkeitsprobleme automatisch. Write Skew ist eine Anomalie, die selbst unter REPEATABLE READ-Snapshot-Isolation auftreten kann: Zwei Transaktionen lesen jeweils denselben konsistenten Snapshot, treffen darauf basierend unabhängige Entscheidungen und schreiben beide erfolgreich, obwohl das Ergebnis eine Geschäftsregel verletzt, die beide Transaktionen zusammen hätten einhalten müssen. Das klassische Beispiel: Zwei Ärzte prüfen unabhängig, ob noch ein dritter Arzt Dienst hat, bevor sie sich selbst abmelden, beide sehen denselben Snapshot mit zwei verbleibenden Ärzten, beide melden sich ab, am Ende ist niemand mehr im Dienst.

Nur das echte SERIALIZABLE-Isolation-Level, in PostgreSQL implementiert als Serializable Snapshot Isolation, erkennt solche Konflikte zuverlässig und bricht eine der beteiligten Transaktionen mit einem Serialization-Failure-Fehler ab. Das zeigt: MVCC allein garantiert Snapshot-Konsistenz, aber nicht automatisch vollständige Serialisierbarkeit. Wer echte Serialisierbarkeit braucht, muss das explizit über das Isolation Level anfordern und im Anwendungscode auf Serialization-Failures mit Retry reagieren.

Eine weitere Grenze von MVCC zeigt sich bei Lost Updates unter READ COMMITTED: Zwei Transaktionen lesen denselben Wert, berechnen daraus unabhängig einen neuen Wert und schreiben ihn zurück, wobei die zweite Schreiboperation die erste stillschweigend überschreibt, ohne dass die Datenbank einen Konflikt meldet. Klassische Lösung dafür ist explizites Sperren mit SELECT ... FOR UPDATE an der kritischen Stelle, wodurch die zweite Transaktion auf die erste warten muss, statt einen veralteten Snapshot ungeprüft zu überschreiben. Diese gezielte Kombination aus MVCC für Lesevorgänge und expliziten Sperren für kritische Schreiboperationen ist in der Praxis der übliche Mittelweg zwischen voller Serialisierbarkeit und maximalem Durchsatz.


-- Explicit locking closes the lost-update gap that plain MVCC reads leave open
BEGIN;
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;  -- blocks concurrent writers
-- application computes new_balance based on the locked value
UPDATE accounts SET balance = :new_balance WHERE id = 42;
COMMIT;
-- A second concurrent transaction now waits for this lock instead of
-- silently overwriting the first transaction's update

9. MVCC-Implementierungen im Vergleich

Die folgende Tabelle stellt die beiden wichtigsten MVCC-Implementierungen gegenüber und zeigt, welche praktischen Konsequenzen sich aus den architektonischen Unterschieden ergeben.

Aspekt PostgreSQL MySQL/InnoDB Praktische Konsequenz
Alte Versionen In der Haupttabelle als tote Zeilen Im separaten Undo-Log Unterschiedliche Bloat-Muster
Aufräumprozess Vacuum / Autovacuum Purge-Thread Beide brauchen Monitoring
Update-Kosten Neue physische Zeile pro UPDATE In-Place-Update plus Undo-Eintrag Index-Wartung unterscheidet sich
Risiko bei langen Transaktionen Tabellen-Bloat, Wraparound-Risiko Wachsende History List Length Kurze Transaktionen in beiden Pflicht
Standard-Isolation READ COMMITTED REPEATABLE READ Unterschiedliches Anomalie-Risiko
Monitoring-Signal n_dead_tup / dead_ratio pro Tabelle History List Length im Status Beide sollten regelmäßig geprüft werden

Trotz dieser Implementierungsunterschiede verfolgen beide Systeme dasselbe Grundprinzip von MVCC: Leser sehen eine konsistente Version der Daten, ohne Schreiber zu blockieren, und die Kosten dafür verlagern sich in einen separaten, nachgelagerten Aufräumprozess.

Mironsoft

Datenbank-Architektur, Storage-Engine-Tuning und Wartungsstrategien

Tabellen-Bloat oder wachsende History List als Dauerthema?

Wir analysieren euer MVCC-Verhalten, identifizieren lang laufende Transaktionen, die Vacuum oder Purge blockieren, und richten Autovacuum- und Undo-Log-Konfiguration so ein, dass Wartung nicht zum Notfall wird.

MVCC-Diagnose

Analyse von Tabellen-Bloat, History List und langen Transaktionen

Vacuum-Tuning

Autovacuum-Parameter an eure tatsächliche Schreiblast anpassen

Isolation-Level-Beratung

Passendes Isolation Level für eure Konsistenzanforderungen wählen

10. Zusammenfassung

MVCC ist das architektonische Fundament, das modernen relationalen Datenbanken erlaubt, hohe Nebenläufigkeit zu erreichen, ohne Leser und Schreiber gegenseitig zu blockieren. Statt Sperren zu verwenden, hält die Datenbank mehrere Versionen jeder Zeile vor und zeigt jeder Transaktion genau die Version, die zu ihrem Snapshot passt. PostgreSQL erzeugt dafür neue physische Zeilenversionen in der Haupttabelle, InnoDB modifiziert Zeilen in-place und rekonstruiert ältere Versionen über Undo-Logs, beide Ansätze erreichen dasselbe Ziel auf unterschiedlichem Weg.

Der Preis dieser Sperrfreiheit ist die Notwendigkeit, alte Versionen aufzuräumen, sobald sie von keiner aktiven Transaktion mehr benötigt werden, in PostgreSQL durch Vacuum, in InnoDB durch den Purge-Thread. Lang laufende Transaktionen verzögern beide Prozesse und führen zu Tabellen-Bloat beziehungsweise wachsender History List. Wer MVCC versteht, weiß auch, warum kurze Transaktionen, regelmäßiges Monitoring von Wartungsprozessen und ein bewusst gewähltes Isolation Level keine Nice-to-haves sind, sondern direkte Konsequenzen der zugrunde liegenden Versionierungsarchitektur.

Für die tägliche Praxis bedeutet das: Wer eine neue Datenbank plant oder eine bestehende betreibt, sollte Autovacuum-Parameter und Undo-Log-Konfiguration genauso ernst nehmen wie Indexierung oder Query-Tuning. Ein System, das unter MVCC hervorragend skaliert, solange die Wartungsprozesse mit der Schreiblast Schritt halten, kann bei vernachlässigter Konfiguration binnen weniger Wochen in spürbare Performance-Probleme laufen, die sich erst nach eingehender Diagnose als Bloat oder wachsende History List entpuppen.

Beide Monitoring-Signale, der Dead-Tuple-Anteil in PostgreSQL und die History List Length in InnoDB, gehören deshalb auf jedes Datenbank-Dashboard neben klassischen Metriken wie Query-Latenz und Verbindungsauslastung.

MVCC verstehen: Das Wichtigste auf einen Blick

Grundprinzip

Mehrere Versionen jeder Zeile gleichzeitig, jede Transaktion sieht ihren eigenen konsistenten Snapshot ohne Sperren.

PostgreSQL vs. InnoDB

PostgreSQL dupliziert Zeilen physisch, InnoDB modifiziert in-place und rekonstruiert über Undo-Logs.

Wartung

Vacuum in PostgreSQL, Purge-Thread in InnoDB, beide brauchen kurze Transaktionen, um effektiv zu arbeiten.

Grenzen

MVCC verhindert nicht automatisch Write Skew, echte Serialisierbarkeit erfordert SERIALIZABLE plus Retry-Logik.

11. FAQ: MVCC verstehen

1Was bedeutet MVCC?
Multi-Version Concurrency Control. Mehrere Zeilenversionen erlauben konsistente Lesesnapshots ohne Blockierung von Schreibern.
2Warum blockieren Leser keine Schreiber?
Leser nutzen ihre Snapshot-Version ohne Sperre, Schreiber erzeugen parallel eine neue Version.
3Unterschied PostgreSQL vs. InnoDB?
PostgreSQL erzeugt neue physische Zeilen, InnoDB modifiziert in-place mit Undo-Log für alte Versionen.
4Was ist ein Snapshot?
Erfasst committete und offene Transaktionen zu einem Zeitpunkt, bestimmt die Sichtbarkeit von Zeilenversionen.
5Was ist Vacuum?
Entfernt alte Zeilenversionen in PostgreSQL. Ohne Vacuum wächst die Tabelle kontinuierlich (Bloat).
6Was ist Transaction-ID-Wraparound?
Begrenzte Transaktions-IDs, blockiertes Vacuum kann im Extremfall zu einem Nur-Lese-Notmodus führen.
7Was ist die History List Length?
Anzahl vorgehaltener Undo-Log-Einträge in InnoDB. Wächst mit langen Transaktionen, senkt Performance.
8Verhindert MVCC alle Anomalien?
Nein, Write Skew kann trotzdem auftreten. Nur SERIALIZABLE erkennt solche Konflikte zuverlässig.
9Warum kurze Transaktionen unter MVCC?
Lange Transaktionen blockieren Vacuum und Purge, was zu Bloat und wachsender History List führt.
10Standard-Isolation Level bei MVCC?
PostgreSQL: READ COMMITTED. MySQL/InnoDB: REPEATABLE READ. Beide bauen auf MVCC auf.