Zum Inhalt springen

Datenbanken

Von Oracle bis MongoDB. Betrieb, Migration und Performance-Tuning.

MySQL

InnoDB, Galera

PostgreSQL

MVCC, Partitioning

MongoDB

Documents, Sharding

Oracle

RAC, Dataguard

Datenbanken sind unsere Welt

Nicht die Datenbank ist langsam — das Query ist es. Oder der Index fehlt. Oder der Index ist da, aber macht es schlimmer. Wer Datenbanken wirklich versteht, denkt nicht in Tools, sondern in Speicherstrukturen, Ausführungsplänen und Concurrency-Modellen.

Jedes System tickt anders

MySQL, PostgreSQL, Oracle und MongoDB lösen die gleichen Grundprobleme — auf fundamental unterschiedliche Weise. Wer das nicht versteht, optimiert an der falschen Stelle. Wer es versteht, findet die Ursache statt die Symptome zu behandeln.

MVCC — und warum SELECTs Writes erzeugen

Multi-Version Concurrency Control (MVCC) ist das Herzstück moderner Datenbanken — aber jede implementiert es anders:

MySQL / InnoDB

Undo-Logs im gemeinsamen Tablespace. Bei einem SELECT wird der konsistente Snapshot aus dem Undo-Log rekonstruiert. Lange Transaktionen halten Undo-Segmente offen → Tablespace wächst → Purge-Thread kommt nicht nach → Performance-Degradation ohne eine einzige Schreiboperation.

PostgreSQL

Alte Row-Versionen bleiben direkt im Heap liegen (xmin/xmax). Tote Tupel warten auf VACUUM. Ein reiner SELECT erzeugt Writes — wenn die Visibility Map aktualisiert wird (Hint Bits). Das erklärt I/O bei Read-Only-Workloads.

Oracle

Separater Undo-Tablespace mit konfigurierbarer Retention. Konzeptionell ähnlich wie InnoDB, aber mit Flashback-Fähigkeit. Bei Engpässen: der berüchtigte ORA-01555: snapshot too old.

MongoDB

WiredTiger nutzt Document-Level Concurrency mit optimistischem Locking. Kein Undo-Log, keine toten Tupel — Copy-on-Write auf Dokumentebene. Skaliert horizontal, macht aber Multi-Document-Transaktionen komplexer.

Dirty Pages — warum Lesen schreibt

InnoDB Buffer Pool Stats

SELECT DATABASE_PAGES,
  MODIFIED_DB_PAGES as dirty,
  ROUND(MODIFIED_DB_PAGES /
    DATABASE_PAGES * 100, 2) as pct
FROM information_schema
  .INNODB_BUFFER_POOL_STATS;

dirty: 12.847 | pct: 8.41%
→ Checkpoint-Druck steigt
→ I/O-Writes nehmen zu

Datenbanken arbeiten im Arbeitsspeicher. Der Buffer Pool (InnoDB) oder Shared Buffers (PostgreSQL) halten Datenseiten im RAM. Wird eine Seite gelesen und dabei ein Hint Bit gesetzt, wird sie als dirty markiert.

Dirty Pages müssen irgendwann auf die Platte (Checkpoint/Flushing). Wenn der Buffer Pool zu klein ist oder die I/O-Kapazität nicht reicht, blockieren auch SELECT-Queries — weil keine freien Seiten mehr verfügbar sind. Einer der häufigsten Performance-Killer.

Indexe — warum mehr nicht immer besser ist

SHOW INDEX FROM orders

idx_03 (status)
  → Kardinalität: 4. Nutzlos.

idx_04 (customer_id, status)
  → Redundant zu idx_01.

idx_07 (shipping_method)
  → 3 Werte. Full Table Scan schneller.

Cleanup: 3 statt 8 Indexe
INSERT-Throughput: +60%

Ein fehlender Index ist offensichtlich. Ein falscher Index ist gefährlich. Ein überflüssiger Index kostet bei jedem INSERT, UPDATE und DELETE.

Jeder Index ist ein B-Tree der bei Schreiboperationen gepflegt werden muss — Split-Operationen, Rebalancing, Write-Amplification. Bei hohem Schreibdurchsatz wird jeder überflüssige Index zum Bremsklotz.

Execution Plans lesen

Gleiche Query, gleiche Daten — Faktor 100+ Unterschied durch den richtigen Index. Ein typisches Beispiel aus dem E-Commerce:

EXPLAIN ANALYZE

SELECT o.id, c.name, o.total
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at > now() – interval ’30 days‘
  AND o.status = ’shipped‘
ORDER BY o.total DESC
LIMIT 20;

Eine einfache Abfrage: Die 20 umsatzstärksten Bestellungen der letzten 30 Tage. Bei 1.2 Millionen Zeilen in der orders-Tabelle entscheidet der Execution Plan über Millisekunden oder Sekunden.

Vorher: Seq Scan

Limit (cost=45832 rows=20)
  → Sort (key: total DESC)
    → Hash Join
      → Seq Scan on orders
        Filter: status = ’shipped‘
        Rows Removed: 1.247.000

1.2M Zeilen gelesen, 28K behalten.

Nachher: Index Only Scan

CREATE INDEX idx_status_created
  ON orders (status, created_at)
  INCLUDE (total, customer_id);

Limit (cost=0.43 rows=20)
  → Index Only Scan
    on idx_status_created
    Heap Fetches: 0

Kein Heap-Zugriff. Faktor 100+.

Row Locks — wenn ein SELECT zur Waffe wird

Realer Fall: Mehrere Systeme holen eingehende Aufträge aus einer gemeinsamen Queue-Tabelle ab — quasi ein Message Bus auf Datenbankebene. Jeder Agent führt ein SELECT ... FOR UPDATE NOWAIT aus, um sich den nächsten offenen Auftrag zu sichern. Wer am schnellsten ist, bekommt den Datensatz. Die anderen pollen weiter.

Das funktioniert — solange alles rund läuft. Aber sobald ein Agent den Lock bekommt und dann hängt (langsame Verarbeitung, externer API-Call, Exception ohne Rollback), blockiert er die Zeile. Die anderen Agenten bekommen NOWAIT-Fehler, springen zum nächsten Datensatz. Aufträge bleiben stecken. Keiner merkt es — bis der Kunde anruft.

Queue-Tabelle mit FOR UPDATE

Agent 1:
BEGIN;
SELECT * FROM job_queue
WHERE status = ‚pending‘
ORDER BY priority DESC
LIMIT 1
FOR UPDATE NOWAIT;
— Lock erhalten. Verarbeitet…
— API-Call hängt. Kein COMMIT.

Agent 2, 3, 4:
Gleiche Query →
→ Zeile gesperrt, NOWAIT → skip
→ Nächster Datensatz. Weiter pollen.

Ergebnis:
Auftrag #4271 bleibt hängen.
Status: pending. Lock: aktiv.
Niemand verarbeitet ihn.

Das Tückische: Die Architektur ist nicht falsch. SELECT FOR UPDATE NOWAIT als Queue-Mechanismus ist ein gängiges Pattern. Aber ohne Absicherung — Statement-Timeout, Lock-Timeout, Transaction-Watchdog — wird aus einem robusten Pattern ein stilles Risiko.

Ohne Absicherung

SELECT * FROM job_queue
WHERE status = ‚pending‘
FOR UPDATE NOWAIT;

— Kein Statement-Timeout
— Kein Lock-Timeout
— Kein Watchdog

→ Agent hängt = Auftrag hängt
→ Niemand merkt es

Mit Absicherung

SET lock_timeout = ‚5s‘;
SET statement_timeout = ’30s‘;

SELECT * FROM job_queue
WHERE status = ‚pending‘
FOR UPDATE SKIP LOCKED;

— SKIP LOCKED: gesperrte
— Zeilen überspringen statt
— Fehler werfen

→ Agent blockiert nie
→ Queue läuft immer weiter

Zwischen den Zeilen lesen

Die meisten Datenbank-Probleme sind keine Datenbank-Probleme. Sie sind Architektur-Probleme, ORM-Probleme oder fehlende Timeouts. Wer nur auf die DB schaut, behandelt Symptome. Wer den gesamten Stack versteht — vom Connection Pool über das Query bis zum Execution Plan — findet die Ursache.

Datenbank-Problem das keiner findet? Lassen Sie uns draufschauen