Der Performance-Unterschied, konkret gemessen und erklaert
Tausend einzelne INSERT-Statements sind fast immer langsamer als dieselben tausend Zeilen in einem einzigen Batch, weil jeder einzelne Round-Trip zur Datenbank Netzwerk-Latenz, Parsing und Transaktions-Overhead kostet. Batch-Processing buendelt diese Kosten, egal ob ueber Multi-Row-INSERTs, Bulk-Load-Tools oder groessere Transaktionen, und kann die Ladezeit um eine Groessenordnung senken.
Inhaltsverzeichnis
- 1. Warum Batch-Processing beim Massendaten-Import unverzichtbar ist
- 2. Die Round-Trip-Kosten eines einzelnen INSERT
- 3. Multi-Row-INSERT-Syntax richtig einsetzen
- 4. Bulk-Load-Tools: COPY, LOAD DATA und BULK INSERT
- 5. Transaktionsgroesse als Stellschraube
- 6. Indizes und Constraints waehrend des Batch-Ladens
- 7. Fehlerbehandlung bei Batch-Operationen
- 8. Die optimale Batch-Groesse finden
- 9. Methoden im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Batch-Processing beim Massendaten-Import unverzichtbar ist
Wer grosse Datenmengen in eine Datenbank schreibt, stoesst frueher oder spaeter auf denselben Effekt: Ein Skript, das Zeile fuer Zeile mit einzelnen INSERT-Statements arbeitet, braucht fuer hunderttausend Zeilen oft Stunden, waehrend dieselbe Datenmenge per Batch-Processing in wenigen Minuten geladen ist. Der Unterschied liegt nicht an der Datenbank selbst, sondern an der Anzahl der Round-Trips zwischen Anwendung und Datenbankserver, die bei einzelnen INSERTs linear mit der Zeilenzahl waechst.
Batch-Processing beschreibt die Strategie, mehrere Datenaenderungen zu einer einzigen Operation zu buendeln, statt sie einzeln auszufuehren. Das kann ein Multi-Row-INSERT mit hunderten Werten in einem Statement sein, ein spezialisiertes Bulk-Load-Tool wie COPY in PostgreSQL, oder schlicht eine groessere Transaktion, die tausend Einzel-INSERTs zusammenfasst, statt jedes einzeln zu committen. Alle drei Ansaetze reduzieren denselben Flaschenhals: die Anzahl der Round-Trips und der damit verbundene Overhead.
Dieser Artikel zeigt konkret, woher die Kosten einzelner INSERTs kommen, welche Batch-Techniken auf welchen Datenbanken zur Verfuegung stehen, und wie man eine sinnvolle Batch-Groesse fuer den eigenen Anwendungsfall findet. Die Prinzipien gelten unabhaengig davon, ob die Daten aus einer CSV-Datei, einer API oder einer Migration zwischen zwei Systemen stammen.
2. Die Round-Trip-Kosten eines einzelnen INSERT
Jedes einzelne SQL-Statement, das eine Anwendung an die Datenbank sendet, durchlaeuft denselben Ablauf: Netzwerk-Latenz zum Server, Parsing des Statements, Planerstellung oder Cache-Lookup des Plans, Ausfuehrung, und schliesslich die Rueckmeldung an die Anwendung. Selbst bei einer schnellen lokalen Verbindung mit unter einer Millisekunde Latenz summiert sich dieser Overhead bei hunderttausend Einzel-INSERTs auf spuerbare Zeit, ganz unabhaengig von der eigentlichen Schreibzeit der Daten selbst.
Noch staerker wirkt sich dieser Effekt aus, wenn Anwendung und Datenbank nicht auf derselben Maschine laufen, etwa bei einer Cloud-Datenbank mit 5 bis 20 Millisekunden Netzwerk-Latenz pro Round-Trip. Bei hunderttausend Einzel-INSERTs mit je 10 Millisekunden Latenz ergeben sich allein durch das Warten auf Netzwerk-Antworten mehr als 16 Minuten, bevor ueberhaupt eine einzige Zeile tatsaechlich geschrieben wurde. Batch-Processing reduziert diese Wartezeit auf einen Bruchteil, weil dieselbe Datenmenge in deutlich weniger Round-Trips uebertragen wird.
Zusaetzlich zur reinen Netzwerk-Latenz kommt der Overhead fuer Transaktions-Commits hinzu, sofern jedes INSERT einzeln committet wird. Ein Commit erzwingt in den meisten Datenbanken ein Flush des Transaktionslogs auf die Platte, eine relativ teure I/O-Operation. Werden tausend INSERTs einzeln committet, entstehen tausend solcher Flushes, waehrend ein einziger Commit am Ende eines Batches denselben Effekt fuer die gesamte Menge nur einmal ausloest.
-- Slow: 100,000 individual round trips, each with network latency and commit overhead
-- (pseudocode representing what an application loop typically sends)
INSERT INTO products (sku, name, price) VALUES ('SKU-0001', 'Widget A', 9.99);
INSERT INTO products (sku, name, price) VALUES ('SKU-0002', 'Widget B', 14.99);
INSERT INTO products (sku, name, price) VALUES ('SKU-0003', 'Widget C', 7.49);
-- ... repeated 100,000 times, one round trip and often one commit per row
3. Multi-Row-INSERT-Syntax richtig einsetzen
Der einfachste Einstieg in Batch-Processing ist die Multi-Row-INSERT-Syntax, die von MySQL, PostgreSQL und den meisten anderen Datenbanken unterstuetzt wird. Statt eines VALUES-Tupels pro Statement listet man mehrere Tupel in einem einzigen INSERT-Statement auf. Die Datenbank parst das Statement einmal und schreibt alle enthaltenen Zeilen in einer Operation, was Parsing-Overhead und Round-Trips gleichermassen reduziert.
In der Praxis liegt die sinnvolle Grenze pro Statement meist zwischen 100 und 1000 Zeilen, abhaengig von der Zeilenbreite und den Limits der jeweiligen Datenbank. MySQL begrenzt die maximale Statement-Groesse ueber max_allowed_packet, PostgreSQL hat praktisch kein hartes Limit, profitiert aber ab einer bestimmten Groesse nicht mehr proportional von noch groesseren Statements. Ein zu grosses einzelnes Statement kann zudem den Query-Parser messbar belasten und den Vorteil des Batch-Processing teilweise wieder aufheben.
Ein haeufiger Fehler beim Einsatz von Multi-Row-INSERTs ist, die Werte per String-Konkatenation im Anwendungscode zusammenzubauen, was SQL-Injection-Risiken birgt. Die korrekte Vorgehensweise nutzt Prepared Statements mit parametrisierten Platzhaltern fuer jede Zeile im Batch, sodass die Werte weiterhin sicher escaped werden, waehrend trotzdem nur ein einziges Statement an die Datenbank gesendet wird.
-- Fast: a single multi-row INSERT bundles many rows into one round trip
INSERT INTO products (sku, name, price) VALUES
('SKU-0001', 'Widget A', 9.99),
('SKU-0002', 'Widget B', 14.99),
('SKU-0003', 'Widget C', 7.49),
('SKU-0004', 'Widget D', 19.99),
('SKU-0005', 'Widget E', 4.99);
-- One parse, one execution, one round trip for all five rows
-- With parameter placeholders, safely generated by the application layer
INSERT INTO products (sku, name, price) VALUES
(?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?);
4. Bulk-Load-Tools: COPY, LOAD DATA und BULK INSERT
Fuer wirklich grosse Datenmengen, ab mehreren hunderttausend bis Millionen Zeilen, uebertreffen native Bulk-Load-Tools sogar Multi-Row-INSERTs deutlich. PostgreSQL bietet den COPY-Befehl, der Daten direkt aus einer CSV-Datei oder einem Stream liest und dabei einen speziell optimierten, minimal protokollierten Schreibpfad nutzt, der erheblich schneller ist als jede Kombination aus INSERT-Statements. MySQL bietet mit LOAD DATA INFILE eine funktional aehnliche Loesung.
SQL Server stellt mit BULK INSERT beziehungsweise dem Kommandozeilentool bcp eine vergleichbare Funktion bereit, die ebenfalls Daten direkt aus Dateien laedt, unter Umgehung eines Grossteils des regulaeren SQL-Statement-Overheads. Diese Tools sind fuer Batch-Processing im industriellen Massstab konzipiert, etwa fuer taegliche ETL-Laeufe, Datenmigrationen oder den initialen Import eines neuen Systems, bei denen Millionen Zeilen in Minuten statt Stunden geladen werden muessen.
Der Nachteil dieser spezialisierten Tools ist die geringere Flexibilitaet: Komplexe Transformationslogik pro Zeile laesst sich meist nicht direkt waehrend des Ladens ausfuehren, sondern muss vorher, etwa in einer Staging-Tabelle oder im ETL-Prozess, erfolgen. Fuer reine Rohdaten-Uebertragung ohne komplexe Logik pro Zeile sind Bulk-Load-Tools jedoch die schnellste verfuegbare Form von Batch-Processing.
-- PostgreSQL: COPY reads directly from a file with a minimally logged fast path
COPY products (sku, name, price)
FROM '/data/products_import.csv'
WITH (FORMAT csv, HEADER true);
-- MySQL equivalent
LOAD DATA INFILE '/data/products_import.csv'
INTO TABLE products
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- SQL Server equivalent
BULK INSERT products
FROM 'C:\data\products_import.csv'
WITH (FIELDTERMINATOR = ',', ROWTERMINATOR = '\n', FIRSTROW = 2);
5. Transaktionsgroesse als Stellschraube
Neben der Statement-Form selbst spielt die Transaktionsgroesse eine zentrale Rolle im Batch-Processing. Wird jede Zeile in einer eigenen impliziten Transaktion committet, entsteht der bereits beschriebene Commit-Overhead pro Zeile. Werden dagegen zu viele Zeilen in einer einzigen Transaktion zusammengefasst, etwa eine Million Zeilen ohne Zwischen-Commit, wachsen Sperren, Transaktionslog und Rollback-Segmente uebermaessig, was bei einem Fehler mitten im Batch einen sehr teuren Rollback der gesamten Transaktion erzwingt.
Die bewaehrte Praxis liegt dazwischen: Mehrere tausend Zeilen pro Transaktion, mit explizitem Commit nach jedem Batch. Diese Groessenordnung reduziert den Commit-Overhead drastisch gegenueber Einzel-Commits, haelt aber gleichzeitig das Transaktionslog und die Sperrdauer in einem handhabbaren Rahmen. Die genaue Zahl haengt von der Zeilenbreite, der Hardware und der gleichzeitigen Systemlast ab und sollte im Zweifel empirisch ermittelt werden, wie im Abschnitt zur optimalen Batch-Groesse beschrieben.
-- Explicit transaction batching: commit every N rows, not every single row
BEGIN;
INSERT INTO products (sku, name, price) VALUES
('SKU-0001', 'Widget A', 9.99), ('SKU-0002', 'Widget B', 14.99),
('SKU-0003', 'Widget C', 7.49); -- ... up to a few thousand rows
COMMIT;
BEGIN;
INSERT INTO products (sku, name, price) VALUES
('SKU-5001', 'Widget X', 22.49), ('SKU-5002', 'Widget Y', 11.25);
-- ... next batch of a few thousand rows
COMMIT;
6. Indizes und Constraints waehrend des Batch-Ladens
Jeder Index auf einer Zieltabelle muss bei jeder eingefuegten Zeile aktualisiert werden, was auch bei effizientem Batch-Processing spuerbaren Overhead verursacht. Bei sehr grossen initialen Ladevorgaengen, etwa dem Befuellen einer leeren Tabelle mit Millionen Zeilen, ist es oft schneller, nicht zwingend benoetigte Indizes vor dem Laden zu entfernen und danach neu aufzubauen, statt sie waehrend jeder einzelnen Einfuegung zu pflegen.
Aehnlich verhaelt es sich mit Fremdschluessel-Constraints, deren Pruefung bei jeder eingefuegten Zeile zusaetzliche Lookups in der referenzierten Tabelle erfordert. Fuer vertrauenswuerdige Datenquellen, etwa eine bereits validierte Migration, kann es sinnvoll sein, Constraints waehrend des Ladens zu deaktivieren und nach Abschluss des Batch-Processing mit einer einzigen Pruefung ueber den gesamten Datenbestand zu reaktivieren, statt jede Zeile einzeln zu validieren.
Diese Optimierung ist mit Vorsicht einzusetzen, denn sie verschiebt die Fehlerpruefung vom Zeitpunkt des Einfuegens auf den Zeitpunkt des Reaktivierens der Constraints. Bei fehlerhaften Daten scheitert dann der gesamte nachtraegliche Check, statt dass einzelne fehlerhafte Zeilen sofort erkennbar waeren. Fuer unsichere Datenquellen ist deshalb oft eine Staging-Tabelle mit anschliessender Validierung der sicherere Weg.
7. Fehlerbehandlung bei Batch-Operationen
Ein zentraler Nachteil von Batch-Processing gegenueber Einzel-INSERTs ist die Fehlerbehandlung: Scheitert eine einzelne Zeile innerhalb eines Multi-Row-INSERT oder einer grossen Transaktion, etwa wegen eines Constraint-Verstosses, scheitert je nach Datenbank und Konfiguration das gesamte Statement oder die gesamte Transaktion. Bei Einzel-INSERTs waeren nur die tatsaechlich fehlerhaften Zeilen betroffen, waehrend alle anderen erfolgreich eingefuegt worden waeren.
Fuer robustes Batch-Processing mit potenziell fehlerhaften Daten empfiehlt sich ein zweistufiger Ansatz: Zunaechst werden die Daten in eine Staging-Tabelle ohne strenge Constraints geladen, mit maximaler Batch-Geschwindigkeit. Danach validiert eine separate Pruefung die Daten in der Staging-Tabelle und trennt gueltige von ungueltigen Zeilen, bevor nur die gueltigen Zeilen in die eigentliche Zieltabelle uebernommen werden. Dieser Ansatz kombiniert die Geschwindigkeit von Bulk-Operationen mit der Robustheit einzeiliger Validierung.
PostgreSQL bietet zusaetzlich die ON CONFLICT-Klausel, mit der einzelne Konflikte innerhalb eines Batches behandelt werden koennen, ohne das gesamte Statement abzubrechen, etwa durch Ueberschreiben oder Ignorieren des Konflikts. MySQL bietet mit INSERT IGNORE beziehungsweise ON DUPLICATE KEY UPDATE aehnliche Mechanismen, die einzelne problematische Zeilen innerhalb eines Batches abfedern, ohne den Geschwindigkeitsvorteil des Batch-Processing vollstaendig zu verlieren.
-- PostgreSQL: ON CONFLICT handles a duplicate key without aborting the batch
INSERT INTO products (sku, name, price) VALUES
('SKU-0001', 'Widget A', 9.99),
('SKU-0002', 'Widget B', 14.99)
ON CONFLICT (sku) DO UPDATE SET price = EXCLUDED.price;
-- MySQL equivalent: skip duplicates instead of failing the whole batch
INSERT IGNORE INTO products (sku, name, price) VALUES
('SKU-0001', 'Widget A', 9.99),
('SKU-0002', 'Widget B', 14.99);
-- MySQL: update existing rows on duplicate key instead of skipping
INSERT INTO products (sku, name, price) VALUES
('SKU-0001', 'Widget A', 9.99)
ON DUPLICATE KEY UPDATE price = VALUES(price);
8. Die optimale Batch-Groesse finden
Es gibt keine universell richtige Batch-Groesse fuer Batch-Processing, weil die optimale Zahl von Zeilenbreite, Netzwerk-Latenz, Hardware und gleichzeitiger Systemlast abhaengt. Als Ausgangspunkt fuer Multi-Row-INSERTs haben sich in der Praxis 100 bis 500 Zeilen pro Statement bewaehrt, fuer Transaktionsgroessen eher 1000 bis 10000 Zeilen pro Commit. Diese Werte sind aber lediglich ein sinnvoller Startpunkt fuer Messungen, kein feststehendes Optimum.
Die zuverlaessigste Methode ist ein einfacher empirischer Test: Dieselbe Datenmenge mit unterschiedlichen Batch-Groessen laden und die Gesamtzeit messen. Typischerweise zeigt sich ein Kurvenverlauf mit stark abnehmendem Grenznutzen: Der Sprung von Batch-Groesse 1 auf 100 bringt einen enormen Geschwindigkeitsgewinn, der Sprung von 1000 auf 10000 bringt oft nur noch marginale Verbesserung, waehrend Speicherverbrauch und Transaktionsrisiko weiter steigen. Diese Messung, einmal pro Zieldatenbank und Anwendungsfall durchgefuehrt, liefert eine deutlich verlaesslichere Grundlage fuer Batch-Processing als eine pauschale Faustregel.
9. Methoden im direkten Vergleich
Die folgende Tabelle stellt die besprochenen Methoden des Batch-Processing mit ihren typischen Einsatzgebieten gegenueber.
| Methode | Relative Geschwindigkeit | Flexibilitaet | Typischer Einsatz |
|---|---|---|---|
| Einzelne INSERTs | Basislinie (langsamst) | Sehr hoch | Einzelne, interaktive Schreibvorgaenge |
| Multi-Row-INSERT | 5 bis 20x schneller | Hoch | Anwendungsseitige Batch-Schreibvorgaenge |
| Transaktions-Batching | 3 bis 10x schneller | Hoch | Reduzierung von Commit-Overhead |
| COPY / LOAD DATA / BULK INSERT | 20 bis 100x schneller | Gering | ETL, Migration, Massenimport aus Datei |
Die reale Beschleunigung haengt stark vom Einzelfall ab, insbesondere von der Netzwerk-Latenz und der Zeilenbreite. Diese Tabelle dient als grobe Orientierung, welche Methode des Batch-Processing fuer welchen Anwendungsfall infrage kommt, bevor man mit konkreten Messungen die exakten Werte fuer die eigene Umgebung bestimmt.
10. Zusammenfassung
Batch-Processing ist bei jedem Massendaten-Schreibvorgang die richtige Wahl gegenueber einzelnen INSERT-Statements, weil es Netzwerk-Round-Trips, Parsing-Overhead und Commit-Kosten buendelt, statt sie pro Zeile erneut zu bezahlen. Multi-Row-INSERTs sind der einfachste Einstieg und liefern bereits einen erheblichen Geschwindigkeitsgewinn, native Bulk-Load-Tools wie COPY oder LOAD DATA INFILE gehen fuer wirklich grosse Datenmengen noch einen deutlichen Schritt weiter.
Die richtige Transaktions- und Batch-Groesse ist immer ein Kompromiss zwischen Geschwindigkeitsgewinn und Rollback-Risiko und sollte empirisch fuer den jeweiligen Anwendungsfall ermittelt werden, statt eine pauschale Zahl unreflektiert zu uebernehmen. Wer diese Prinzipien einmal verinnerlicht hat, kann Ladezeiten von Stunden auf Minuten reduzieren, ohne die Datenbank selbst aendern zu muessen, allein durch eine bewusste Strategie beim Schreiben von Daten.
Batch-Processing vs. einzelne INSERTs, das Wichtigste auf einen Blick
Round-Trips sind der Kostentreiber
Jedes einzelne Statement kostet Netzwerk-Latenz, Parsing und oft einen Commit-Flush.
Multi-Row-INSERT als Einstieg
Hunderte Zeilen pro Statement reduzieren Round-Trips drastisch, ohne Tool-Wechsel.
Bulk-Tools fuer echte Massendaten
COPY, LOAD DATA INFILE und BULK INSERT sind fuer Millionen Zeilen die schnellste Option.
Batch-Groesse empirisch ermitteln
Keine feste Zahl ist universell richtig, immer mit echten Messungen fuer die eigene Umgebung bestaetigen.