26 JSON und BSON: Von menschenlesbar zu maschinenoptimiert

MongoDB bezeichnet sich selbst als “Document Database” und Dokumente werden als JSON dargestellt – zumindest aus Entwickler-Sicht. Ein mongosh-Query returned Dokumente, die wie JSON aussehen. Die API akzeptiert JSON-ähnliche Strukturen. Aber intern speichert MongoDB kein JSON. Es nutzt BSON – Binary JSON, eine binäre Serialisierung, die fundamental anders ist als textbasiertes JSON.

Diese Dualität – JSON als Interface, BSON als Storage – ist zentral für MongoDB’s Design. Sie erlaubt die Developer Experience von JSON (leicht lesbar, universell verstanden, flexibel) mit der Performance und Typen-Reichhaltigkeit von BSON (kompakt, schnell zu parsen, mehr Datentypen). Das Verständnis beider Formate und ihrer Trade-offs ist essentiell, um MongoDB effektiv zu nutzen und Performance-Probleme zu vermeiden.

26.1 JSON: Die universelle Datensprache des Webs

JavaScript Object Notation – JSON – wurde Anfang der 2000er Jahre populär als lightweight Alternative zu XML. Die Syntax ist simpel und intuitiv, abgeleitet von JavaScript’s Objekt-Literalen. Ein JSON-Dokument ist entweder ein Objekt (Key-Value-Paare in geschweiften Klammern) oder ein Array (geordnete Liste in eckigen Klammern).

Ein typisches JSON-Objekt für einen User könnte so aussehen:

{
  "userId": 12345,
  "username": "alice_smith",
  "email": "alice@example.com",
  "isActive": true,
  "registeredAt": "2024-01-15T10:30:00Z",
  "roles": ["user", "moderator"],
  "profile": {
    "firstName": "Alice",
    "lastName": "Smith",
    "age": 28
  },
  "metadata": null
}

Die Struktur ist selbsterklärend. Keys sind Strings in doppelten Anführungszeichen, Values können verschiedene Typen sein. Das profile-Feld ist ein nested Object – JSON unterstützt beliebige Verschachtelung. Das roles-Array enthält Strings. Das metadata-Feld ist explizit null, was “kein Wert” signalisiert.

JSON’s Datentypen sind bewusst minimal gehalten. Es gibt nur sechs: String, Number, Boolean, Array, Object und null. Strings sind immer in doppelten Anführungszeichen. Numbers können Integer oder Floating-Point sein, aber JSON unterscheidet nicht – “42” und “42.0” sind beide Numbers. Booleans sind die Literals true und false (lowercase, ohne Anführungszeichen). Arrays und Objects können beliebige Werte enthalten, inklusive verschachtelte Arrays und Objects.

Diese Simplizität ist JSON’s Stärke und Schwäche. Stärke: Jede Programmiersprache kann JSON trivial parsen und generieren. Parser sind winzig und schnell. Schwäche: Wichtige Datentypen fehlen. Dates gibt es nicht – man muss Strings verwenden und hoffen, dass alle dieselbe Format-Konvention nutzen. Binary Data gibt es nicht – man muss Base64-Encoding verwenden, was 33% Overhead bedeutet. Präzise Dezimalzahlen für Finanzberechnungen gibt es nicht – man nutzt Strings oder akzeptiert Floating-Point-Ungenauigkeit.

JSON ist textbasiert, was human-readable bedeutet. Man kann ein JSON-Dokument in einem Text-Editor öffnen und verstehen, was drin steht. Aber textbasiert heißt auch: ineffizient für Maschinen. Jedes Zeichen ist ein Byte (oder mehrere bei Unicode). Die Number 123456789 braucht 9 Bytes als Text, könnte aber als 32-bit Integer in 4 Bytes gespeichert werden. Keys sind wiederholt in jedem Dokument – in einer Collection mit Millionen User-Dokumenten ist “username” Millionen mal gespeichert.

26.2 BSON: Binär, schnell und typenreich

BSON wurde von MongoDB’s ursprünglichen Entwicklern als Antwort auf JSON’s Limitationen designed. Es ist keine neue Sprache, sondern eine binäre Serialisierung von JSON-ähnlichen Dokumenten. BSON-Dokumente haben dieselbe logische Struktur wie JSON – Objects, Arrays, nested Structures – aber die physische Repräsentation ist fundamental anders.

Ein BSON-Dokument beginnt mit einem 4-Byte-Integer, der die Gesamtlänge des Dokuments angibt. Dies erlaubt schnelles Überspringen von Dokumenten – wenn man nur das dritte Dokument in einer Sequenz will, kann man die ersten zwei überspringen, indem man deren Länge liest und den Filepointer entsprechend vorwärts bewegt. JSON hat keine solche Metadaten – man muss parsen, um zu wissen, wo ein Dokument endet.

Jedes Feld in BSON besteht aus drei Teilen: einem Type-Byte, einem null-terminierten Key-String und einem Value. Das Type-Byte kodiert den Datentyp – String, Integer, Double, Boolean, Array, Object, und viele weitere. Dies erlaubt effiziente Speicherung: Ein Integer wird als 4 oder 8 Bytes gespeichert, nicht als Text. Ein Boolean ist ein einzelnes Byte (0 oder 1), nicht die Strings “true” oder “false”.

BSON’s zusätzliche Datentypen sind der Hauptvorteil über JSON. Der wichtigste ist ObjectId – ein 12-Byte-Identifier, der in MongoDB als Default für _id-Felder verwendet wird. Ein ObjectId ist nicht nur einzigartig, sondern embedded auch einen Timestamp (die ersten 4 Bytes sind Sekunden seit Unix-Epoch). Dies erlaubt sortieren nach Creation-Time ohne separates createdAt-Feld.

// ObjectId-Struktur (12 Bytes total)
// [4 Bytes Timestamp][5 Bytes Random][3 Bytes Counter]
ObjectId("507f1f77bcf86cd799439011")

Der Date-Typ ist ein 64-bit Integer, der Millisekunden seit Unix-Epoch repräsentiert. Dies ist präzise, sortierbar und international – keine Timezone-Konfusion, keine String-Parsing-Fehler. MongoDB’s ISODate("2024-01-15T10:30:00Z") ist intern ein BSON Date.

Der Decimal128-Typ ist eine 128-bit Dezimalzahl mit exakter Präzision. Für Finanzdaten, wo 0.1 + 0.2 = 0.30000000000000004 (Floating-Point-Ungenauigkeit) inakzeptabel ist, ist Decimal128 essentiell. Es speichert Werte wie 19.99 exakt, ohne Rundungsfehler.

BinData erlaubt effizienten Storage von binären Daten – Images, PDFs, verschlüsselte Blobs. Statt Base64-Encoding (33% Overhead) speichert man die rohen Bytes direkt. MongoDB unterstützt verschiedene BinData-Subtypes – Generic, Function, UUID, MD5, Encrypted, etc.

RegEx speichert reguläre Ausdrücke nativ. Queries können direkt gegen RegEx-Patterns matchen, ohne String-Serialisierung:

db.users.insertOne({
  username: "alice",
  emailPattern: /^[a-z]+@example\.com$/i
})

Die vollständige BSON-Type-Liste umfasst über 20 Typen, von MinKey und MaxKey (spezielle Boundary-Werte für Sortierung) über Timestamp (intern für Replication) bis JavaScript und JavaScript with Scope (Code-Speicherung, selten genutzt). Diese Typen-Reichhaltigkeit erlaubt präzise Datenmodellierung ohne Workarounds.

26.3 Performance-Charakteristiken: Warum BSON schneller ist

Die binäre Natur von BSON bringt messbare Performance-Vorteile. Das Parsen ist schneller, weil keine String-zu-Number-Konversionen nötig sind. Ein BSON-Integer ist bereits als 32-bit oder 64-bit Integer kodiert – direktes Memory-Mapping ist möglich. JSON-Numbers müssen geparst werden, Character für Character, mit Fehlerbehandlung für invalide Syntax.

Die Traversierung ist schneller wegen der Length-Prefixes. BSON-Arrays und Objects beginnen mit ihrer Gesamtlänge. Um ein nested Object zu überspringen, liest man die Länge und springt vorwärts. JSON erfordert vollständiges Parsen aller nested Structures, um zu wissen, wo sie enden.

Ein konkretes Benchmark-Szenario: Eine Collection mit 1 Million User-Dokumenten. Jedes Dokument hat 20 Felder, inklusive nested Profile-Object und Arrays. Als JSON würde dies etwa 200 MB benötigen (angenommen durchschnittlich 200 Bytes pro Dokument). Als BSON sind es etwa 150 MB – 25% kompakter. Das Laden und Parsen ist 2-3x schneller bei BSON.

Der Overhead von BSON ist die Metadaten. Jedes Feld hat ein Type-Byte und null-Terminierung des Keys. Für kleine Dokumente mit wenigen Feldern kann BSON tatsächlich größer sein als JSON. Ein Dokument {"a":1} ist 7 Bytes in JSON, aber etwa 12 Bytes in BSON (Document-Length + Type + Key + Null-Terminator + Value + Document-End-Marker). Bei größeren, komplexen Dokumenten amortisiert sich der Overhead und BSON ist kompakter.

MongoDB nutzt BSON intern für Storage und Netzwerk-Protocol. Wenn eine App ein Dokument inserted, sendet der Treiber es als BSON über das Netzwerk. mongod empfängt BSON, speichert es fast direkt (mit minimalen Transformationen). Reads funktionieren umgekehrt: mongod liest BSON von Disk, sendet es über Netzwerk, der Treiber deserialisiert zu nativen Language-Objekten (etwa Python Dicts oder JavaScript Objects).

Dieser gesamte Pfad vermeidet JSON-Parsing. Die einzige Stelle, wo JSON involviert ist: Developer-Tools wie mongosh. mongosh zeigt Dokumente als JSON-ähnlich für Lesbarkeit, aber intern kommuniziert es in BSON. Dies ist reine Convenience für Menschen.

26.4 Entwickler-Erfahrung: Das Beste aus beiden Welten

MongoDB’s Developer Experience basiert auf JSON, weil JSON universell ist. APIs in allen Sprachen akzeptieren JSON-ähnliche Strukturen. JavaScript-Entwickler nutzen Objekt-Literale direkt. Python-Entwickler nutzen Dicts. Die Cognitive Load ist minimal – jeder, der mit modernen Programmiersprachen arbeitet, versteht JSON intuitiv.

# Python mit PyMongo
user = {
    "username": "alice",
    "email": "alice@example.com",
    "age": 28,
    "registered": datetime.now()
}
collection.insert_one(user)

PyMongo konvertiert dieses Python-Dict automatisch zu BSON für die Netzwerk-Übertragung. Das datetime-Objekt wird zu BSON Date. Der Developer sieht nie BSON-Bytes. Die Abstraktion ist perfekt.

Aber manchmal ist Awareness von BSON nützlich. Performance-Debugging etwa: Warum ist eine Query langsam? Möglicherweise weil die Dokumente riesig sind und viel Netzwerk-Transfer benötigen. db.collection.stats() zeigt avgObjSize in Bytes – die durchschnittliche BSON-Größe pro Dokument. Wenn dies mehrere Megabytes ist, ist das ein Problem.

Oder Type-Confusion: Ein Feld, das als String gespeichert wurde, obwohl es numerisch sein sollte. Queries mit $gt oder $lt funktionieren nicht korrekt, weil String-Vergleich statt numerischer Vergleich passiert:

// Falsch: age als String gespeichert
db.users.insertOne({ age: "28" })
db.users.find({ age: { $gt: 25 } })  // Findet nichts!

// Korrekt: age als Number
db.users.insertOne({ age: 28 })
db.users.find({ age: { $gt: 25 } })  // Findet das Dokument

MongoDB speichert Typen explizit in BSON. “28” (String) und 28 (Number) sind unterschiedliche Typen und matchen nicht in Queries. Dies ist ein häufiger Fehler bei Migrationen aus schemaless-Systemen oder wenn Daten aus externen Quellen importiert werden.

Die $type-Operator in Queries kann Typ-Inkonsistenzen finden:

// Finde alle Dokumente, wo age ein String ist (sollte Number sein)
db.users.find({ age: { $type: "string" } })

Type-Codes sind dokumentiert: 1 für Double, 2 für String, 16 für Int32, 18 für Int64, etc. In neueren MongoDB-Versionen kann man auch Type-Namen verwenden: $type: "double" statt $type: 1.

26.5 Die 16-MB-Grenze: BSON’s fundamentales Limit

BSON-Dokumente haben ein hartes Limit: 16 MB maximal. Dies ist keine willkürliche Einschränkung, sondern ein Design-Entscheidung. Dokumente sollten “reasonable size” sein. Ein 16-MB-Dokument kann etwa 200.000 Felder haben oder mehrere tausend nested Objects – mehr als jede vernünftige Anwendung braucht.

Das Limit existiert aus mehreren Gründen. Technisch: MongoDB lädt komplette Dokumente in RAM für Operationen. 16 MB ist groß genug für komplexe Daten, aber klein genug, dass viele Dokumente gleichzeitig im RAM sein können. Theoretisch unbegrenzte Dokumente würden RAM-Management komplizieren.

Praktisch: Riesige Dokumente sind ein Design-Smell. Sie deuten auf falsche Daten-Modellierung hin. Ein User-Dokument mit einem Array von 100.000 Orders ist schlecht designed. Orders sollten eine separate Collection sein, referenziert durch ID. Embedding ist für kleine, häufig zusammen abgefragte Daten gedacht, nicht für unbegrenzte Listen.

Wenn man wirklich größere Daten speichern muss – etwa PDFs oder Videos – nutzt man GridFS. GridFS ist ein Spec, das große Files in 255-KB-Chunks aufteilt und in zwei Collections speichert: fs.files (Metadaten) und fs.chunks (Daten). MongoDB-Treiber abstrahieren dies und erlauben Streaming-Uploads/-Downloads.

Das 16-MB-Limit gilt nur für einzelne Dokumente. Collections sind praktisch unbegrenzt. Eine Collection mit Milliarden Dokumenten ist kein Problem, solange jedes Dokument unter 16 MB ist.

26.6 JSON und BSON in der Praxis: Wann was nutzen

Für API-Responses an Web-Clients ist JSON die Wahl. Browsers parsen JSON nativ mit JSON.parse(). Die Lesbarkeit erlaubt einfaches Debugging in DevTools. Die Universalität garantiert Kompatibilität mit jedem Frontend-Framework.

Für MongoDB-Storage und interne Kommunikation ist BSON automatisch – man wählt nicht, es passiert einfach. Die Treiber handhaben die Konversion transparent.

Für Daten-Exports gibt es Trade-offs. mongodump erzeugt BSON-Files, die kompakt und schnell zu restoren sind. Aber sie sind binär und nicht human-readable. Für Audits oder manuelle Inspektion ist mongoexport mit JSON-Output besser. Es ist größer und langsamer, aber man kann es in Text-Editoren öffnen oder mit jq prozessieren.

Für Backup-Strategien ist BSON überlegen. Ein mongodump eines 100-GB-Datasets erzeugt etwa 100 GB BSON-Files. Ein mongoexport zu JSON erzeugt etwa 130-150 GB. Die Restore-Zeit ist ebenfalls dramatisch unterschiedlich – BSON ist 3-5x schneller.

Die folgende Tabelle fasst die Charakteristiken zusammen:

Aspekt JSON BSON
Format Textbasiert Binär
Lesbarkeit Human-readable Maschinenlesbar
Größe Größer (Text-Overhead) Kompakter (binär)
Parse-Speed Langsamer Schneller
Datentypen 6 Basis-Typen 20+ Typen
Date/Time String-Workaround Native Date
Binary Data Base64 (+33% Overhead) Native BinData
Präzise Decimals Keine Decimal128
Max Document Size Unbegrenzt (theoretisch) 16 MB
Use-Case API, Config, Data Exchange Database Storage, Performance

JSON und BSON sind komplementär, nicht konkurrierend. JSON ist das universelle Interface – menschenfreundlich, sprachunabhängig, einfach. BSON ist die maschinenoptimierte Implementierung – schnell, typenreich, effizient. MongoDB’s Genius war, beide zu kombinieren: Die Developer Experience von JSON mit der Performance von BSON. Entwickler denken in JSON, MongoDB arbeitet in BSON. Diese Abstraktion ist so nahtlos, dass viele MongoDB-Nutzer BSON’s Existenz kaum bewusst wahrnehmen – bis Performance-Probleme oder Type-Confusion sie zwingt, tiefer zu graben. Dann ist das Verständnis beider Formate essentiell.