Portabilitaet in der Praxis, nicht in der Theorie
SQL ist ein Standard, aber jedes Datenbanksystem interpretiert diesen Standard mit eigenen Erweiterungen und Abweichungen. Quoting-Regeln fuer Bezeichner, die Syntax fuer Paging, die Schreibweise fuer automatisch generierte IDs und selbst grundlegende Datentypen unterscheiden sich zwischen MySQL, PostgreSQL und SQL Server so deutlich, dass echtes datenbank-agnostisches SQL bewusstes Design statt Zufall erfordert.
Inhaltsverzeichnis
- 1. Was datenbank-agnostisches SQL bedeutet und wo Grenzen liegen
- 2. Quoting: Identifiers, Strings und ihre Unterschiede
- 3. Paging: LIMIT/OFFSET vs. TOP vs. FETCH FIRST
- 4. Auto-Increment: AUTO_INCREMENT vs. SERIAL vs. IDENTITY
- 5. Datentypen: VARCHAR, BOOLEAN und Datum/Zeit
- 6. String-Funktionen und Concatenation
- 7. Transaktionsisolation und Locking-Unterschiede
- 8. Abstraktionsschichten: ORM/Query-Builder vs. natives SQL
- 9. Wann sich Portabilitaet nicht lohnt
- 10. Zusammenfassung
- 11. FAQ
1. Was datenbank-agnostisches SQL bedeutet und wo Grenzen liegen
Datenbank-agnostisches SQL ist SQL, das sich auf verschiedenen Datenbanksystemen konsistent verhaelt, ohne fuer jedes System eine eigene Version pflegen zu muessen. Der SQL-Standard, formal als ANSI SQL beziehungsweise ISO/IEC 9075 definiert, legt die Grundlagen fest, aber kein einziges verbreitetes Datenbanksystem implementiert den Standard vollstaendig und ohne eigene Erweiterungen. MySQL, PostgreSQL und SQL Server folgen alle demselben grundlegenden Modell aus Tabellen, JOINs und Transaktionen, weichen aber in Details voneinander ab, die bei naiver Uebernahme von SQL zwischen Systemen zu Fehlern fuehren, die oft erst spaet im Deployment-Prozess auffallen.
Wichtig ist eine realistische Erwartungshaltung: Datenbank-agnostisches SQL zu schreiben bedeutet nicht, dass jede Zeile Code auf jedem System identisch funktioniert, sondern dass die Unterschiede bekannt, dokumentiert und an klar abgegrenzten Stellen im Code isoliert sind. Vollstaendige Portabilitaet fuer komplexe Anwendungen ist in der Praxis selten erreichbar und meist auch nicht das eigentliche Ziel, weil sie den Verzicht auf leistungsfaehige, systemspezifische Funktionen erzwingen wuerde.
Dieser Artikel behandelt die haeufigsten Fallstricke, die zwischen MySQL, PostgreSQL und SQL Server tatsaechlich in der Praxis auftreten: Quoting-Regeln, Paging-Syntax, Auto-Increment-Mechanismen, Datentypen und String-Operationen. Am Ende steht eine ehrliche Einordnung, wann sich der Aufwand fuer datenbank-agnostisches SQL lohnt und wann er unnoetige Komplexitaet ohne realen Nutzen erzeugt.
2. Quoting: Identifiers, Strings und ihre Unterschiede
Die grundlegendste und zugleich am haeufigsten uebersehene Portabilitaetsfalle betrifft das Quoting von Bezeichnern. Der SQL-Standard schreibt doppelte Anfuehrungszeichen fuer Bezeichner wie Tabellen- oder Spaltennamen vor, etwa "order". PostgreSQL und SQL Server folgen dieser Standardregel weitgehend, MySQL hingegen nutzt standardmaessig Backticks, `order`, und interpretiert doppelte Anfuehrungszeichen im ANSI_QUOTES-Modus abweichend vom Standard-Verhalten. SQL Server bietet zusaetzlich eckige Klammern, [order], als proprietaere Alternative zu doppelten Anfuehrungszeichen.
Fuer String-Literale gilt in allen drei Systemen einheitlich das einfache Anfuehrungszeichen, 'text', was eine der wenigen Konstanten zwischen den Dialekten darstellt. Problematisch wird es bei der Behandlung reservierter Woerter als Bezeichner: Ein Spaltenname wie order oder user kollidiert mit SQL-Schluesselwoertern und muss zwingend gequotet werden, wobei die Quoting-Syntax je nach Zielsystem unterschiedlich ist. Datenbank-agnostisches SQL vermeidet dieses Problem am elegantesten, indem reservierte Woerter von vornherein nicht als Bezeichner verwendet werden, statt die Quoting-Unterschiede systemuebergreifend zu handhaben.
-- Identifier quoting differs significantly between systems
-- ANSI SQL standard / PostgreSQL / SQL Server (default mode)
SELECT "order_id", "customer_name" FROM "orders" WHERE "order_id" = 1;
-- MySQL default: backticks instead of double quotes
SELECT `order_id`, `customer_name` FROM `orders` WHERE `order_id` = 1;
-- SQL Server proprietary alternative: square brackets
SELECT [order_id], [customer_name] FROM [orders] WHERE [order_id] = 1;
-- String literals: single quotes are consistent across all three systems
SELECT * FROM orders WHERE status = 'shipped';
-- Best practice: avoid reserved words as identifiers entirely
-- instead of: SELECT "order" FROM "user";
-- use: SELECT order_status FROM app_user;
3. Paging: LIMIT/OFFSET vs. TOP vs. FETCH FIRST
Kaum eine Portabilitaetsfalle wird so haeufig unterschaetzt wie Paging. MySQL und PostgreSQL nutzen die vertraute Syntax LIMIT n OFFSET m, um eine begrenzte Anzahl Zeilen ab einer bestimmten Position zurueckzugeben. SQL Server kannte diese Syntax lange Zeit ueberhaupt nicht und verwendete stattdessen TOP n, das aber kein Offset unterstuetzt und daher fuer echtes Paging mit ORDER BY und OFFSET ... FETCH NEXT kombiniert werden muss, einer Syntax, die erst seit SQL Server 2012 verfuegbar ist.
Der ANSI-SQL-Standard definiert eigentlich FETCH FIRST n ROWS ONLY in Kombination mit OFFSET n ROWS als portable Loesung, und moderne Versionen aller drei Systeme unterstuetzen diese Syntax mittlerweile. In der Praxis trifft man dennoch haeufig auf aeltere Codebasen, die die jeweils systemspezifische, aeltere Syntax verwenden, weil sie historisch zuerst verfuegbar war. Wer heute neuen Code schreibt, sollte konsequent die ANSI-Standard-Syntax bevorzugen, weil sie auf allen drei Systemen ohne Anpassung funktioniert und den Umstieg auf ein anderes System spuerbar erleichtert.
-- Paging syntax differs significantly by system and by SQL version
-- MySQL / PostgreSQL classic syntax
SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET 40;
-- SQL Server classic syntax (TOP has no offset, requires a workaround)
SELECT TOP 20 id, name FROM products ORDER BY name;
-- True paging on SQL Server (2012+) requires OFFSET ... FETCH
SELECT id, name FROM products
ORDER BY name
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- ANSI SQL standard syntax: works on modern MySQL, PostgreSQL and SQL Server
SELECT id, name FROM products
ORDER BY name
OFFSET 40 ROWS FETCH FIRST 20 ROWS ONLY;
4. Auto-Increment: AUTO_INCREMENT vs. SERIAL vs. IDENTITY
Automatisch generierte Primaerschluessel sind eines der Kernfeatures relationaler Datenbanken, aber ihre Syntax ist zwischen Systemen komplett unterschiedlich. MySQL nutzt das Schluesselwort AUTO_INCREMENT direkt an der Spaltendefinition. PostgreSQL kennt historisch den Pseudo-Datentyp SERIAL, der intern eine INTEGER-Spalte mit einer angehaengten Sequenz erzeugt, waehrend moderne PostgreSQL-Versionen den ANSI-konformeren Ansatz GENERATED ALWAYS AS IDENTITY bevorzugen. SQL Server nutzt wiederum das Schluesselwort IDENTITY(1,1) mit Start- und Inkrement-Wert als Parameter.
Diese drei Mechanismen unterscheiden sich nicht nur syntaktisch, sondern auch in ihrem Verhalten bei Transaktionsabbruch: Bei manchen Systemen werden bereits reservierte, aber nicht committete Werte einer Sequenz nicht wieder freigegeben, was zu Luecken in der ID-Folge fuehrt. Fuer datenbank-agnostisches SQL bedeutet das: Wer sich auf eine luecklose, fortlaufende ID-Sequenz verlaesst, baut auf eine Garantie, die kein relevantes Datenbanksystem tatsaechlich gibt, unabhaengig vom gewaehlten Auto-Increment-Mechanismus.
-- Auto-increment / identity syntax is entirely different per system
-- MySQL
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
total DECIMAL(10,2)
);
-- PostgreSQL: legacy SERIAL pseudo-type
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
total NUMERIC(10,2)
);
-- PostgreSQL: modern ANSI-style identity column (preferred since PG 10)
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
total NUMERIC(10,2)
);
-- SQL Server
CREATE TABLE orders (
id INT IDENTITY(1,1) PRIMARY KEY,
total DECIMAL(10,2)
);
5. Datentypen: VARCHAR, BOOLEAN und Datum/Zeit
Auch scheinbar einfache Datentypen bergen Portabilitaetsfallen. Ein BOOLEAN ist in PostgreSQL ein echter, eigenstaendiger Datentyp mit den Werten TRUE, FALSE und NULL. MySQL kennt keinen echten BOOLEAN-Typ, sondern behandelt BOOLEAN intern als Synonym fuer TINYINT(1), wobei 0 als falsch und jeder andere Wert als wahr gilt. SQL Server verwendet stattdessen BIT mit den Werten 0 und 1. Vergleiche, die auf einem System exakt TRUE gegen 1 pruefen, koennen auf einem anderen System zu subtil abweichendem Verhalten fuehren.
Datum- und Zeitangaben unterscheiden sich ebenfalls in Praezision und Zeitzonenbehandlung. PostgreSQL bietet mit TIMESTAMP WITH TIME ZONE eine explizite, zeitzonenbewusste Speicherung, MySQL speichert Zeitzoneninformationen bei TIMESTAMP implizit relativ zur Server-Zeitzone, waehrend DATETIME in MySQL komplett zeitzonenfrei ist. SQL Server bietet mit DATETIME2 eine praezisere Alternative zum aelteren DATETIME, aber ebenfalls ohne native Zeitzonenunterstuetzung wie DATETIMEOFFSET sie bietet. Wer zeitzonenkritische Anwendungen datenbank-agnostisch entwickelt, sollte Zeitstempel grundsaetzlich als UTC speichern und die Zeitzonenkonvertierung explizit in der Anwendungsschicht vornehmen, statt sich auf implizites Datenbankverhalten zu verlassen.
-- Data type differences: boolean and timestamp handling per system
-- MySQL: no real BOOLEAN type, TINYINT(1) under the hood
CREATE TABLE flags_mysql (
is_active TINYINT(1) NOT NULL DEFAULT 0
);
-- PostgreSQL: native BOOLEAN type
CREATE TABLE flags_postgres (
is_active BOOLEAN NOT NULL DEFAULT FALSE
);
-- SQL Server: BIT type instead of BOOLEAN
CREATE TABLE flags_sqlserver (
is_active BIT NOT NULL DEFAULT 0
);
-- Timestamps: always store as UTC, convert timezone in the application layer
-- PostgreSQL
CREATE TABLE events_postgres (
occurred_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- MySQL (implicit server timezone, store UTC explicitly)
CREATE TABLE events_mysql (
occurred_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- SQL Server (no native timezone awareness in DATETIME2)
CREATE TABLE events_sqlserver (
occurred_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
6. String-Funktionen und Concatenation
String-Verkettung ist ein weiteres Feld mit erheblicher Divergenz. ANSI SQL definiert den ||-Operator fuer Verkettung, den PostgreSQL vollstaendig unterstuetzt. MySQL interpretiert || standardmaessig als logisches ODER statt als Verkettung und erfordert stattdessen die Funktion CONCAT(). SQL Server nutzt wiederum den +-Operator fuer String-Verkettung, was bei gemischten Datentypen zu impliziten und teils ueberraschenden Typkonvertierungen fuehren kann.
Auch grundlegende String-Funktionen wie Teilstring-Extraktion oder Laengenbestimmung unterscheiden sich in Benennung und Parameter-Reihenfolge zwischen den Systemen, auch wenn ihre grundsaetzliche Funktionalitaet aehnlich ist. Bei datenbank-agnostischem SQL ist es meist praktikabler, die String-Verkettung ueber die Datenzugriffsschicht der Anwendung zu abstrahieren, etwa ueber einen Query-Builder mit eingebauter Dialekt-Uebersetzung, statt fuer jeden Zieldialekt eine eigene SQL-Variante zu pflegen.
-- String concatenation syntax differs significantly between systems
-- ANSI SQL standard / PostgreSQL: double pipe operator
SELECT first_name || ' ' || last_name AS full_name FROM customers;
-- MySQL: || means logical OR by default, use CONCAT() instead
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;
-- SQL Server: plus operator for concatenation
SELECT first_name + ' ' + last_name AS full_name FROM customers;
-- Portable alternative: use CONCAT(), supported on all three systems
-- (PostgreSQL and SQL Server both implement CONCAT() as well)
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;
7. Transaktionsisolation und Locking-Unterschiede
Auch wenn alle drei Systeme die ANSI-Isolationslevel READ COMMITTED, REPEATABLE READ und SERIALIZABLE kennen, unterscheidet sich ihr tatsaechliches Verhalten unter Last erheblich. PostgreSQL implementiert READ COMMITTED und REPEATABLE READ ueber MVCC ohne Lesesperren, waehrend SQL Server im Standardmodus fuer READ COMMITTED tatsaechlich kurzlebige Sperren beim Lesen setzt, sofern nicht explizit Snapshot Isolation aktiviert wird. MySQL mit InnoDB verwendet standardmaessig REPEATABLE READ statt READ COMMITTED wie die anderen beiden Systeme, was bei uebernommenem Code aus einem anderen System zu unerwartetem Verhalten bei nebenlaeufigen Transaktionen fuehren kann.
Diese Unterschiede lassen sich selten vollstaendig portabel abstrahieren, weil sie tief in der jeweiligen Storage-Engine verankert sind. Der pragmatische Ansatz fuer datenbank-agnostisches SQL ist, das Isolationslevel explizit in jeder Transaktion zu setzen, statt sich auf systemspezifische Defaults zu verlassen, und Race-Conditions durch explizite Locking-Strategien wie SELECT ... FOR UPDATE abzusichern, das auf allen drei Systemen in aehnlicher Form verfuegbar ist.
8. Abstraktionsschichten: ORM/Query-Builder vs. natives SQL
Eine der pragmatischsten Loesungen fuer viele der genannten Portabilitaetsprobleme ist der Einsatz einer Datenzugriffsschicht, die Dialektunterschiede zentral kapselt. Ein Query-Builder oder ORM uebersetzt eine abstrakte Beschreibung der gewuenschten Abfrage, etwa Paging mit Limit und Offset oder Auto-Increment-Spaltendefinitionen, automatisch in die korrekte Syntax des Zielsystems. Das reduziert den manuellen Aufwand fuer datenbank-agnostisches SQL drastisch, weil Entwickler die Unterschiede nicht mehr fuer jede einzelne Abfrage im Kopf behalten muessen.
Der Nachteil dieses Ansatzes: Abstraktionsschichten decken meist nur den kleinsten gemeinsamen Nenner der unterstuetzten Systeme ab und stehen komplexen, systemspezifischen Optimierungen wie speziellen Indextypen, Fensterfunktionen mit Erweiterungen oder Volltextsuche haeufig im Weg. In der Praxis kombinieren viele Projekte beide Ansaetze bewusst: eine Abstraktionsschicht fuer die alltaeglichen CRUD-Operationen, gezieltes natives SQL fuer performancekritische oder komplexe Abfragen, klar gekennzeichnet und isoliert, damit bei einem Systemwechsel nur diese wenigen Stellen angepasst werden muessen.
| Merkmal | MySQL | PostgreSQL | SQL Server |
|---|---|---|---|
| Identifier-Quoting | `backtick` |
"doppelte Quotes" |
[eckige Klammern] |
| Paging | LIMIT n OFFSET m |
LIMIT n OFFSET m |
OFFSET ... FETCH NEXT |
| Auto-Increment | AUTO_INCREMENT |
GENERATED AS IDENTITY |
IDENTITY(1,1) |
| String-Concat | CONCAT() |
|| |
+ |
| Boolean-Typ | TINYINT(1) | Nativer BOOLEAN | BIT |
| Standard-Isolation | REPEATABLE READ | READ COMMITTED | READ COMMITTED |
9. Wann sich Portabilitaet nicht lohnt
Nicht jedes Projekt profitiert davon, konsequent datenbank-agnostisches SQL zu schreiben. Wenn ein System von vornherein und auf absehbare Zeit an ein einziges Datenbanksystem gebunden ist, etwa weil ein Cloud-Anbieter oder eine Unternehmensrichtlinie das Zielsystem fest vorgibt, erzeugt der zusaetzliche Aufwand fuer Portabilitaet Kosten ohne realistischen Gegenwert. Systemspezifische Features, etwa PostgreSQLs JSONB-Indizierung, MySQLs Volltextsuche oder SQL Servers Columnstore-Indizes, bieten haeufig erhebliche Performance- oder Funktionsvorteile, die bei strikter Portabilitaet ungenutzt blieben.
Ein realistischer Ansatz gewichtet den Portabilitaetsaufwand nach der tatsaechlichen Wahrscheinlichkeit eines Systemwechsels. Fuer die meisten produktiven Anwendungen lohnt es sich, offensichtliche Portabilitaetsfallen wie inkonsistentes Quoting oder nicht standardisiertes Paging zu vermeiden, weil das kaum Mehraufwand bedeutet und den Code ohnehin lesbarer macht. Tiefgreifende Abstraktion gegen jede denkbare Systemabweichung lohnt sich dagegen nur, wenn ein Systemwechsel als reale Moeglichkeit im Projektlebenszyklus eingeplant ist, nicht als theoretisches Gedankenexperiment.
Mironsoft
Datenbankmigrationen, Cross-Database-Entwicklung und Architektur-Beratung
Planen Sie einen Wechsel des Datenbanksystems?
Wir pruefen bestehenden SQL-Code auf Portabilitaetsfallen, entwerfen pragmatische Abstraktionsstrategien und begleiten Migrationen zwischen MySQL, PostgreSQL und SQL Server mit klar isolierten, systemspezifischen Codepfaden.
Portabilitaets-Audit
Bestehenden SQL-Code auf systemspezifische Abhaengigkeiten pruefen
Migrations-Planung
Strategie fuer den Wechsel zwischen Datenbanksystemen entwickeln
Abstraktions-Design
Pragmatische Datenzugriffsschicht zwischen Portabilitaet und Performance
10. Zusammenfassung
Datenbank-agnostisches SQL zu schreiben bedeutet nicht, jede systemspezifische Funktion zu meiden, sondern die konkreten, dokumentierten Unterschiede bewusst zu kennen und gezielt zu handhaben. Quoting-Regeln, Paging-Syntax, Auto-Increment-Mechanismen, Datentypen wie BOOLEAN und Datum/Zeit sowie String-Verkettung sind die haeufigsten Stellen, an denen Code, der auf einem System funktioniert, auf einem anderen scheitert oder sich subtil anders verhaelt. Wer diese Fallen kennt, vermeidet einen Grossteil der Portabilitaetsprobleme, ohne auf leistungsfaehige systemspezifische Features verzichten zu muessen.
Die realistischste Strategie fuer datenbank-agnostisches SQL ist selten vollstaendige Abstraktion, sondern eine bewusste Trennung: Standardkonforme ANSI-SQL-Syntax fuer alltaegliche Operationen, wo sie ohne Mehraufwand verfuegbar ist, gezielte und klar isolierte systemspezifische Optimierung dort, wo Performance oder Funktionsumfang es rechtfertigen. Diese Balance macht ein System wartbar, ohne bei einem spaeteren Wechsel des Datenbanksystems vor einer kompletten Neuimplementierung zu stehen.
Datenbank-agnostisches SQL, das Wichtigste auf einen Blick
Quoting und Paging
Reservierte Woerter als Bezeichner vermeiden, ANSI-Standard OFFSET/FETCH FIRST statt systemspezifischer Syntax nutzen.
Auto-Increment und Datentypen
Keine luecklose ID-Sequenz voraussetzen, Boolean- und Zeitstempel-Verhalten je System explizit pruefen.
Abstraktionsschicht gezielt einsetzen
Query-Builder fuer alltaegliche CRUD-Operationen, natives SQL isoliert fuer performancekritische Abfragen.
Portabilitaet realistisch bewerten
Aufwand nur investieren, wenn ein Systemwechsel real absehbar ist, sonst systemspezifische Vorteile nutzen.