von Datentypen bis Upsert Syntax
Wer Anwendungen zwischen PostgreSQL und MySQL migriert oder beide Systeme parallel betreibt, stoesst schnell auf Syntax-Unterschiede, die auf den ersten Blick harmlos wirken, aber ganze Migrationen verzoegern koennen. Dieser Artikel vergleicht Datentypen, Auto-Increment, JSON-Unterstuetzung, Pagination und Upsert Syntax zwischen PostgreSQL und MySQL anhand lauffaehiger Beispiele.
Inhaltsverzeichnis
- 1. Warum PostgreSQL und MySQL trotz gemeinsamer Basis so unterschiedlich sind
- 2. Datentypen: wo PostgreSQL und MySQL grundlegend abweichen
- 3. String-Funktionen und Concat-Syntax im Vergleich
- 4. LIMIT, OFFSET und Pagination-Syntax
- 5. Auto-Increment: SERIAL vs. AUTO_INCREMENT
- 6. Case-Sensitivity bei Identifiern und Strings
- 7. JSON-Unterstuetzung: JSONB vs. JSON-Spalte
- 8. Upsert-Syntax: ON CONFLICT vs. ON DUPLICATE KEY UPDATE
- 9. PostgreSQL vs. MySQL im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum PostgreSQL und MySQL trotz gemeinsamer Basis so unterschiedlich sind
PostgreSQL und MySQL sprechen beide SQL, doch wer glaubt, eine Anwendung liesse sich per Suchen und Ersetzen zwischen den beiden Systemen migrieren, wird schnell eines Besseren belehrt. Der Grund liegt in der Entstehungsgeschichte: PostgreSQL wurde von Anfang an als objektrelationales System mit strikter Typisierung und enger Anlehnung an den SQL Standard konzipiert. MySQL entstand aus dem Bedarf nach einem schnellen, einfach zu betreibenden Datenbankserver fuer Webanwendungen und hat viele pragmatische Abkuerzungen eingebaut, die spaeter zu eigenen Konventionen wurden.
Diese unterschiedliche Philosophie zeigt sich in fast jedem Bereich: bei der Behandlung von Datentypen, bei der Gross- und Kleinschreibung von Tabellennamen, bei der Fehlerbehandlung bei ungueltigen Werten und bei der Unterstuetzung fortgeschrittener Features wie Window Functions oder rekursiven Common Table Expressions. Wer PostgreSQL und MySQL parallel einsetzt, etwa weil ein Legacy-System auf MySQL laeuft und neue Microservices auf PostgreSQL, muss diese Unterschiede kennen, um nicht in Produktion von unerwartetem Verhalten ueberrascht zu werden. Die folgenden Abschnitte gehen die praktisch relevantesten Unterschiede zwischen PostgreSQL und MySQL systematisch durch.
2. Datentypen: wo PostgreSQL und MySQL grundlegend abweichen
Der erste Stolperstein zwischen PostgreSQL und MySQL sind die Datentypen selbst. PostgreSQL bietet einen echten BOOLEAN-Typ mit den Werten true, false und null, waehrend MySQL BOOLEAN lediglich als Alias fuer TINYINT(1) behandelt. Das bedeutet: In MySQL kann in eine Boolean-Spalte technisch auch der Wert 2 geschrieben werden, ohne dass die Datenbank protestiert. PostgreSQL wuerde diesen Wert konsequent ablehnen. Fuer Anwendungen, die auf strikte Datenintegritaet angewiesen sind, ist das ein relevanter Unterschied zwischen PostgreSQL und MySQL.
Ein weiterer wichtiger Unterschied betrifft Arrays: PostgreSQL unterstuetzt native Array-Spalten (INTEGER[], TEXT[]), MySQL kennt dieses Konzept nicht und erfordert stattdessen eine separate Tabelle oder eine JSON-Spalte als Workaround. Auch bei Enum-Typen unterscheiden sich beide Systeme: PostgreSQL erlaubt benutzerdefinierte Enum-Typen ueber CREATE TYPE, die wiederverwendbar sind, waehrend MySQL Enums direkt an der Spaltendefinition festmacht, was Aenderungen aufwendiger macht, weil die gesamte Spalte veraendert werden muss.
-- PostgreSQL: strict boolean type and reusable enum type
CREATE TYPE order_status AS ENUM ('pending', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending',
is_paid BOOLEAN NOT NULL DEFAULT false,
tags TEXT[] DEFAULT '{}'
);
-- MySQL: enum tied to the column, boolean is just TINYINT(1)
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
status ENUM('pending', 'shipped', 'delivered', 'cancelled') NOT NULL DEFAULT 'pending',
is_paid TINYINT(1) NOT NULL DEFAULT 0
-- No native array type, tags need a separate table
);
3. String-Funktionen und Concat-Syntax im Vergleich
Beim Verketten von Strings zeigt sich einer der sichtbarsten Syntax-Unterschiede zwischen PostgreSQL und MySQL. PostgreSQL folgt dem SQL Standard und nutzt den Operator ||, waehrend MySQL traditionell die Funktion CONCAT() verwendet, weil || in MySQL je nach SQL-Modus als logisches ODER interpretiert werden kann. Wer Code zwischen beiden Systemen portieren will, sollte konsequent auf CONCAT() setzen, weil PostgreSQL diese Funktion ebenfalls unterstuetzt, MySQL aber den ||-Operator standardmaessig nicht als Verkettung interpretiert.
Auch bei der Gross- und Kleinschreibung von Vergleichsoperationen gibt es Unterschiede. MySQL vergleicht Strings standardmaessig case-insensitive, sofern die Spalte mit einer entsprechenden Collation wie utf8mb4_general_ci definiert ist. PostgreSQL vergleicht standardmaessig case-sensitive, und wer case-insensitive Vergleiche braucht, muss explizit ILIKE statt LIKE verwenden oder die Spalte auf CITEXT umstellen. Dieser Unterschied ist einer der haeufigsten Ursachen fuer nicht gefundene Datensaetze nach einer Migration von MySQL zu PostgreSQL.
-- PostgreSQL: portable concatenation and case-insensitive search
SELECT first_name || ' ' || last_name AS full_name FROM customers;
SELECT * FROM customers WHERE email ILIKE '%@example.com';
-- MySQL: CONCAT works the same in both systems, || does not
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;
SELECT * FROM customers WHERE email LIKE '%@example.com'; -- case-insensitive by default collation
4. LIMIT, OFFSET und Pagination-Syntax
Bei einfacher Pagination sind sich PostgreSQL und MySQL erstaunlich aehnlich: beide unterstuetzen LIMIT n OFFSET m. Der Unterschied zeigt sich erst bei der Reihenfolge der Klauseln und bei alternativen Kurzschreibweisen. MySQL erlaubt zusaetzlich die Kurzform LIMIT offset, count, also mit vertauschter Reihenfolge und Komma statt OFFSET-Schluesselwort, was in PostgreSQL nicht funktioniert und beim Kopieren von Queries zwischen beiden Systemen zu einem sofortigen Syntaxfehler fuehrt.
Ein zweiter, subtilerer Unterschied betrifft LIMIT in Kombination mit UPDATE und DELETE. MySQL erlaubt DELETE FROM tabelle LIMIT 10 direkt, PostgreSQL kennt diese Syntax nicht und erfordert stattdessen eine Subquery mit ctid oder einer Sortierspalte, um dasselbe Verhalten zu erreichen. Wer Wartungsskripte fuer beide Datenbanken schreibt, muss diesen Unterschied explizit beruecksichtigen, sonst schlaegt das Skript auf PostgreSQL mit einem Syntaxfehler fehl.
-- PostgreSQL: standard LIMIT/OFFSET, no LIMIT on UPDATE/DELETE
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 40;
DELETE FROM products
WHERE id IN (
SELECT id FROM products WHERE stock = 0 ORDER BY id LIMIT 10
);
-- MySQL: LIMIT offset, count shorthand and direct LIMIT on DELETE
SELECT * FROM products ORDER BY id LIMIT 40, 20;
DELETE FROM products WHERE stock = 0 ORDER BY id LIMIT 10;
5. Auto-Increment: SERIAL vs. AUTO_INCREMENT
Fuer automatisch hochzaehlende Primaerschluessel nutzen PostgreSQL und MySQL komplett unterschiedliche Mechanismen. MySQL kennt das Spaltenattribut AUTO_INCREMENT, das direkt in die Spaltendefinition geschrieben wird. PostgreSQL kennt historisch den Pseudo-Typ SERIAL, der im Hintergrund eine Sequenz erzeugt und die Spalte per DEFAULT nextval(...) daran koppelt. Seit PostgreSQL 10 ist die empfohlene, standardnaehere Alternative GENERATED ALWAYS AS IDENTITY, die dem SQL Standard folgt und mit dem urspruenglichen SERIAL weitgehend austauschbar ist.
Der praktische Unterschied zeigt sich beim expliziten Einfuegen von Werten. Bei MySQL kann man in eine AUTO_INCREMENT-Spalte problemlos einen expliziten Wert einfuegen, der Zaehler passt sich automatisch an. Bei PostgreSQL mit GENERATED ALWAYS AS IDENTITY ist ein expliziter Insert standardmaessig verboten und erfordert die Klausel OVERRIDING SYSTEM VALUE. Wer GENERATED BY DEFAULT AS IDENTITY statt ALWAYS verwendet, erhaelt ein Verhalten, das dem von MySQL naeher kommt.
-- PostgreSQL: modern identity column (SQL standard compliant)
CREATE TABLE customers (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
-- Legacy syntax, still widely used and functionally equivalent
CREATE TABLE customers_legacy (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
-- MySQL: AUTO_INCREMENT column attribute
CREATE TABLE customers (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE
);
6. Case-Sensitivity bei Identifiern und Strings
Ein oft uebersehener Unterschied zwischen PostgreSQL und MySQL liegt in der Behandlung von Gross- und Kleinschreibung bei Bezeichnern wie Tabellen- und Spaltennamen. PostgreSQL wandelt unquotierte Identifier automatisch in Kleinbuchstaben um, sodass SELECT * FROM Products und SELECT * FROM products identisch behandelt werden, es sei denn, der Tabellenname wurde bei der Erstellung mit doppelten Anfuehrungszeichen als "Products" quotiert. MySQL dagegen behandelt Tabellennamen auf Linux-Systemen standardmaessig case-sensitive, weil sie direkt auf Dateisystemebene abgebildet werden, auf Windows und macOS hingegen oft case-insensitive.
Diese Inkonsistenz zwischen Betriebssystemen ist eine haeufige Quelle fuer Fehler, die lokal auf einem Windows-Entwicklungsrechner unbemerkt bleiben, aber auf einem Linux-Produktionsserver mit MySQL sofort auffallen. Die Einstellung lower_case_table_names in der MySQL-Konfiguration steuert dieses Verhalten, sollte aber nie nachtraeglich auf einem bestehenden System geaendert werden, weil das zu inkonsistenten Tabellennamen fuehren kann. PostgreSQL ist in dieser Hinsicht konsistenter, weil das Verhalten unabhaengig vom Betriebssystem immer gleich ist.
7. JSON-Unterstuetzung: JSONB vs. JSON-Spalte
Beide Datenbanken unterstuetzen JSON-Daten, aber mit unterschiedlicher Tiefe. PostgreSQL bietet zwei JSON-Typen: JSON, das den Text unveraendert speichert, und JSONB, das den Inhalt in einem binaeren, indexierbaren Format ablegt. JSONB ist fuer nahezu alle praktischen Anwendungsfaelle die richtige Wahl, weil es GIN-Indizes fuer schnelle Abfragen auf einzelne JSON-Schluessel unterstuetzt und Operatoren wie ->, ->> und @> fuer komfortable Abfragen bereitstellt.
MySQL kennt seit Version 5.7 einen nativen JSON-Datentyp, der intern ebenfalls binaer gespeichert wird, jedoch mit einer anderen Operatorsyntax arbeitet: JSON_EXTRACT() beziehungsweise der Kurzform -> fuer den Zugriff auf Werte, ->> fuer die unquotierte Textextraktion. Funktional sind sich beide Systeme naeher gekommen, aber die konkreten Funktionsnamen und Indexstrategien unterscheiden sich weiterhin, sodass JSON-lastige Abfragen bei einer Migration zwischen PostgreSQL und MySQL fast immer neu geschrieben werden muessen.
-- PostgreSQL: JSONB with GIN index and containment operator
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
payload JSONB NOT NULL
);
CREATE INDEX idx_events_payload ON events USING GIN (payload);
SELECT payload->>'event_type' AS event_type
FROM events
WHERE payload @> '{"source": "checkout"}';
-- MySQL: JSON type with generated column for indexing
CREATE TABLE events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
payload JSON NOT NULL,
event_type VARCHAR(64) AS (payload->>'$.event_type') STORED,
INDEX idx_event_type (event_type)
);
SELECT payload->>'$.event_type' AS event_type
FROM events
WHERE JSON_CONTAINS(payload, '"checkout"', '$.source');
8. Upsert-Syntax: ON CONFLICT vs. ON DUPLICATE KEY UPDATE
Fuer den Fall, dass ein Insert bei einem bestehenden Datensatz stattdessen ein Update ausloesen soll, bieten beide Datenbanken eine eigene Upsert Syntax an, die sich grundlegend unterscheidet. PostgreSQL nutzt die Klausel INSERT ... ON CONFLICT (spalte) DO UPDATE SET ..., die explizit angibt, auf welchen Unique Constraint sich der Konflikt bezieht. MySQL verwendet INSERT ... ON DUPLICATE KEY UPDATE ..., das implizit auf jeden verletzten Unique- oder Primary-Key-Constraint der Tabelle reagiert, ohne dass man die betroffene Spalte explizit benennen muss.
Dieser Unterschied hat praktische Konsequenzen: Bei PostgreSQL kann man gezielt entscheiden, dass ein Konflikt bei einem bestimmten Unique Index ignoriert wird (DO NOTHING), waehrend ein Konflikt bei einem anderen Constraint einen Fehler wirft. MySQL bietet diese Granularitaet nicht, dort greift die Update-Klausel bei jedem verletzten Key. Wer Upsert-Logik zwischen beiden Systemen portabel halten will, muss diesen semantischen Unterschied in der Anwendungslogik selbst abbilden, weil eine reine Syntax-Uebersetzung nicht ausreicht.
-- PostgreSQL: explicit conflict target, precise control
INSERT INTO product_stats (product_id, view_count)
VALUES (42, 1)
ON CONFLICT (product_id)
DO UPDATE SET view_count = product_stats.view_count + 1;
-- MySQL: implicit conflict on any unique/primary key violation
INSERT INTO product_stats (product_id, view_count)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE view_count = view_count + 1;
9. PostgreSQL vs. MySQL im direkten Vergleich
Die folgende Tabelle fasst die wichtigsten Syntax-Unterschiede zwischen PostgreSQL und MySQL zusammen, die in der taeglichen Praxis am haeufigsten zu Migrationsfehlern fuehren.
| Feature | PostgreSQL | MySQL | Praxis-Hinweis |
|---|---|---|---|
| Auto-Increment | GENERATED ALWAYS AS IDENTITY |
AUTO_INCREMENT |
Explizite Inserts verhalten sich unterschiedlich |
| String-Verkettung | || oder CONCAT() |
CONCAT() nur |
CONCAT() fuer portablen Code nutzen |
| Upsert | ON CONFLICT ... DO UPDATE |
ON DUPLICATE KEY UPDATE |
PostgreSQL erlaubt gezielten Conflict Target |
| JSON | JSONB mit GIN-Index |
JSON mit Generated Column |
Operatorsyntax unterscheidet sich vollstaendig |
| Case-Sensitivity | Immer case-sensitive bei Strings | Abhaengig von Collation und OS | ILIKE in PostgreSQL fuer Toleranz nutzen |
Diese Tabelle zeigt exemplarisch, warum ein pauschales Suchen und Ersetzen bei der Migration zwischen PostgreSQL und MySQL nicht funktioniert. Jeder dieser Punkte erfordert eine bewusste Entscheidung im Code, nicht nur eine mechanische Uebersetzung der Syntax. Teams, die beide Datenbanken parallel unterstuetzen, profitieren davon, eine Abstraktionsschicht wie ein Query Builder oder ORM zu nutzen, das diese Unterschiede kapselt, statt Rohdaten-SQL in der gesamten Codebasis zu verstreuen.
Mironsoft
Datenbank-Architektur, Migrationen und Performance-Beratung
Migration zwischen PostgreSQL und MySQL geplant?
Wir analysieren eure bestehenden Queries, identifizieren kritische Syntax-Unterschiede und begleiten die Migration von der Schema-Uebersetzung bis zum produktiven Cutover, ohne dass Datenintegritaet oder Performance auf der Strecke bleiben.
Schema-Audit
Analyse aller Datentypen, Constraints und dialektspezifischer Features vor der Migration
Query-Portierung
Uebersetzung von Upsert-, JSON- und Pagination-Logik in beide Richtungen
Cutover-Begleitung
Testplaene, Rollback-Strategie und Monitoring fuer den produktiven Wechsel
10. Zusammenfassung
Der Vergleich zwischen PostgreSQL und MySQL zeigt, dass beide Systeme zwar denselben SQL Standard als gemeinsame Basis nutzen, in der praktischen Umsetzung aber deutlich voneinander abweichen. Datentypen wie BOOLEAN und Arrays werden unterschiedlich streng behandelt, Auto-Increment-Spalten folgen unterschiedlichen Mechanismen, und Upsert Syntax sowie JSON-Operatoren erfordern jeweils eigenen Code. Wer diese Unterschiede kennt, bevor eine Migration beginnt, spart sich wochenlange Fehlersuche in Produktion.
Der wichtigste praktische Rat: Bei parallelem Betrieb von PostgreSQL und MySQL, etwa in einer Microservice-Landschaft mit mehreren Teams, lohnt sich eine dokumentierte Uebersetzungstabelle im Team-Wiki, ergaenzt um automatisierte Tests, die genau die in diesem Artikel beschriebenen Randfaelle abdecken. So wird aus einem potenziellen Produktionsfehler ein bekannter, dokumentierter Unterschied zwischen PostgreSQL und MySQL, den jedes Teammitglied nachschlagen kann.
PostgreSQL vs. MySQL: Das Wichtigste auf einen Blick
Datentypen
PostgreSQL hat echte Booleans und native Arrays, MySQL bildet beides mit Workarounds nach.
Upsert
ON CONFLICT in PostgreSQL erlaubt gezielten Conflict Target, ON DUPLICATE KEY UPDATE in MySQL greift implizit.
JSON
JSONB mit GIN-Index in PostgreSQL, JSON mit Generated Column in MySQL, Operatoren unterscheiden sich.
Case-Sensitivity
PostgreSQL ist konsistent case-sensitive, MySQL haengt von Collation und Betriebssystem ab.