Relational Database

MySQL

MySQL ist die relationale Datenbank hinter einem riesigen Teil des Webs. InnoDB ermöglicht echte Transaktionen, Replikation vereinfacht die Skalierung von Lesezugriffen und das Ökosystem ist enorm.

intermediate15 min readUpdated 16. Sept. 2026
schema.sql
sql
-- schema.sql
CREATE TABLE products (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  sku         VARCHAR(64) NOT NULL,
  name        VARCHAR(200) NOT NULL,
  price_cents INT UNSIGNED NOT NULL,
  stock       INT NOT NULL DEFAULT 0,
  attributes  JSON NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uniq_products_sku (sku)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Veröffentlicht
1995
Lizenz
GPL / kommerziell (Oracle)
Storage Engine
InnoDB (Standard)
Standard-Isolation
Repeatable Read
Charset
utf8mb4
Aktuelle Hauptversion
9.x

Warum es wichtig ist

Warum MySQL immer noch das Web antreibt

InnoDB Transaktionen

Row-Level Locking, ACID-Transaktionen und Crash Recovery werden von der Standard-Engine gehandhabt, sodass Konsistenz kein nachträglicher Gedanke ist.

Replikation für Lese-Skalierung

Ein Primary streamt sein Binary Log an Replicas; diese übernehmen den Lese-Traffic oder stehen für ein Failover bereit.

Ein enormes Ökosystem

Jedes Framework, jede Sprache und jeder Hosting-Provider unterstützt MySQL, was bedeutet, dass Tutorials, Treiber und Managed Services allgegenwärtig sind.

Das Gesamtbild

Die drei Kernideen hinter MySQL

InnoDB für Transaktionen, Indizes für Geschwindigkeit und Replikation für die Lese-Skalierung – die Engine wurde gezielt für diese drei Aufgaben optimiert.

InnoDB

Commit

Die Standard-Storage-Engine bietet Row-Level Locking, MVCC-Reads, Foreign Keys und automatische Crash Recovery.

Indizes

Locate

B-Tree-Indizes verwandeln Full Scans in gezielte Lookups, und die Entscheidung des Optimizers ist über EXPLAIN einsehbar.

Verbindungen

Serve

MySQL handhabt jede Verbindung mit einem Thread, weshalb Connection-Pools auf der Anwendungsseite verhindern, dass tausende Clients den Server überlasten.

Datenmodell

Eine Zeile pro Produkt

Eine Zeile repräsentiert ein einzelnes verkaufbares Produkt mit einer stabilen SKU, einem Integer-Preis und einem JSON-Objekt für variable Attribute.

Die Produkte-TabelleMySQL table
  • idbigint unsignedAUTO_INCREMENT Primary Key
  • skuvarchar(64)Menschenlesbare, eindeutige Stock-Keeping Unit
  • namevarchar(200)Anzeigename für Kunden
  • price_centsint unsignedPreis in kleinsten Einheiten; vermeidet Floating-Point-Probleme bei Geldwerten
  • stockintVorhandene Einheiten; kann bei Korrekturen negativ sein
  • attributesjsonVariable Eigenschaften wie Farbe, Größe oder Material

Eine Zeile repräsentiert ein einzelnes verkaufbares Produkt mit einer stabilen SKU, einem Integer-Preis und einem JSON-Objekt für variable Attribute.

Eine kurze Geschichte

Von 18 Tags zu einem lebendigen Standard

  1. 1995

    MySQL wird veröffentlicht

    Michael Widenius und David Axmark veröffentlichen eine schnelle, einfache Datenbank, die auf leseintensive Web-Workloads ausgelegt ist.

    95
  2. 2000

    GPL-Lizenzierung und der LAMP-Stack

    Die Open-Source-Lizenzierung hilft MySQL, das 'M' in Linux, Apache, MySQL und PHP zu werden.

    00
  3. 2008

    Sun übernimmt MySQL AB

    Das Unternehmen wird von Sun Microsystems gekauft und zwei Jahre später an Oracle übergeben.

    08
  4. 2010

    InnoDB wird Standard

    MySQL 5.5 macht die transaktionale Engine zum Standard und ersetzt MyISAM für ernsthafte Workloads.

    10
  5. 2018

    MySQL 8 überarbeitet die Internals

    Ein transaktionales Data Dictionary, CTEs, Window Functions und besseres JSON werden gleichzeitig eingeführt.

    18

Der vollständige Leitfaden

MySQL: Alles was Sie wissen müssen

Was ist MySQL?

MySQL ist eine Open-Source-relationale Datenbank, die zur Standard-Storage-Layer des frühen Webs wurde. Sie wurde 1995 mit einem Fokus auf Geschwindigkeit und Einfachheit für leseintensive Seiten veröffentlicht und wuchs zusammen mit dem LAMP-Stack — Linux, Apache, MySQL und PHP — zu einer der am weitesten verbreiteten Datenbanken überhaupt heran.

Oracle entwickelt sie mittlerweile weiter, aber eine große Community und eine Vielzahl an Managed Services sorgen dafür, dass sie omnipräsent bleibt. WordPress, Magento, E-Commerce-Plattformen im Stil von Shopify und zahllose maßgeschneiderte Anwendungen laufen auf MySQL. Wenn Sie eine Website mit einem Login-Formular genutzt haben, ist die Chance groß, dass eine MySQL-Tabelle involviert war.

Das moderne MySQL ist nicht mehr die einfache Engine der 1990er Jahre. Seit Version 8 verfügt es über ein transaktionales Data Dictionary, Common Table Expressions, Window Functions und einen leistungsfähigen JSON-Support. MySQL heute zu lernen bedeutet, eine ernstzunehmende relationale Datenbank zu erlernen, die zudem außerordentlich gut unterstützt wird.

Wo MySQL läuft

Der größte praktische Vorteil von MySQL ist seine Allgegenwärtigkeit. Fast jeder Shared-Hoster, Cloud-Provider und Platform-as-a-Service bietet es an. Frameworks liefern passende Treiber mit, ORMs unterstützen es nativ und DBAs verfügen über jahrzehntelange Erfahrung damit. Das reduziert die Kosten für alles rund um die Datenbank: Recruiting, Tooling, Monitoring und Migration.

Es ist die Standardwahl für Content-Management-Systeme, E-Commerce, SaaS-Backends und jede Anwendung, bei der das Ökosystem genauso wichtig ist wie die Engine selbst. Es skaliert von einer einzelnen kleinen Instanz bis hin zu sharded clusters, und der Weg von der einen zur anderen Variante ist bestens dokumentiert.

InnoDB ist die entscheidende Engine

MySQL verfügt über eine plugbare Storage-Engine-Architektur, aber in der Praxis werden Sie InnoDB verwenden. Diese bietet:

  • ACID-Transaktionen mit COMMIT und ROLLBACK.
  • Row-level locking, sodass Schreibvorgänge keine Lesevorgänge blockieren.
  • Foreign keys und die Durchsetzung von Constraints.
  • Crash recovery über ein Redo-Log.
  • MVCC für konsistente Reads.

Der ältere MyISAM-Engine fehlten Transaktionen und Row-level locking. In einem neuen Schema hat sie nichts zu suchen. Deklarieren Sie ENGINE=InnoDB immer explizit, damit die Wahl sichtbar ist und nicht von den Server-Defaults abhängt.

CREATE TABLE products (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  sku         VARCHAR(64) NOT NULL,
  name        VARCHAR(200) NOT NULL,
  price_cents INT UNSIGNED NOT NULL,
  stock       INT NOT NULL DEFAULT 0,
  attributes  JSON NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uniq_products_sku (sku)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Datentypen und AUTO_INCREMENT

Das Typsystem von MySQL ist pragmatisch. Die gängigsten Optionen:

  • INT und BIGINT — ganze Zahlen; verwenden Sie UNSIGNED für IDs und Zähler, die nicht negativ sein dürfen.
  • VARCHAR(n) — Text mit variabler Länge und einem Maximum. Im Gegensatz zu Postgres profitiert MySQL tatsächlich von einer sinnvollen Längenangabe.
  • TEXT — große Texte, die bei Bedarf außerhalb der Zeile gespeichert werden.
  • DECIMAL(p, s) — exakte Dezimalzahlen; der richtige Typ für Geldbeträge, sofern Sie keine Integer-Minor-Units verwenden.
  • TIMESTAMP und DATETIME — Zeitstempel; TIMESTAMP konvertiert in UTC und hat eine Bereichsbeschränkung, während DATETIME die Werte genau so speichert, wie sie übergeben werden.
  • JSON — ein validiertes JSON-Dokument, das effizient gespeichert wird.
  • ENUM — eine feste Menge an Strings; praktisch, aber eine Änderung der Liste erfordert eine Schema-Änderung.

AUTO_INCREMENT generiert die nächste Ganzzahl für eine Spalte, fast immer den Primary Key. Es ist schnell und toleriert Lücken: Rollbacks bei Inserts verbrauchen einen Wert, und gleichzeitige Inserts führen unter Umständen nicht zu fortlaufenden Nummern. Verlassen Sie sich niemals darauf, dass die ID lückenlos ist oder dass ihre Reihenfolge eine bestimmte Bedeutung hat.

INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-1', 'Widget', 999, 10, JSON_OBJECT('colour', 'blue'));

SELECT LAST_INSERT_ID();

CRUD ohne Überraschungen

Die vier Grundoperationen lassen sich auf vier Statements übertragen. Betrachten Sie diese als Set, da sich das Muster wiederholt.

INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-2', 'Gadget', 1499, 5, JSON_OBJECT('colour', 'red'));

SELECT id, sku, name, price_cents
FROM products
WHERE stock > 0
ORDER BY price_cents ASC
LIMIT 20 OFFSET 40;

UPDATE products
SET price_cents = 1299, stock = stock - 1
WHERE id = 42;

DELETE FROM products
WHERE stock = 0 AND created_at < NOW() - INTERVAL 90 DAY;

Zwei Gewohnheiten verhindern die klassischen Fehler. Erstens: Fügen Sie bei UPDATE und DELETE immer eine WHERE-Klausel hinzu; ohne diese wird jede einzelne Zeile geändert. Zweitens: Führen Sie zuerst das entsprechende SELECT aus, um zu bestätigen, welche Zeilen Sie gerade beeinflussen werden. MySQL verfügt über einen sql_safe_updates-Modus, der Statements ohne Key in der WHERE ablehnt – dies in der Entwicklung zu aktivieren, ist eine einfache und effektive Sicherheitsmaßnahme.

LIMIT mit OFFSET ermöglicht die Paginierung, allerdings scannen große Offsets Zeilen, nur um sie dann wieder zu verwerfen. Für eine tiefe Paginierung ist Keyset-Pagination vorzuziehen: WHERE id > :last_id ORDER BY id LIMIT 20.

Joins und Aggregation

Joins kombinieren Tabellen basierend auf einer Übereinstimmungsbedingung, genau wie in Standard-SQL.

SELECT c.name             AS category,
       COUNT(*)           AS product_count,
       SUM(p.price_cents) AS inventory_value
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE p.stock > 0
GROUP BY c.id, c.name
ORDER BY inventory_value DESC
LIMIT 20;

JOIN behält passende Paare bei, LEFT JOIN behält alle Zeilen der linken Tabelle und füllt fehlende Spalten der rechten Tabelle mit NULL auf. Aggregate wie COUNT, SUM, AVG, MIN und MAX fassen Gruppen zusammen; jede ausgewählte Spalte, die nicht aggregiert wird, muss gruppiert werden.

MySQL erlaubte es historisch, nicht gruppierte Spalten auszuwählen, und gab einen beliebigen Wert zurück, was Bugs verschleierte. Wenn ONLY_FULL_GROUP_BY aktiviert ist – der Standard in MySQL 8 –, lehnt der Server mehrdeutige Abfragen ab, was genau das gewünschte Verhalten ist. Deaktivieren Sie dies nicht, nur um eine alte Abfrage zum Laufen zu bringen; korrigieren Sie stattdessen die Abfrage.

WHERE filtert Zeilen vor der Gruppierung und HAVING filtert danach, daher gehören Bedingungen für Aggregate in HAVING.

Indexe und EXPLAIN

Ein Index ist eine sortierte Struktur, die einen Full Table Scan vermeidet. MySQL erstellt automatisch einen Index für den Primary Key und für jeden UNIQUE-Constraint. Fügen Sie weitere Indexe für die Spalten hinzu, nach denen Sie filtern, joinen oder sortieren.

CREATE INDEX idx_products_price ON products (price_cents);

Ein Composite Index deckt mehrere Spalten ab und folgt der Leftmost-Prefix-Regel: Ein Index auf (category_id, price_cents) hilft bei Abfragen, die nach category_id filtern, oder nach category_id und price_cents, aber nicht nach price_cents allein. Ordnen Sie die Spalten so an, dass die selektivsten und am häufigsten gefilterten Spalten außen stehen.

Fragen Sie den Optimizer, was er tun wird:

EXPLAIN
SELECT id, name, price_cents
FROM products
WHERE price_cents < 2500
ORDER BY price_cents
LIMIT 25;

Lesen Sie zuerst die Spalte type: const, eq_ref und ref sind gut; range ist akzeptabel; index und ALL bedeuten einen Scan. Die Spalte key zeigt an, welcher Index gewählt wurde, und rows schätzt, wie viele Datensätze geprüft werden. Ein großer Wert in rows bei einem kleinen Ergebnis deutet meist auf einen fehlenden oder unbrauchbaren Index hin. Verwenden Sie in MySQL 8 EXPLAIN ANALYZE, um die Abfrage auszuführen und die tatsächlichen Timings zu sehen.

Covering Indexes verdienen eine Erwähnung. Wenn ein Index jede Spalte enthält, die eine Abfrage benötigt, kann MySQL die Antwort allein aus dem Index liefern und muss die eigentliche Zeile nie anfassen. Eine Spalte nur deshalb zu einem Index hinzuzufügen, um ihn zu einem Covering Index zu machen, bringt oft einen erheblichen Performance-Gewinn.

utf8mb4 und die Charset-Falle

Zeichensätze sind ein Bereich, in dem MySQL oft für Überraschungen sorgt. Über einen Großteil seiner Geschichte war der Standard-utf8 auf maximal drei Bytes pro Zeichen beschränkt. Das deckt zwar die meisten Texte ab, aber keine vier-Byte-Codepoints wie Emojis oder viele seltene Schriften. Der Versuch, ein Emoji in einer utf8-Spalte zu speichern, führt je nach Server-Modus entweder zu einem Fehler oder dazu, dass der Wert abgeschnitten wird.

Die Lösung ist utf8mb4, welches echtes UTF-8 ist und alles speichern kann. Setzen Sie dies auf jeder Ebene – Server, Datenbank, Tabelle und Verbindung:

CREATE DATABASE shop
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_0900_ai_ci;

Die Collation (Kollation) bestimmt, wie Strings verglichen und sortiert werden. utf8mb4_0900_ai_ci ist akzent- und case-insensitive, was in der Regel den Erwartungen der Nutzer bei einer Suche entspricht. Collations beeinflussen zudem das Verhalten von Indizes. Halten Sie diese daher bei verknüpften Spalten konsistent; eine Diskrepanz erzwingt Konvertierungen, die einen Index unbrauchbar machen können.

Transaktionen und Isolationsstufen

InnoDB bietet Transaktionen an. Gruppieren Sie zusammengehörige Schreibvorgänge, sodass diese gemeinsam erfolgreich abgeschlossen werden oder gemeinsam fehlschlagen.

START TRANSACTION;

UPDATE inventory SET quantity = quantity - 1
WHERE product_id = 42 AND quantity >= 1;

INSERT INTO orders (product_id, quantity)
VALUES (42, 1);

COMMIT;

Prüfen Sie die betroffenen Zeilen und ROLLBACK, falls das geschützte Update nichts geändert hat. Die Standard-Isolationsstufe ist Repeatable Read, welche einen konsistenten Snapshot für die Transaktion bereitstellt und in InnoDB Gap Locks setzt, um Phantom-Zeilen zu verhindern. Die anderen Stufen sind Read Uncommitted, Read Committed und Serializable.

Ein wichtiger Unterschied zu Postgres: Aufgrund des Gap Lockings unter Repeatable Read können gleichzeitige Inserts in einen Bereich leichter zu Blockaden oder Deadlocks führen. Halten Sie Transaktionen kurz, aktualisieren Sie Zeilen in einer konsistenten Reihenfolge und stellen Sie sich darauf ein, einen Deadlock zu wiederholen – InnoDB meldet diesen als Fehler, anstatt den Zustand zu korrumpieren.

Replikation und Read-Scaling

Die meisten MySQL-Deployments skalieren Lesezugriffe (Reads) vor Schreibzugriffen (Writes). Die Primary-Instanz zeichnet jede Änderung in ihrem Binary Log auf, und eine oder mehrere Replicas verbinden sich damit, um dieses Log zu reproduzieren. Standardmäßig ist die Replikation asynchron, sodass eine Replica der Primary um Millisekunden oder mehr hinterherhinken kann.

Das gängige Muster besteht darin, Schreibvorgänge an die Primary zu senden und Lesezugriffe auf die Replicas zu verteilen, wobei akzeptiert wird, dass ein Read kurzzeitig leicht veraltete Daten zurückgeben kann. Für eine Read-after-Write-Konsistenz sollten die Lesezugriffe eines Nutzers für ein kurzes Zeitfenster nach seinem Schreibvorgang an die Primary geroutet werden, oder die Replica nur für Daten genutzt werden, bei denen ein Lag tolerierbar ist.

Replikation bietet zudem Hochverfügbarkeit. Wenn die Primary ausfällt, kann eine Replica befördert werden. Tools wie Orchestratoren und Managed Services automatisieren diesen Failover, aber man muss den Trade-off zwischen synchronen und asynchronen Modi verstehen: Der synchrone Modus wartet auf die Replicas und gefährdet die Verfügbarkeit, während der asynchrone Modus das Risiko birgt, bei einem Failover die letzten paar Transaktionen zu verlieren.

Upserts mit ON DUPLICATE KEY UPDATE

Das idiomatische Upsert in MySQL ist INSERT ... ON DUPLICATE KEY UPDATE. Wenn das Insert einen Primary Key oder Unique Key verletzen würde, wird stattdessen die Update-Klausel ausgeführt.

INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-1', 'Widget', 1099, 5, JSON_OBJECT('colour', 'blue'))
ON DUPLICATE KEY UPDATE
  price_cents = VALUES(price_cents),
  stock       = stock + VALUES(stock);

Dies ist atomar, was besonders bei Countern und Lagerbeständen wichtig ist. Die Alternative – zuerst ein Select auszuführen und dann zu entscheiden, ob ein Insert oder Update nötig ist – birgt ein Race-Condition-Fenster, in dem zwei Sessions gleichzeitig feststellen, dass keine Zeile existiert, und beide ein Insert ausführen. Nutzen Sie das Upsert immer dann, wenn die Operation tatsächlich ein „Insert-oder-Update“ ist.

JSON-Spalten

Der Typ JSON in MySQL speichert ein validiertes Dokument und unterstützt Funktionen, um Teile davon zu lesen und zu schreiben.

SELECT id, name,
       attributes->>'$.colour' AS colour
FROM products
WHERE attributes->>'$.colour' = 'blue';

Sie können JSON mithilfe von generierten Spalten indexieren. Da MySQL einen JSON-Ausdruck nicht direkt indexieren kann, extrahieren Sie diesen in eine gespeicherte generierte Spalte und indexieren Sie diese:

ALTER TABLE products
  ADD COLUMN colour VARCHAR(32)
    GENERATED ALWAYS AS (attributes->>'$.colour') STORED,
  ADD INDEX idx_products_colour (colour);

Verwenden Sie JSON für variable Attribute und Payloads von Drittanbietern. Behalten Sie Felder, nach denen Sie ständig filtern, als echte Spalten bei; der Trick mit den generierten Spalten funktioniert zwar, aber eine native Spalte mit einem echten Typ ist einfacher und schneller.

Stored Procedures: sparsam einsetzen

MySQL unterstützt stored procedures, Funktionen, Trigger und geplante Events. Diese können die Anzahl der Round Trips reduzieren und Logik zentralisieren, verschieben die Business-Logik jedoch in eine Sprache, die schwerer zu testen, zu versionieren und zu debuggen ist als Ihr Anwendungscode.

Nutzen Sie diese für Aufgaben, die wirklich in die Datenbank gehören: Massenwartung, Datenmigrationen und Jobs, die nah an den Daten ausgeführt werden müssen. Vermeiden Sie es, Kernregeln Ihrer Domain in Procedures zu schreiben, die nur ein einziges Team versteht. Trigger werden besonders leicht vergessen; ein UPDATE, das stillschweigend drei Trigger auslöst, ist schwer nachvollziehbar, und versteckte Seiteneffekte überraschen früher oder später jeden.

Benutzer, Berechtigungen und Backups

Erstellen Sie einen dedizierten Anwendungsbenutzer, der nur die benötigten Privilegien besitzt, anstatt sich als root zu verbinden.

CREATE USER 'app'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app'@'%';
FLUSH PRIVILEGES;

Halten Sie DDL und Migrationen in einem separaten, höher privilegierten Account. Schränken Sie nach Möglichkeit die erlaubten Hosts ein, fordern Sie TLS und rotieren Sie die Zugangsdaten.

Für Backups erstellt mysqldump einen logischen Dump, der einfach zu verschieben und wiederherzustellen ist:

mysqldump --single-transaction --routines --triggers shop > shop.sql
mysql shop_restore < shop.sql

Das Flag --single-transaction erstellt einen konsistenten Snapshot von InnoDB-Tabellen, ohne diese für den gesamten Dump zu sperren. Bei großen Datenbanken, bei denen die Dump-Zeit zu hoch ist, sollten Sie ein physisches Tool wie Percona XtraBackup verwenden. In jedem Fall sollten Sie die Position des Binary Logs aufzeichnen, um eine Point-in-Time-Recovery zu ermöglichen, und die Wiederherstellung regelmäßig testen.

MySQL vs MariaDB

MariaDB begann als Community-Fork von MySQL nach der Übernahme durch Oracle und hat sich seitdem weiterentwickelt. Es behält den Großteil der MySQL-Syntax bei und fügt eigene Storage Engines und Funktionen hinzu. MySQL hingegen hat sich seit Version 8 schnell vorwärtsbewegt, unter anderem mit einem neuen Data Dictionary, Window Functions und verbessertem JSON.

Für die meisten Anwendungen sind die Unterschiede gering. Entscheiden Sie sich für MySQL, wenn Ihr Managed Provider oder Ihr Support-Vertrag darauf aufbaut, und für MariaDB, wenn Sie ein Community-gesteuertes Projekt bevorzugen oder eine seiner speziellen Engines benötigen. Die relationalen Konzepte, SQL und die operationalen Muster sind zwischen beiden übertragbar, sodass die Entscheidung selten eine endgültige Sackgasse ist.

Volltextsuche in InnoDB

InnoDB wird mit einem Volltextindex ausgeliefert, sodass eine Suchfunktion keinen separaten Dienst benötigt. Erstellen Sie einen FULLTEXT-Index über die Textspalten und fragen Sie diesen mit MATCH ... AGAINST ab.

CREATE FULLTEXT INDEX ft_products_search
  ON products (name, attributes);

SELECT id, name,
       MATCH(name, attributes) AGAINST ('running shoe' IN NATURAL LANGUAGE MODE) AS score
FROM products
WHERE MATCH(name, attributes) AGAINST ('running shoe' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC
LIMIT 20;

NATURAL LANGUAGE MODE sortiert nach Relevanz und ignoriert Wörter, die in den meisten Zeilen vorkommen. BOOLEAN MODE bietet Ihnen Operatoren wie +must, -exclude und "exact phrase" für eine präzisere Steuerung. Der Index hat eine minimale Token-Länge, die über innodb_ft_min_token_size gesteuert wird, sodass sehr kurze Wörter eventuell übersprungen werden.

Die Volltextsuche eignet sich gut für Produktkataloge und die Artikelsuche. Wechseln Sie zu einer dedizierten Engine, wenn Sie eine Toleranz gegenüber Tippfehlern, Faceting oder sprachübergreifende Analysen benötigen, die MySQL nicht bietet.

Views und generierte Spalten

Ein View ist eine benannte Abfrage, die sich wie eine Tabelle verhält. Dies ist nützlich, um eine gemeinsame Struktur zu kapseln und zu steuern, welche Spalten ein Reporting-User sehen kann.

CREATE VIEW in_stock AS
SELECT id, sku, name, price_cents, stock
FROM products
WHERE stock > 0;

SELECT * FROM in_stock WHERE price_cents < 2500;

MySQL 8 unterstützt Window-Funktionen, sodass Views auch analytische Ergebnisse vorstrukturieren können. Beachten Sie, dass ein View seine zugrunde liegende Abfrage jedes Mal neu ausführt, es sei denn, er wird manuell in eine Tabelle materialisiert; MySQL verfügt über keine nativen materialized views, weshalb Teams entweder eine Summary-Tabelle verwenden, die nach einem Zeitplan aktualisiert wird, oder ein Event nutzen.

Generierte Spalten, die im JSON-Abschnitt erläutert werden, sind die andere Seite dieses Konzepts: Eine stored generated column wird beim Schreiben berechnet und kann indexiert werden, während eine virtual column beim Lesen berechnet wird. Verwenden Sie eine stored column, wenn Sie den Ausdruck indexieren müssen, und eine virtual column, wenn Sie den Wert nur gelegentlich benötigen.

Schemas in einer Live-Datenbank ändern

ALTER TABLE in einer großen InnoDB-Tabelle kann dazu führen, dass die Tabelle neu aufgebaut wird und Sperren über einen langen Zeitraum gehalten werden. MySQL 8 unterstützt für viele Operationen Online DDL über ALGORITHM=INPLACE, wodurch die Tabelle in vielen Fällen neu aufgebaut wird, ohne gleichzeitige Lese- und Schreibzugriffe zu blockieren.

ALTER TABLE products
  ADD COLUMN updated_at TIMESTAMP NULL,
  ALGORITHM=INPLACE, LOCK=NONE;

ALTER TABLE products
  ADD INDEX idx_products_updated (updated_at),
  ALGORITHM=INPLACE, LOCK=NONE;

Nicht jede Änderung erfolgt online. Das Ändern eines Spaltentyps, das Hinzufügen eines FULLTEXT-Index oder das Neuaufbauen eines Primärschlüssels erfordert oft immer noch das Kopieren der Tabelle. Verwenden Sie in diesen Fällen Tools wie pt-online-schema-change oder gh-ost. Diese erstellen eine Schatten-Tabelle, kopieren die Zeilen in Batches und tauschen die Tabellen mit minimaler Blockierung aus. Unabhängig von der Methode sollten Schema-Änderungen über versionierte Migrationen durchgeführt und zuerst an einer Kopie der Produktionsdaten getestet werden.

Langsame Queries finden

MySQL zeichnet Queries, die long_query_time überschreiten, im slow query log auf, und EXPLAIN zeigt, wie eine spezifische Query ausgeführt wird. Zusammen sind sie der schnellste Weg von „die App ist langsam“ zu einem konkreten Fix.

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;

SHOW VARIABLES LIKE 'slow_query_log_file';

Das performance_schema und das sys Schema fassen dieselben Daten zusammen. Beginnen Sie mit den Statements, die die meiste Gesamtzeit beanspruchen – nicht mit der einzelnen langsamsten Query – und prüfen Sie EXPLAIN auf Full Scans bei großen Tabellen. SHOW PROFILE und EXPLAIN ANALYZE in MySQL 8 fügen Timings pro Stage hinzu, wenn Sie tiefer in die Analyse einsteigen müssen.

Daten schnell laden

Eine zeilenweise INSERT-Schleife ist der langsamste Weg, um Daten zu laden. Fassen Sie viele Zeilen in einem einzigen Statement zusammen; dies reduziert die Round-Trips und ermöglicht es InnoDB, Pages effizient zu schreiben.

INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES
  ('SKU-10', 'Widget', 999, 5, JSON_OBJECT('colour', 'blue')),
  ('SKU-11', 'Gadget', 1499, 3, JSON_OBJECT('colour', 'red')),
  ('SKU-12', 'Gizmo', 2499, 7, JSON_OBJECT('colour', 'green'));

Für Bulk-Imports streamt LOAD DATA INFILE eine Datei direkt in eine Tabelle und ist dramatisch schneller als jede INSERT-Form.

LOAD DATA LOCAL INFILE '/data/products.csv'
INTO TABLE products
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(sku, name, price_cents, stock, @attributes)
SET attributes = CAST(@attributes AS JSON);

Kapseln Sie große Ladevorgänge in eine Transaction, damit ein Fehler nicht zu einer halb importierten Tabelle führt. Erwägen Sie bei einem einmaligen Bulk-Load, sekundäre Indexes oder Unique-Checks zu deaktivieren und diese anschließend neu aufzubauen. Für alltägliche Schreibvorgänge in der Anwendung ist ein gebatchtes INSERT der richtige Standard.

Best Practices

  • Verwende InnoDB für jede Tabelle und deklariere dies explizit.
  • Erstelle Datenbanken, Tabellen und Verbindungen mit utf8mb4 und einer konsistenten Collation.
  • Speichere Geldbeträge als Integer-Kleinsteinheiten oder DECIMAL, niemals als FLOAT oder DOUBLE.
  • Setze Indexe für die Abfragen, die du tatsächlich ausführst, und bestätige diese mit EXPLAIN.
  • Behalte den Standard-ONLY_FULL_GROUP_BY-Modus bei und schreibe korrekte GROUP BY-Queries.
  • Nutze INSERT ... ON DUPLICATE KEY UPDATE für atomare Upserts anstelle von Check-then-Act.
  • Halte Transaktionen kurz, aktualisiere Zeilen in einer stabilen Reihenfolge und führe bei einem Deadlock einen Retry aus.
  • Weise der Anwendung einen User mit minimalen Berechtigungen (Least-Privilege) zu und führe DDL über einen Migrations-Account aus.
  • Erstelle konsistente Backups mit --single-transaction, protokolliere die Binlog-Position und teste die Wiederherstellung.
  • Bevorzuge Keyset-Pagination gegenüber großen LIMIT ... OFFSET-Scans.

Häufige Fehler

  • Tabellen auf MyISAM belassen und dadurch Transaktionen sowie Row-Level Locking verlieren.
  • Das alte Drei-Byte-utf8 verwenden und sich anschließend wundern, warum Emojis nicht gespeichert werden.
  • Geldbeträge in FLOAT speichern und Rundungsfehler ansammeln.
  • UPDATE oder DELETE ohne WHERE ausführen und so die gesamte Tabelle neu schreiben.
  • Jede einzelne Spalte indexieren, anstatt nur die für die relevanten Queries, was die Schreibvorgänge verlangsamt.
  • Sich darauf verlassen, dass AUTO_INCREMENT lückenlos oder aussagekräftig ist.
  • Read-Modify-Write im Anwendungscode durchführen, anstatt einen atomaren Upsert zu nutzen.
  • Unmittelbar nach einem Schreibvorgang von einer Replica lesen und veraltete Daten sehen.
  • ONLY_FULL_GROUP_BY deaktivieren, um eine fehlerhafte Aggregation zu kaschieren.
  • Die Anwendung als root verbinden lassen.

Wie geht es weiter?

MySQL und seine Konzepte lassen sich hervorragend übertragen. Wenn Sie die andere große relationale Engine vergleichen möchten, lesen Sie den PostgreSQL-Guide, der dieselben Konzepte mit anderen Standardeinstellungen behandelt. Für die Sprache, die beiden zugrunde liegt, führt der SQL-Guide Joins, CTEs und Window-Functions von Grund auf ein. Und da die meisten MySQL-Deployments auf einen Cache setzen, sind Redis und das Dokumentenmodell in MongoDB natürliche Ergänzungen.

In der Praxis

Schema, Query, Plan, Upsert

Vier alltägliche MySQL-Aufgaben: eine InnoDB-Tabelle definieren, Joins nutzen, einen Plan prüfen und sicher upserten.

schema.sql
CREATE TABLE categories (
  id   INT UNSIGNED NOT NULL AUTO_INCREMENT,
  name VARCHAR(80) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uniq_categories_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE products (
  id           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  category_id  INT UNSIGNED NOT NULL,
  sku          VARCHAR(64) NOT NULL,
  name         VARCHAR(200) NOT NULL,
  price_cents  INT UNSIGNED NOT NULL,
  stock        INT NOT NULL DEFAULT 0,
  attributes   JSON NOT NULL,
  created_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY uniq_products_sku (sku),
  KEY idx_products_category (category_id),
  CONSTRAINT fk_products_category
    FOREIGN KEY (category_id) REFERENCES categories (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Ein Upsert schreiben

Die Datenbank kann Insert-oder-Update atomar handhaben. Ein vorheriges Prüfen und anschließendes Einfügen führt zu einer Race Condition zwischen den beiden Statements.

Bevorzugt
INSERT INTO products (sku, name, price_cents, stock)
VALUES ('SKU-1', 'Widget', 999, 5)
ON DUPLICATE KEY UPDATE
  stock = stock + VALUES(stock);
Vermeiden
SELECT id FROM products WHERE sku = 'SKU-1';
-- another session can insert the same
-- SKU between these two statements
INSERT INTO products (sku, name, price_cents, stock)
VALUES ('SKU-1', 'Widget', 999, 5);

Ein Charset wählen

utf8mb4 ist das einzige Charset, das jedes Unicode-Zeichen, einschließlich Emojis, speichert. Das ältere utf8 ist eine Falle.

Bevorzugt
CREATE TABLE notes (
  id   BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  body TEXT NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci;
Vermeiden
CREATE TABLE notes (
  id   BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  body TEXT NOT NULL,
  PRIMARY KEY (id)
) DEFAULT CHARSET=utf8;
-- utf8 here is a 3-byte subset: emoji
-- are silently rejected or mangled

Abwägungen

Ist MySQL die richtige relationale Datenbank für dich?

MySQL ist praxiserprobt und universell unterstützt. Die Kompromisse liegen hauptsächlich bei den Standardwerten und dem Dialekt.

Strengths

  • Bewährt im Web-Maßstab

    Es betreibt seit Jahrzehnten leseintensive Web-Anwendungen, und das Operational Playbook für Replikation und Failover ist ausgereift.

  • Überall präsent

    Frameworks, ORMs, Hoster und DBAs kennen MySQL, sodass das Onboarding und Recruiting einfach sind und Managed-Optionen im Überfluss vorhanden sind.

  • Einfache Lese-Skalierung

    Asynchrone Replikation macht es unkompliziert, Replicas hinzuzufügen und Leseabfragen dorthin zu routen, ohne das Schema zu ändern.

Trade-offs

  • Standardwerte können tückisch sein

    Ältere Server nutzen oft noch schwächere Einstellungen und Charsets, und Repeatable Read verhält sich anders als in Postgres; prüfe daher die Konfiguration, statt ihr blind zu vertrauen.

  • Der Dialekt ist eigenwillig

    Upserts, JSON-Operatoren und Funktionen sind MySQL-spezifisch. Hier geschriebener SQL-Code lässt sich selten unverändert auf eine andere Engine übertragen.

  • Feature-Wildwuchs

    Stored Procedures, Trigger und Events sind mächtig, werden aber leicht überstrapaziert, wodurch Business-Logik in schwer testbaren Datenbank-Code verwandelt wird.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, MySQL zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch MySQL — mit Quizzen und echtem Code, den Sie im Browser ausführen können.