Datenbank-Grundlagen unter Linux: Relationale Modellierung, Normalisierung & SQL-Engineering
Erstellt: 06.11.2026 · Zuletzt aktualisiert: 06.11.2026 · Nejat Hakan
Management Summary: Jeder zustandsbehaftete Backend-Dienst, jedes ERP-System und jede Datenpipeline basiert auf einem relationalen Datenbankmanagementsystem (RDBMS). Bevor Kernel-Hugepages getunt oder Streaming-Replikationen konfiguriert werden, müssen Schemas entworfen werden, die referenzielle Integrität auf Speicherebene erzwingen, Update-Anomalien durch die Dritte Normalform (3NF/BCNF) eliminieren und optimale, maschinennahe Datentypen wählen. Dieser Leitfaden schafft ein solides Fundament für relationales Datenbank-Engineering auf Debian Linux mit PostgreSQL 17 und MariaDB 11.
-
Relationales Modell & ACID
Datenkorrektheit über parallele Verbindungen und Stromausfälle hinweg durch Atomarität, Konsistenz, Isolation und Dauerhaftigkeit garantieren.
-
Schema-Normalisierung (1NF–BCNF)
Redundante Datenverdopplungen und destruktive Update-/Delete-Anomalien durch formale funktionale Abhängigkeitsregeln eliminieren.
-
Debian-Engine-Bereitstellung
PostgreSQL 17 und MariaDB 11 auf Debian Linux mit
peer/scram-sha-256-Authentifizierung undsystemdinstallieren und absichern. -
Mengenbasiertes SQL & JOINs
Deklaratives
DDL,DML, idempotenteUPSERT-Muster und relationale Algebra-Verknüpfungen (INNER,LEFT,FULL,CROSS) mit strikten Constraints beherrschen.
1. Das relationale Paradigma & ACID-Garantien
Das 1970 von Edgar F. Codd bei IBM vorgestellte relationale Modell trennt die logische Darstellung der Daten (Relationen/Tabellen, Tupel/Zeilen und Attribute/Spalten) von deren physischer Byte-Speicherung auf dem Datenträger.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
APP["💻 Client-Anwendung<br/>(SQL-Abfrage über TCP/Unix-Socket)"]
PARSE["🔍 Parser & Rewriter<br/>(Syntax- & View-Prüfung)"]
OPT["📐 Kostenbasierter Planer<br/>(Wählt optimalen Pfad / Join-Algorithmus)"]
EXEC["⚙️ Ausführungs-Engine<br/>(ACID- & Lock-Manager)"]
DISK["💽 Speicher & WAL<br/>(8 KiB / 16 KiB Datenseiten)"]
APP --> PARSE
PARSE --> OPT
OPT --> EXEC
EXEC <--> DISK
Die vier Säulen von ACID-Transaktionen
| ACID-Eigenschaft | Technische Definition | Linux- / RDBMS-Mechanismus | Was ohne sie passiert |
|---|---|---|---|
| Atomarität ("Alles oder nichts") | Jede Transaktion mit mehreren Anweisungen (BEGIN ... COMMIT) wird entweder vollständig ausgeführt oder restlos zurückgerollt. |
Undo-Logs / MVCC-Tupelsichtbarkeit (xmin/xmax-Statusbits in pg_xact). |
Eine Banküberweisung bucht Konto A ab, stürzt mittendrin ab und schreibt Konto B niemals gut. |
| Konsistenz ("Invariantenwahrung") | Jede Transaktion überführt die Datenbank von einem gültigen Zustand in einen anderen und respektiert alle definierten Regeln. | PRIMARY KEY-, FOREIGN KEY-, UNIQUE-, NOT NULL- und CHECK-Constraints. |
Verwaiste Bestellpositionen verweisen auf gelöschte Kunden oder negative Lagerbestände. |
| Isolation ("Nebenläufigkeitsschutz") | Parallele Transaktionen laufen ab, ohne gegenseitig unfertige Zwischenzustände zu manipulieren oder zu lesen. | Multi-Version Concurrency Control (MVCC)-Snapshots & Zeilen-Sperren. | Zwei parallele Checkouts kaufen denselben letzten Artikel (Lost Update / Race Condition). |
| Dauerhaftigkeit ("Ausfallsicherheit") | Sobald das COMMIT dem Client Erfolg meldet, überstehen die Daten einen sofortigen Stromausfall. |
Write-Ahead Logging (WAL / Redo Log) wird via fsync() vor der Rückmeldung auf die NVMe geschrieben. |
Kürzlich bestätigte Bestellungen verschwinden nach einem Kernel-Panic aus dem RAM. |
2. Schema-Design & Normalisierung (1NF → 3NF / BCNF)
Schlecht entworfene Schemas speichern kommagetrennte Zeichenketten oder duplizieren Kundenprofile in jeder Rechnungszeile – dies führt unweigerlich zu Einfüge-, Änderungs- und Lösch-Anomalien. Die Normalisierung zerlegt flache Tabellen in saubere, orthogonale Relationen, die über Primärschlüssel (PK) und Fremdschlüssel (FK) verknüpft sind.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
CUST["👤 customers<br/>PK: customer_id<br/>email (UNIQUE)<br/>company_name"]
ORD["🧾 orders<br/>PK: order_id<br/>FK: customer_id<br/>status | ordered_at"]
ITEM["📦 order_items<br/>PK: (order_id, product_id)<br/>FK: order_id | product_id<br/>quantity | unit_cents"]
PROD["🏷️ products<br/>PK: product_id<br/>sku (UNIQUE)<br/>price_cents | stock"]
CUST -->|"1 : N"| ORD
ORD -->|"1 : N"| ITEM
PROD -->|"1 : N"| ITEM
Stufen der Normalisierung
| Normalform | Kernregel | Typischer Verstoß (Anti-Pattern) | Normalisierte Lösung |
|---|---|---|---|
| 1NF (Erste Normalform) | Jede Spalte enthält atomare (unteilbare) Werte; keine wiederkehrenden Spaltengruppen (tag1, tag2, tag3). |
Speichern von "linux,debian,server" als kommagetrennte VARCHAR-Zeichenkette. |
Eine Verknüpfungstabelle (article_tags) extrahieren oder ein stark indiziertes natives Array/JSONB nutzen. |
| 2NF (Zweite Normalform) | Erfüllt 1NF + jedes Nicht-Schlüssel-Attribut hängt vom gesamten zusammengesetzten Primärschlüssel ab (keine partiellen Abhängigkeiten). |
product_name in order_items (order_id, product_id) speichern, obwohl der Name nur von product_id abhängt. |
product_name in die Tabelle products verschieben; in order_items verbleiben nur quantity und unit_cents. |
| 3NF (Dritte Normalform) | Erfüllt 2NF + keine transitiven Abhängigkeiten (A → B → C, wobei Spalte C von Nicht-Schlüssel-Spalte B abhängt). |
department_id und department_manager_email direkt in der Tabelle employees ablegen. |
Eine eigene Tabelle departments (department_id, manager_email) anlegen und per Fremdschlüssel referenzieren. |
| BCNF (Boyce-Codd) | Jede Determinante (X → Y) ist ein Superschlüssel. |
Überlappende Schlüsselkandidaten in komplexen Buchungs- oder Terminierungs-Tabellen. | Die Zuordnung der Determinante in eine eigene Relation mit entsprechenden Einschränkungen auslagern. |
3. Installation & Härtung von PostgreSQL & MariaDB auf Debian Linux
Zur Bereitstellung nutzen wir entweder PostgreSQL (der Standard für strikte SQL-Konformität, komplexe analytische Abfragen und JSONB) oder MariaDB (der quelloffene Nachfolger von MySQL mit geclusterter InnoDB-Engine).
# Datei: /opt/db-ops/install_postgresql_debian.sh
#!/usr/bin/env bash
set -euo pipefail
# 1. PostgreSQL-Server, Client-Tools und Contrib-Erweiterungen (pg_stat_statements, uuid-ossp) installieren
sudo apt update && sudo apt install -y postgresql postgresql-client postgresql-contrib
# 2. Systemd-Dienst aktivieren und Unix-Socket-Verfügbarkeit prüfen
sudo systemctl enable --now postgresql
pg_isready
# 3. Dedizierte Rolle (mit SCRAM-SHA-256 Passwortverschlüsselung) und Datenbank erstellen
sudo -u postgres psql <<'EOF'
SET password_encryption = 'scram-sha-256';
CREATE ROLE app_engineer WITH LOGIN PASSWORD 'ChangeMe_StrongSecret_2026!';
CREATE DATABASE inventory_prod OWNER app_engineer ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8' TEMPLATE template0;
REVOKE ALL ON DATABASE inventory_prod FROM PUBLIC;
EOF
# Datei: /opt/db-ops/install_mariadb_debian.sh
#!/usr/bin/env bash
set -euo pipefail
# 1. MariaDB-Server- und Client-Pakete installieren
sudo apt update && sudo apt install -y mariadb-server mariadb-client
# 2. Systemd-Dienst aktivieren (bindet standardmäßig an 127.0.0.1:3306)
sudo systemctl enable --now mariadb
sudo systemctl status mariadb --no-pager
# 3. Root-Zugriff härten, Testbenutzer entfernen und UTF8MB4-Datenbank anlegen
sudo mariadb <<'EOF'
DELETE FROM mysql.user WHERE User='';
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%';
CREATE DATABASE inventory_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER 'app_engineer'@'127.0.0.1' IDENTIFIED BY 'ChangeMe_StrongSecret_2026!';
GRANT ALL PRIVILEGES ON inventory_prod.* TO 'app_engineer'@'127.0.0.1';
FLUSH PRIVILEGES;
EOF
4. Optimale Datentypen wählen (Geld niemals in FLOAT speichern!)
Die Auswahl präziser Datentypen spart Platten- und Arbeitsspeicher, beschleunigt die CPU-Cache-Lokalität und verhindert gravierende Rundungsfehler.
| Datenbereich | ❌ Anti-Pattern | ✅ Produktion PostgreSQL | ✅ Produktion MariaDB | Warum das entscheidend ist |
|---|---|---|---|---|
| Primärschlüssel | SERIAL (32-Bit-Int) oder zufällige VARCHAR(36) UUIDv4 |
BIGINT GENERATED ALWAYS AS IDENTITY oder UUID (UUIDv7) |
BIGINT UNSIGNED AUTO_INCREMENT |
32-Bit-Integer laufen bei 2,14 Milliarden Zeilen über; unstrukturierte UUIDv4 fragmentiert B-Tree-Blattseiten. |
| Währungen / Finanzen | FLOAT / REAL / DOUBLE PRECISION |
BIGINT (in Cents/Satoshis) oder NUMERIC(14, 2) |
BIGINT (in Cents) oder DECIMAL(14, 2) |
IEEE-754 Gleitkommazahlen können 0.10 + 0.20 nicht exakt abbilden (0.30000000000000004). |
| Zeitstempel | TIMESTAMP (ohne Zeitzone) oder VARCHAR |
TIMESTAMPTZ (TIMESTAMP WITH TIME ZONE, gespeichert als UTC) |
DATETIME(6) oder TIMESTAMP(6) (in UTC) |
Zeitstempel ohne Zeitzonenbezug führen bei Sommerzeit-Wechseln und Server-Clustern zu Fehlern. |
| Zeichenketten | CHAR(255) (füllt Leerzeichen auf) |
TEXT oder VARCHAR(n) (in PG speichertechnisch identisch) |
VARCHAR(n) |
CHAR(n) verschwendet ungenutzten Speicherplatz durch Padding. |
| IP-Adressen | VARCHAR(45) |
INET / CIDR (7 oder 19 Bytes, unterstützt Subnetz-Operationen <<) |
INET6 (nativer 16-Byte Binärtyp in MariaDB) |
Native IP-Typen validieren Syntax automatisch und ermöglichen performante Subnetz-Eingrenzungen. |
5. DDL, DML & relationale JOINs in der Praxis
Das folgende, idempotente SQL-Skript demonstriert strikte Bedingungen (CHECK, FOREIGN KEY), atomare UPSERT-Vorgänge (ON CONFLICT) und analytische JOIN-Abfragen:
-- Datei: /opt/db-ops/inventory_schema_workshop.sql
BEGIN;
CREATE TABLE customers (
customer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE CHECK (position('@' IN email) > 1),
company_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku VARCHAR(32) NOT NULL UNIQUE,
title TEXT NOT NULL,
price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
stock_qty INTEGER NOT NULL DEFAULT 0 CHECK (stock_qty >= 0)
);
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE RESTRICT,
status VARCHAR(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'SHIPPED', 'CANCELLED')),
ordered_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(product_id) ON DELETE RESTRICT,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_cents INTEGER NOT NULL CHECK (unit_cents >= 0),
PRIMARY KEY (order_id, product_id)
);
-- Idempotenter UPSERT: Produkt anlegen oder Bestand & Preis aktualisieren, falls SKU existiert
INSERT INTO products (sku, title, price_cents, stock_qty)
VALUES
('SRV-NVME-2TB', 'Enterprise U.2 NVMe 2TB SSD', 24900, 40),
('RPI5-8GB-KIT', 'Raspberry Pi 5 8GB Node Kit', 11900, 25)
ON CONFLICT (sku) DO UPDATE
SET price_cents = EXCLUDED.price_cents,
stock_qty = products.stock_qty + EXCLUDED.stock_qty;
COMMIT;
Visualisierung von relationalen JOINs
%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
subgraph JoinTypes["🔗 Relationale JOIN-Semantik"]
INNER["INNER JOIN<br/>Nur übereinstimmende Zeilen<br/>in beiden Tabellen (A ∩ B)"]
LEFT["LEFT OUTER JOIN<br/>Alle linken Zeilen (A) +<br/>Treffer rechts (oder NULL)"]
ANTI["LEFT ANTI-JOIN<br/>LEFT JOIN ... WHERE<br/>B.id IS NULL (A \\ B)"]
end
-- Datei: /opt/db-ops/analytical_joins.sql
-- 1. INNER JOIN + Aggregation: Umsatzstärkste Kunden mit bezahlten Bestellungen
SELECT
c.company_name,
c.email,
COUNT(DISTINCT o.order_id) AS total_orders,
ROUND(SUM(oi.quantity * oi.unit_cents) / 100.0, 2) AS total_eur
FROM customers c
INNER JOIN orders o ON o.customer_id = c.customer_id
INNER JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'PAID'
GROUP BY c.customer_id, c.company_name, c.email
HAVING SUM(oi.quantity * oi.unit_cents) > 10000
ORDER BY total_eur DESC;
-- 2. LEFT ANTI-JOIN: Produkte finden, die NOCH NIE bestellt wurden
SELECT p.sku, p.title, p.stock_qty
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.product_id
WHERE oi.product_id IS NULL;
6. Wichtige CLI-Administrationsbefehle im Überblick
| Administrationsaufgabe | PostgreSQL (psql) |
MariaDB (mariadb) |
|---|---|---|
| Lokal als Admin verbinden | sudo -u postgres psql |
sudo mariadb |
| Alle Datenbanken auflisten | \l+ |
SHOW DATABASES; |
| Mit Datenbank verbinden | \c inventory_prod |
USE inventory_prod; |
| Tabellen & Größen auflisten | \dt+ |
SHOW TABLE STATUS; |
| Tabellenschema inspizieren | \d+ products |
SHOW CREATE TABLE products\G |
| Benutzer & Rollen anzeigen | \du+ |
SELECT User, Host FROM mysql.user; |
| Vertikale Ausgabe umschalten | \x auto |
Anstelle von ; ein \G an die Abfrage anhängen |
Nächste Schritte
Vom relationalen Basiswissen zu komplexen Abfragen, Stored Procedures, Backups und Index-Internals:
-
Fortgeschrittene Datenbankpraxis (Intermediate)
Window Functions (
OVER), CTEs, Views, Trigger, RBAC-Sicherheitskonzepte und automatisiertepg_dump-Backups. -
Expertenwissen (Advanced Engineering)
Tieftauchen in B-Tree/GIN/BRIN-Internals, MVCC-Vacuum-Tuning, Streaming-Replikation und PgBouncer-Pooling.
-
Python Backend & FastAPI
PostgreSQL- und MariaDB-Schemas mit asynchronen Python-Services via SQLAlchemy 2.0 und
asyncpgverbinden.