Zum Inhalt springen

4. NoSQL und alternative Datenmodelle

Zu Zen-Modus wechseln

Bisher stand fast immer das relationale Modell im Mittelpunkt: Daten in Tabellen, verbunden über Schlüssel, abgefragt mit SQL. Es ist mächtig und für die meisten Aufgaben die richtige Wahl. Aber es ist nicht die einzige Möglichkeit, Daten zu strukturieren. Für bestimmte Anforderungen passen andere Modelle besser. Sie werden unter dem Sammelbegriff NoSQL zusammengefasst.

Ein Datenmodell legt fest, wie Daten strukturiert, gespeichert und abgefragt werden. Die wichtigsten im Überblick:

ModellStrukturStärkeBeispiel
RelationalTabellen mit Zeilen und SpaltenKonsistenz, komplexe Abfragen, TransaktionenPostgreSQL, MariaDB
Dokumentverschachtelte Dokumente (JSON)flexibles Schema, ganze Objekte am StückMongoDB
Key-ValueSchlüssel → Wertextrem schnell, einfachRedis
GraphKnoten und Kantenvernetzte Daten, BeziehungenNeo4j

Die verschiedenen NoSQL-Typen wurden bereits im Kapitel DBMS Markt der 4. Klasse vorgestellt. Hier vertiefen wir, wann sich der Wechsel weg vom relationalen Modell lohnt, und arbeiten mit einem dokumentenorientierten System.

NoSQL heißt nicht „kein SQL”, sondern „Not only SQL”. Die Modelle sind als Ergänzung gedacht, für Fälle, in denen das relationale Modell an Grenzen stößt:

  • Flexibles Schema: Wenn sich die Datenstruktur häufig ändert oder von Datensatz zu Datensatz unterschiedlich ist, wird das starre Tabellenschema hinderlich.
  • Verschachtelte Daten: Wenn ein Objekt (etwa eine Bestellung mit ihren Positionen) fast immer am Stück gelesen wird, spart ein Dokument die vielen Joins.
  • Horizontale Skalierung: Viele NoSQL-Systeme sind von Grund auf darauf ausgelegt, Daten über viele Server zu verteilen.

Der Preis dafür ist meist, dass strenge Konsistenz und komplexe Abfragen über viele Beziehungen schwieriger werden. Viele NoSQL-Systeme setzen statt sofortiger Konsistenz auf eventual consistency: Änderungen sind erst nach kurzer Zeit überall sichtbar.

MongoDB ist die bekannteste dokumentenorientierte Datenbank. Statt Zeilen in Tabellen speichert sie Dokumente in Collections. Als durchgehendes Beispiel dient eine kleine Video-Plattform: Eine Collection videos hält je Video ein Dokument, das die ganze Entität samt verschachtelter Struktur enthält.

{
_id: ObjectId("665f1a2c9d3e4b0012a7f001"),
title: "Color grading in DaVinci Resolve",
channel: "Media Technology HTL",
duration_sec: 742,
tags: ["tutorial", "video editing", "color grading"],
resolutions: [
{ label: "1080p", bitrate_kbps: 4500 },
{ label: "720p", bitrate_kbps: 2500 }
],
published_at: ISODate("2026-02-14T09:00:00Z"),
visible: true
}

Jedes Dokument hat einen eindeutigen Schlüssel _id, standardmäßig eine ObjectId. Intern speichert MongoDB nicht reines JSON, sondern BSON (Binary JSON) — eine binäre Form, die zusätzliche Typen wie ISODate, ObjectId oder Ganzzahlen kennt und schneller zu verarbeiten ist.

Anders als bei einer Tabelle müssen nicht alle Dokumente einer Collection dieselben Felder haben. Ein neues Video darf ein zusätzliches Feld subtitle_langs mitbringen, ohne dass die anderen Dokumente geändert werden. Das ist der große Vorteil, wenn sich die Struktur oft ändert oder von Fall zu Fall unterscheidet.

Der Preis dafür: Die Datenbank prüft von sich aus nichts. Ein Tippfehler wie titl statt title fällt nicht auf, ein duration_sec als Text "742" statt als Zahl ebenso wenig. Die Konsistenz, die im relationalen Modell das Schema erzwingt, muss hier die Anwendung sicherstellen — genau dafür kommt später Mongoose ins Spiel.

Die zentrale Modellierungsfrage lautet: Zusammengehörige Daten einbetten oder referenzieren?

  • Eingebettet (embedded): Die verwandten Daten stecken im selben Dokument (wie resolutions oben). Ein Lesevorgang holt alles, kein Join. Passt, wenn die Daten fast immer gemeinsam gebraucht werden, überschaubar bleiben und zum Elterndokument gehören.
  • Referenziert (referenced): Das Dokument speichert nur die _id eines anderen Dokuments, ähnlich einem Fremdschlüssel. Passt, wenn die Daten groß sind, unbegrenzt wachsen, sich unabhängig ändern oder von vielen geteilt werden.

Am Video-Beispiel: Die Auflösungen und Tags werden eingebettet — sie gehören fest zum Video und bleiben klein. Die Kommentare dagegen wachsen potenziell ins Tausendfache und werden meist getrennt geladen. Sie kommen deshalb in eine eigene Collection und verweisen per video_id zurück:

// Collection: comments
{
_id: ObjectId("..."),
video_id: ObjectId("665f1a2c9d3e4b0012a7f001"), // reference to the video
author: "lisa_k",
text: "The section on LUTs helped me a lot.",
created_at: ISODate("2026-02-15T18:20:00Z")
}

MongoDB wird nicht mit SQL, sondern über eine eigene API abgefragt (in der Shell mongosh oder im Node.js-Treiber). Die vier Grundoperationen am Beispiel der videos:

// Create – insert one or more documents
db.videos.insertOne({ title: "Greenscreen Basics", channel: "Media Technology HTL", visible: false });
// Read – filter and return only selected fields (projection)
db.videos.find(
{ visible: true, duration_sec: { $lt: 600 } }, // condition
{ title: 1, duration_sec: 1, _id: 0 } // only these fields
);
// Update – set fields, append array elements
db.videos.updateOne(
{ title: "Greenscreen Basics" },
{ $set: { visible: true }, $push: { tags: "compositing" } }
);
// Delete
db.videos.deleteOne({ title: "Greenscreen Basics" });

Ein Filter wie { visible: true } entspricht einem WHERE visible = true. Für Vergleiche gibt es Operatoren wie $gt (größer), $lt (kleiner), $in (in einer Liste) oder $exists (Feld vorhanden). Mit einer Projektion (zweites Argument von find) wird gesteuert, welche Felder zurückkommen — das spart Datenübertragung.

Ohne Index durchsucht MongoDB bei jeder Abfrage die ganze Collection (ein Collection Scan). Ein Index auf ein häufig gefiltertes Feld beschleunigt das drastisch, genau wie im relationalen Modell:

db.videos.createIndex({ channel: 1 }); // ascending by channel
db.comments.createIndex({ video_id: 1, created_at: -1 }); // compound index

Der zweite Index hilft der typischen Abfrage „alle Kommentare eines Videos, neueste zuerst”. Indizes kosten Speicher und verlangsamen Schreibvorgänge leicht — man legt sie gezielt für die tatsächlichen Abfragen an.

Für Auswertungen, die in SQL GROUP BY bräuchten, gibt es die Aggregation Pipeline: Die Dokumente durchlaufen mehrere Stufen, jede formt sie weiter. Das folgende Beispiel ermittelt je Kanal die Gesamt-Wiedergabedauer der sichtbaren Videos, absteigend:

db.videos.aggregate([
{ $match: { visible: true } }, // filter (like WHERE)
{ $group: { _id: "$channel", // group (like GROUP BY)
count: { $sum: 1 },
total_duration: { $sum: "$duration_sec" } } },
{ $sort: { total_duration: -1 } }, // sort
{ $limit: 5 }
]);

Über $lookup lässt sich sogar ein Join nachbilden — etwa jedes Video mit seinen Kommentaren verbinden. Das ist möglich, aber aufwendiger als in SQL; wenn viele solcher Verknüpfungen nötig sind, ist das oft ein Zeichen, dass ein relationales Modell besser passen würde.

db.videos.aggregate([
{ $lookup: {
from: "comments",
localField: "_id",
foreignField: "video_id",
as: "comments" } },
{ $addFields: { comment_count: { $size: "$comments" } } }
]);

Mongoose: ein Schema für die schemalose Datenbank

Abschnitt betitelt „Mongoose: ein Schema für die schemalose Datenbank“

Der native Treiber spricht MongoDB direkt an — ohne jede Prüfung. In echten Node.js-Projekten wird MongoDB deshalb fast immer über Mongoose angesprochen, ein sogenanntes ODM (Object Document Mapper). Mongoose legt dieselbe Rolle wie ein ORM im relationalen Umfeld an: Es bildet Dokumente auf JavaScript-Objekte ab und legt eine Schema- und Validierungsschicht über die Collection. Damit kehrt genau die Struktur zurück, die die Datenbank selbst nicht erzwingt.

Ein Schema beschreibt Felder, Typen, Pflichtangaben und Standardwerte. Aus dem Schema entsteht ein Model — das Werkzeug, mit dem die Anwendung Dokumente liest und schreibt.

import mongoose from 'mongoose';
const { Schema, model } = mongoose;
await mongoose.connect('mongodb://localhost:27017/media_library');
const videoSchema = new Schema({
title: { type: String, required: true, trim: true },
channel: { type: Schema.Types.ObjectId, ref: 'Channel', required: true },
duration_sec: { type: Number, min: 0 },
status: { type: String, enum: ['draft', 'scheduled', 'public'], default: 'draft' },
tags: [String],
resolutions: [{ label: String, bitrate_kbps: Number }],
published_at: { type: Date, default: Date.now }
});
const Video = model('Video', videoSchema);

Schon hier steckt viel Prüfung drin: required erzwingt Pflichtfelder, min verbietet negative Dauern, enum lässt für status nur drei erlaubte Werte zu, default füllt fehlende Felder. Ein Speicherversuch mit ungültigen Daten scheitert jetzt mit einer klaren Fehlermeldung, statt kaputte Dokumente in der Datenbank abzulegen.

Dieselben vier Operationen wie oben, aber über das Model — und mit async/await, weil jeder Datenbankzugriff asynchron ist:

// Create
const v = await Video.create({
title: 'Astro in 10 minutes',
channel: channelId,
duration_sec: 612,
tags: ['tutorial', 'web']
});
// Read
const latest = await Video.find({ status: 'public' })
.sort({ published_at: -1 })
.limit(10);
// Update
await Video.updateOne({ _id: v._id }, { status: 'public' });
// Delete
await Video.deleteOne({ _id: v._id });

Der Code liest sich mit Mongoose kompakter, vor allem aber prüft er beim Schreiben automatisch gegen das Schema.

Das Feld channel speichert nur die ObjectId des Kanals — eine Referenz, wie oben beschrieben. Mit populate löst Mongoose diese Referenz beim Lesen auf und setzt das ganze Kanal-Dokument ein. Das ist Mongooses Antwort auf den Join:

const videos = await Video.find({ status: 'public' }).populate('channel');
console.log(videos[0].channel.name); // with populate the whole channel object,
// without populate only the ObjectId

Über die einfachen Regeln hinaus kann ein Schema eigene Prüfungen und automatische Abläufe enthalten. Eine Middleware (ein Hook) läuft etwa vor jedem Speichern — hier, um vor dem Veröffentlichen einen Zeitstempel zu setzen:

videoSchema.pre('save', function (next) {
if (this.status === 'public' && !this.published_at) {
this.published_at = new Date();
}
next();
});

So wandert Logik, die die Datenqualität sichert, an eine zentrale Stelle statt sich über die ganze Anwendung zu verteilen. Mongoose bringt damit einen Teil dessen zurück, was das relationale Schema von Haus aus bietet — Typen, Pflichtfelder, erlaubte Werte, Regeln — und lässt zugleich die Flexibilität von MongoDB bestehen.

  • Relational, wenn die Daten stark vernetzt sind, komplexe Abfragen über viele Beziehungen und strenge Transaktionen gebraucht werden und ein festes Schema die Datenqualität sichert.
  • NoSQL, wenn die Daten von Natur aus als Dokumente vorliegen, das Schema flexibel sein soll, oder wenn horizontale Skalierung im Vordergrund steht.

Die Wahl folgt der Aufgabe, nicht der Mode.

  • Ein Datenmodell bestimmt, wie Daten strukturiert und abgefragt werden; neben dem relationalen gibt es Dokument-, Key-Value- und Graphmodelle.
  • NoSQL ergänzt das relationale Modell für flexible Schemata, verschachtelte Daten und horizontale Skalierung, oft um den Preis strenger Konsistenz.
  • MongoDB speichert Dokumente (als BSON) in Collections; das Schema ist flexibel, wird aber von der Datenbank nicht geprüft.
  • Die Kernfrage der Modellierung ist eingebettet vs. referenziert; Indizes beschleunigen Abfragen, die Aggregation Pipeline ersetzt GROUP BY.
  • Mongoose ist ein ODM: Es legt mit Schema und Model eine Validierungsschicht über MongoDB, bildet Beziehungen mit ref/populate ab und bringt so Struktur in die schemalose Datenbank zurück.
  • Relational und NoSQL ergänzen sich; die Wahl richtet sich nach der Aufgabe.