MySQL
database-schema-dev und backend-dev besitzen eine MySQL-spezifische Expertise-Ebene:
diese Agenten sind engine-agnostisch und passen sich dem jeweiligen Migrationstool/ORM an,
das ein Projekt bereits verwendet — dieses Skill stellt das eigentliche MySQL-Wissen bereit, auf das sie zurückgreifen, wenn die Engine MySQL oder der eng kompatible Fork MariaDB ist.
Wo MariaDB sich anders verhält, wird dies explizit erwähnt — gehe nicht von vollständiger Parität bei fortgeschrittenen Funktionen aus.
Schritt 1 — Überprüfen, ob die Engine tatsächlich MySQL/MariaDB ist
Überprüfe package.json auf mysql2/mysql, einen ORM-Dialekt/Provider
(provider = "mysql" in Prisma, dialect: 'mysql' in Sequelize,
type: 'mysql'/'mariadb' in TypeORM), einen docker-compose.yml-Dienst,
der ein mysql/mariadb-Image ausführt, oder einen mysql://-Verbindungsstring. Falls nichts davon zutrifft, stoppe — verwende stattdessen die engine-agnostischen Standardeinstellungen von database-schema-dev/backend-dev oder das passende engine-spezifische Skill.
Schritt 2 — Zeichensatz, Kollation, Storage Engine
- Standardzeichensatz ist
utf8mb4, nichtutf8— MySQLsutf8ist ein auf 3 Byte begrenztes Alias, das die meisten Emoji/Astralzeichen nicht speichern kann — mit einer expliziten Kollation (utf8mb4_0900_ai_ciauf MySQL 8+; MariaDB hat keine0900-Kollationen, verwendeutf8mb4_unicode_cifür alles, das auf beide abzielt). InnoDBaußer es gibt einen spezifischen Grund fürMyISAM(keine Transaktionen, keine Zeilensperren, keine FKs) — es ist seit MySQL 5.5/MariaDB 10.x die Standardeinstellung, daher bestätigt dies normalerweise nur, dass nichts es überschreibt._ci-Kollation bedeutet, dass'[email protected]'mit'[email protected]'übereinstimmt — verwende_bin/_cs(oder eine gehashte Spalte) für alles, das case-sensitiv verglichen werden muss (Tokens, Geheimnisse).
Schritt 3 — Korrekte Datentypen
- Geld/exakte Dezimalstellen:
DECIMAL(p,s), niemalsFLOAT/DOUBLE— Binärfließkomma kann die meisten Basis-10-Brüche nicht exakt darstellen. - Ganzzahlen richtig dimensionieren (
TINYINT/SMALLINT/INT/BIGINT) nach tatsächlichem Bereich, nicht aus Gewohnheit überallBIGINT— kleinere Typen bedeuten kleinere Indizes.UNSIGNEDist freie Reserve für eine ID-Spalte, die nie negativ ist. DATETIMEfür einen Wanduhrzeitwert ohne Zeitzonenkonvertierung;TIMESTAMP(UTC-normalisiert, automatische Konvertierung pro Sitzungszeitzone, 2038-Bereichsbegrenzung) nur für einen tatsächlichen Zeitpunkt — verwende es nicht standardmäßig.VARCHAR(n)auf die tatsächliche Einschränkung dimensioniert, nicht reflexartigVARCHAR(255);TEXT/BLOBfür wirklich unbegrenzte Inhalte — keines akzeptiert einen Standardwert oder einen vollständigen (nicht-präfix) Index.ENUMnur für eine feste, selten ändernde Menge — Werte sortieren nach Definitionsreihenfolge, nicht alphabetisch (eine Überraschung beiORDER BY), und das Anhängen eines Werts ist auf MySQL 8 nur Metadaten, kann aber auf MariaDB/älterem MySQL die Tabelle umschreiben. Eine Nachschlagetabelle skaliert besser, sobald die Menge wächst oder Metadaten benötigt.- Natives
JSON(MySQL 5.7.8+/MariaDB 10.2+ — MariaDBsJSONist eigentlichLONGTEXTplus einCHECK, kein Binärtyp, daher gelten keine von MySQLs Binärspeicher-/Teilaktualisierungsvorteilen) nur für wirklich schemalose Daten — alles, das regelmäßig gefiltert oder mit Joins verknüpft wird, gehört in eine echte Spalte.
Schritt 4 — Index-Design
- Zusammengesetzte Indizes: Spalte(n) mit Gleichheitsfilter zuerst, die Bereichs-/Sortierspalte zuletzt —
(status, created_at)bedientWHERE status = ? ORDER BY created_at; die umgekehrte Reihenfolge nicht. - Links-Präfix-Regel: Ein Index auf
(a, b, c)bedient auch Lookups auf(a)und(a, b)— füge keinen redundanten(a)-Index daneben hinzu. - Ein abdeckender Index (jede ausgewählte Spalte enthalten) erlaubt MySQL, die Antwort nur aus dem Index zu beziehen (
Using indexinEXPLAIN) — lohnenswert für eine heiße, schmale Abfrage, nicht standardmäßig für jede Abfrage. - Präfix-Index (
col(20)) für langeVARCHAR/TEXT— überprüfeCOUNT(DISTINCT LEFT(col, n))gegenCOUNT(DISTINCT col), umnzu dimensionieren. - Tiefe Paginierung:
LIMIT 20 OFFSET 100000scannt und verwirft trotzdem 100.000 Zeilen pro Seite. Tasten-Paginierung (WHERE (created_at, id) < (:last_created, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20) verwendet denselben zusammengesetzten Index wieder und springt direkt zum Cursor — verwende dies für unendliches Scrollen oder tiefe API-Paginierung anstelle vonOFFSET. - Jeder Index kostet Schreibzeit und Speicherplatz — füge Indizes für reale Abfragemuster hinzu (
database-schema-devSchritt 1), nicht spekulativ pro Spalte.
Schritt 5 — EXPLAIN-gesteuerte Abfrageoptimierung
- Führe
EXPLAIN(oderEXPLAIN ANALYZE, MySQL 8.0.18+/MariaDB 10.4+) aus, bevor du einer Abfrage vertraust, dass sie schnell ist — beurteile nicht allein aus der Abfrageform. type: ALList ein vollständiger Tabellenscan;ref/range/constbedeuten, dass ein Index tatsächlich verwendet wird — eine optimierungswürdige Abfrage sollte bei trivialer Tabellengröße keinALLzeigen.Extra: Using filesort/Using temporarybedeutet, dass MySQL außerhalb eines Index sortiert/materialisiert hat — normalerweise behoben durch einen Index, der demORDER BY/GROUP BYentspricht.- Eine umschließende Funktion oder implizite Typumwandlung auf einer indizierten Spalte (
WHERE YEAR(created_at) = ..., Vergleich eines String-Spalte mit einer Zahl) macht den Index unwirksam — schreibe stattdessen als nackten Spaltenbereich um (created_at >= '2024-01-01' AND created_at < '2025-01-01'). SELECT *auf einer breiten Tabelle verhindert abdeckende Indizes und ziehtTEXT/BLOB-Spalten, die niemand angefordert hat — wähle nur aus, was verwendet wird.
Schritt 6 — Transaktionen und Sperren
- InnoDB sperrt Zeilen, nicht Tabellen, aber die Standardisolierung
REPEATABLE READnimmt auch Lückensperren bei Bereichsscans inUPDATE/DELETE/sperrendemSELECT— eine Bereichsbedingung kann mehr als die übereinstimmenden Zeilen blockieren.READ COMMITTEDvermeidet Lückensperren, wenn wiederholbare Lesevorgänge nicht benötigt werden. - Dieser Snapshot deckt nur ein einfaches
SELECTab — ein sperrender Lesevorgang in derselben Transaktion (SELECT ... FOR UPDATE,UPDATE,DELETE) liest stattdessen die neuesten committeten Daten, sodass er eine Zeile sehen kann, die der Snapshot nie gezeigt hat. Gehe nicht davon aus, dass Snapshot-Isolation auch einen sperrenden Lesevorgang schützt. - Halte Transaktionen kurz — eine offene Transaktion hält ihre Sperren und blockiert unter
REPEATABLE READauch InnoDBs Bereinigung alter Zeilenversionen. SELECT ... FOR UPDATEzum Sperren von Zeilen, die in einer Transaktion gelesen und dann geschrieben werden (Kontostandsprüfung vor Abbuchung); sperre in einer konsistenten Zeilenreihenfolge im gesamten Code, um Deadlocks zu vermeiden.- Ein Deadlock (
ERROR 1213, SQLSTATE40001) ist bei realer Konkurrenz zu erwarten — InnoDB rollt automatisch eine Seite zurück; fange diesen Code ab und wiederhole ihn, anstatt ihn mit einer längeren Transaktion zu "beheben".SHOW ENGINE INNODB STATUS→LATEST DETECTED DEADLOCKzeigt, was jede Seite gehalten/erwartet hat. - Autocommit ist standardmäßig pro Verbindung aktiv — atomare Mehrfachanweisungen benötigen eine explizite Transaktion (oder den ORM-Wrapper), nicht nur die Nachbarschaft im Code.
Schritt 7 — MySQL-spezifische Fallstricke und MariaDB-Abweichungen
sql_modesollteSTRICT_TRANS_TABLESenthalten (Standard seit MySQL 5.7/MariaDB 10.2) — ohne sie werden außerbereichs-/falsche Werte stillschweigend abgeschnitten statt einen Fehler zu verursachen; bestätige, dass es nicht gelockert wurde.AUTO_INCREMENT-Werte sind nicht lückenlos (ein Rollback verbraucht trotzdem einen) — verlasse dich niemals auf fortlaufende IDs.- Die Sitzungs-
time_zonebeeinflusst dieTIMESTAMP-Konvertierung, aber nichtDATETIME— ein Pool mit gemischten Zeitzonen pro Verbindung ist ein echter "falsche Stunde"-Fehler; setze sie explizit fest. - MariaDB hat eigene Erweiterungen ohne MySQL-Äquivalent (
CREATE SEQUENCE,RETURNINGbei Schreibvorgängen) — verwende sie nicht, wenn auch MySQL ein Ziel ist; Fensterfunktionen/CTEs benötigen MySQL 8.0+/MariaDB 10.2+, überprüfe zuerst die festgelegte Version. - MariaDBs Optimierer und
EXPLAIN-Ausgabe sind nicht dieselbe Engine wie die von MySQL — führeEXPLAINauf der tatsächlich verwendeten Engine erneut aus, anstatt einem Plan zu vertrauen, der auf der anderen abgestimmt wurde.
Schritt 8 — Selbstkontrolle vor Abschlussmeldung
Bestätige, dass jede neue/geänderte Spalte einen beabsichtigten Typ hat und der Zeichensatz auf utf8mb4 zurückfällt. Bestätige, dass jeder neue Index durch ein reales Abfragemuster mit EXPLAIN-Ausgabe untermauert wird, die seine Verwendung zeigt, und jeder tiefe/unbegrenzte Listenendpunkt Tasten-Paginierung anstelle von OFFSET verwendet. Bestätige, dass Mehrfachanweisungen-Schreibvorgänge in einer expliziten Transaktion eingeschlossen sind und das Isolationsverhalten eines sperrenden Lesevorgangs bewusst und nicht angenommen ist. Wenn die Compose-Datei/CI MariaDB festlegt, bestätige, dass nichts hinzugefügt wird, das eine reine MySQL-8-Funktion voraussetzt.
Was nicht zu tun ist
- Verwende nicht den
utf8-Zeichensatz oderFLOAT/DOUBLEfür Geld —utf8mb4undDECIMALsind die korrekten Standardeinstellungen. - Füge keinen Index ohne dahinterstehendes Abfragemuster hinzu, oder bezeichne eine Abfrage nicht als "schnell", ohne
EXPLAINzu überprüfen. - Verwende nicht die
OFFSET-Paginierung für eine Liste, die tief paginieren oder unbegrenzt laufen kann — verwende Tasten-Paginierung. - Lasse eine Transaktion nicht länger offen als die Arbeit, die sie schützt, insbesondere unter den Lückensperren von
REPEATABLE READ. - Verlasse dich nicht auf
AUTO_INCREMENT-Lückenlosigkeit oder implizite Typumwandlung für die Korrektheit. - Gehe nicht davon aus, dass ein MariaDB-Ziel reine MySQL-8-Funktionen besitzt (natives
JSON,utf8mb4_0900_ai_ci), ohne die festgelegte Version zu überprüfen. - Schreibe den engine-agnostischen Migrations-/Zugriffsschicht-Code nicht selbst — das ist Aufgabe von
database-schema-dev/backend-dev; dieses Skill ist das MySQL-Wissen, das sie darauf anwenden.