Zum Inhalt

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 und systemd installieren und absichern.

  • Mengenbasiertes SQL & JOINs


    Deklaratives DDL, DML, idempotente UPSERT-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 automatisierte pg_dump-Backups.

    Zum Intermediate-Leitfaden

  • Expertenwissen (Advanced Engineering)


    Tieftauchen in B-Tree/GIN/BRIN-Internals, MVCC-Vacuum-Tuning, Streaming-Replikation und PgBouncer-Pooling.

    Zum Advanced-Leitfaden

  • Python Backend & FastAPI


    PostgreSQL- und MariaDB-Schemas mit asynchronen Python-Services via SQLAlchemy 2.0 und asyncpg verbinden.

    Zum FastAPI-Guide