Read-Operationen sind das Herzstück fast jeder MongoDB-Anwendung. Während Writes wichtig sind, sind Reads typischerweise 90-95% aller Datenbankoperationen. Eine Social-Media-App schreibt einmal einen Post, aber er wird hunderte oder tausende Male gelesen. Eine E-Commerce-Site updated Inventory selten, aber Produkt-Queries laufen ständig. Die Performance und Effizienz von Read-Operationen bestimmt oft die User Experience der gesamten Anwendung.
MongoDB’s Query-Sprache ist mächtig und flexibel. Von simplen Equality-Checks bis zu komplexen Multi-Stage-Aggregations, von Full-Text-Search bis zu Geospatial-Queries – die Möglichkeiten sind umfangreich. Aber mit Macht kommt Verantwortung: Schlecht geschriebene Queries können Performance katastrophal beeinträchtigen. Ein Full-Collection-Scan auf einer Million-Dokument-Collection kann Sekunden dauern und das System belasten. Die richtige Query mit dem richtigen Index dauert Millisekunden.
Dieses Kapitel behandelt Read-Operationen systematisch – von grundlegenden Finds über Query-Operatoren bis zu Performance-Optimierung. Der Fokus ist nicht nur auf “wie funktioniert es”, sondern “wie nutzt man es richtig in Production”.
Die find()-Methode ist der Einstiegspunkt für fast alle
Read-Operationen. Im simpelsten Fall gibt sie alle Dokumente einer
Collection zurück:
db.users.find()Dies ist äquivalent zu SELECT * FROM users in SQL. In
der Shell zeigt MongoDB die ersten 20 Dokumente und wartet auf weitere
Input (tippe “it” für “iterate” um die nächsten 20 zu sehen).
Programmatisch gibt find() einen Cursor zurück, der lazy
iteriert – Dokumente werden on-demand fetched, nicht alle auf einmal in
Memory geladen.
Mit Filter:
Die meisten Queries filtern nach spezifischen Kriterien:
db.users.find({ status: "active" })Dies findet alle Dokumente, wo das status-Feld exakt
“active” ist. Der Filter ist ein JavaScript-Objekt, wo Keys Field-Namen
sind und Values die erwarteten Werte. Multiple Kriterien sind implicit
AND:
db.users.find({ status: "active", age: 28 })Findet Users, die sowohl status: "active" als auch
age: 28 haben. MongoDB scannt die Collection (oder nutzt
einen Index) und returned nur matchende Dokumente.
Case Sensitivity und Type Awareness:
MongoDB ist case-sensitive und type-aware.
{ status: "active" } matched nicht
{ status: "Active" } oder
{ status: "ACTIVE" }. { age: 28 } matched
nicht { age: "28" } (String vs. Number). Dies ist
fundamental anders als manche SQL-Datenbanken mit loser Type-Coercion.
Für case-insensitive Queries muss man Collations oder RegEx nutzen.
Equality-Checks sind nur der Anfang. Für Ranges, Inequalities und komplexere Bedingungen gibt es Comparison-Operatoren:
// Alter größer als 25
db.users.find({ age: { $gt: 25 } })
// Alter zwischen 25 und 35 (inklusive)
db.users.find({ age: { $gte: 25, $lte: 35 } })
// Status ist nicht "banned"
db.users.find({ status: { $ne: "banned" } })
// Username in spezifischer Liste
db.users.find({ username: { $in: ["alice", "bob", "charlie"] } })
// Created date in letzten 7 Tagen
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
db.users.find({ createdAt: { $gte: weekAgo } })Die Syntax { field: { $operator: value } } ist
konsistent über alle Operatoren. Multiple Operatoren auf demselben Feld
werden kombiniert:
// Alter >= 18 UND < 65
db.users.find({ age: { $gte: 18, $lt: 65 } })Performance-Implikation:
Range-Queries auf indizierten Feldern sind effizient. Ein Index auf
age erlaubt MongoDB, direkt zu den Dokumenten im
gewünschten Range zu springen. Ohne Index muss MongoDB jedes Dokument
scannen und checken. Bei großen Collections ist der Unterschied
dramatisch – Millisekunden vs. Sekunden.
// Ohne Index: Scannt alle Dokumente
db.users.find({ age: { $gt: 25 } }).explain("executionStats")
// -> COLLSCAN, totalDocsExamined: 1000000
// Mit Index
db.users.createIndex({ age: 1 })
db.users.find({ age: { $gt: 25 } }).explain("executionStats")
// -> IXSCAN, totalKeysExamined: 500000 (nur relevante Dokumente)Für Queries, die mehr als simple AND benötigen, gibt es logische Operatoren:
$or – Mindestens eine Bedingung:
// User ist entweder admin ODER moderator
db.users.find({
$or: [
{ role: "admin" },
{ role: "moderator" }
]
})Die $or-Syntax nimmt ein Array von Query-Objekten.
MongoDB evaluated jedes und returned Dokumente, die mindestens eines
matchen. Wichtig: $or ist teurer als AND, besonders ohne
Indexes auf allen beteiligten Feldern.
$and – Explizites AND:
Normalerweise ist AND implizit, aber für Queries mit demselben Feld
mehrmals braucht man explizites $and:
// Alter > 18 UND age < 65 (könnten wir auch mit Range machen)
db.users.find({
$and: [
{ age: { $gt: 18 } },
{ age: { $lt: 65 } }
]
})
// Oder komplexere Logik
db.users.find({
$and: [
{ status: "active" },
{ $or: [{ role: "admin" }, { permissions: { $in: ["write"] } }] }
]
})$not – Negation:
// Alle Users NICHT in Berlin
db.users.find({ "address.city": { $not: { $eq: "Berlin" } } })
// Äquivalent zu
db.users.find({ "address.city": { $ne: "Berlin" } })$not ist meist weniger intuitiv als $ne
oder andere Negations-Operatoren. Es ist nützlich für komplexere
Negationen, etwa von RegEx:
// Emails, die NICHT mit .com enden
db.users.find({ email: { $not: /\.com$/ } })MongoDB hat spezielle Semantiken für Array-Felder. Ein Equality-Check matched, wenn das Array das Element enthält:
// Users mit "reading" in hobbies
db.users.find({ hobbies: "reading" })Dies matched { hobbies: ["reading", "gaming"] } genauso
wie { hobbies: ["reading"] }. MongoDB prüft, ob “reading”
irgendwo im Array ist.
$all – Alle Elemente müssen vorhanden sein:
// Users mit SOWOHL "reading" ALS AUCH "gaming" in hobbies
db.users.find({ hobbies: { $all: ["reading", "gaming"] } })Die Reihenfolge ist egal.
["gaming", "reading", "cooking"] würde matchen.
$size – Array-Länge:
// Users mit genau 3 hobbies
db.users.find({ hobbies: { $size: 3 } })Wichtig: $size matched nur exakte Längen, keine Ranges.
Für “mindestens 3” oder “höchstens 5” braucht man andere Approaches –
etwa ein separates hobbyCount-Feld, das bei Updates
inkrementiert wird.
$elemMatch – Komplexe Array-Queries:
Für Arrays von Objects ist $elemMatch essentiell:
// Orders mit mindestens einem item, das > $100 kostet UND quantity >= 2 hat
db.orders.find({
items: {
$elemMatch: {
price: { $gt: 100 },
quantity: { $gte: 2 }
}
}
})Ohne $elemMatch würde MongoDB separate Array-Elemente
matchen können – ein item mit price > 100 und ein
anderes mit quantity >= 2. $elemMatch
stellt sicher, dass dasselbe Element beide Conditions erfüllt.
MongoDB unterstützt Queries auf nested Fields mit Dot-Notation:
// Users in Berlin
db.users.find({ "address.city": "Berlin" })
// Users in Berlin UND PLZ 10115
db.users.find({
"address.city": "Berlin",
"address.postalCode": "10115"
})Die Quotes um den Key sind nötig wegen des Punkts. Dies funktioniert beliebig tief:
db.users.find({ "profile.settings.notifications.email": true })Für komplexere nested Queries kann man das gesamte nested Object matchen:
// Exaktes Match des address-Objects
db.users.find({
address: {
street: "Main St",
city: "Berlin",
postalCode: "10115"
}
})Dies matched nur, wenn das address-Objekt exakt diese
Felder in exakt dieser Reihenfolge hat. Meist ist Dot-Notation
flexibler.
Per Default returned MongoDB alle Felder eines Dokuments. Für Performance sollte man nur benötigte Felder fetchen:
// Nur username und email
db.users.find(
{ status: "active" },
{ username: 1, email: 1 }
)Das zweite Argument zu find() ist die Projection.
1 bedeutet “include”, 0 bedeutet “exclude”.
Man kann nicht mischen (außer für _id, das man explizit
excluden darf):
// Alle Felder außer password
db.users.find(
{ status: "active" },
{ password: 0 }
)
// Username und email, aber ohne _id
db.users.find(
{ status: "active" },
{ username: 1, email: 1, _id: 0 }
)Performance-Impact:
Projection reduziert Netzwerk-Traffic und Memory-Nutzung. Wenn Dokumente 10 KB groß sind, aber man nur 1 KB braucht, spart Projection 90%. Bei tausenden Dokumenten ist dies signifikant. Für Queries, die nur Document-IDs oder wenige Felder brauchen, ist Projection essentiell.
Computed Fields in Projection:
MongoDB 4.4+ erlaubt computed Fields in Projections:
db.users.find({}, {
username: 1,
fullName: { $concat: ["$firstName", " ", "$lastName"] }
})Dies ist syntactic Sugar über Aggregation und weniger common in einfachen Finds.
Die .sort()-Methode auf Cursors sortiert Results:
// Alphabetisch nach username (aufsteigend)
db.users.find().sort({ username: 1 })
// Nach age absteigend (älteste zuerst)
db.users.find().sort({ age: -1 })
// Multi-Field-Sort: Erst nach status, dann nach age
db.users.find().sort({ status: 1, age: -1 })1 ist ascending, -1 ist descending.
Multi-Field-Sort wird left-to-right angewendet – erst status, bei
gleichem status dann age.
Performance:
Sorting ist teuer ohne Index. MongoDB muss alle Dokumente in Memory laden, sortieren und returnen. Für große Result-Sets (tausende Dokumente) kann dies Seconds dauern oder Out-of-Memory-Errors werfen.
Mit Index ist Sorting fast kostenlos – MongoDB iteriert den Index in der gewünschten Reihenfolge:
// Ohne Index: Memory-intensive sort
db.users.find().sort({ createdAt: -1 }).explain("executionStats")
// -> SORT stage in memory
// Mit Index
db.users.createIndex({ createdAt: -1 })
db.users.find().sort({ createdAt: -1 }).explain("executionStats")
// -> IXSCAN, kein extra sort nötigFür Queries mit sort sollte man fast immer einen passenden Index haben.
Die .limit()-Methode beschränkt die Anzahl returned
Dokumente:
// Nur 10 Users
db.users.find().limit(10)Die .skip()-Methode überspringt die ersten N
Dokumente:
// Skip ersten 10, return nächste 10 (Seite 2)
db.users.find().skip(10).limit(10)Dies ist klassische Offset-Based-Pagination. Für Seite N mit
Seiten-Größe M: skip((N-1) * M).limit(M).
Performance-Problem:
skip() ist ineffizient für große Offsets. MongoDB muss
die geskippten Dokumente scannen (auch wenn es sie nicht returned).
skip(10000) scannt 10.000 Dokumente:
// Sehr langsam für große N
db.users.find().skip(10000).limit(10)Besserer Approach – Cursor-Based Pagination:
Statt Offset nutzt man die _id oder einen anderen
sortierenden Key:
// Seite 1
const page1 = db.users.find().sort({ _id: 1 }).limit(10).toArray()
const lastId = page1[page1.length - 1]._id
// Seite 2 - nutze lastId als Cursor
const page2 = db.users.find({ _id: { $gt: lastId } })
.sort({ _id: 1 })
.limit(10)
.toArray()Dies ist konstant schnell, egal wie tief man paginiert, weil MongoDB direkt zum richtigen Index-Position springen kann.
find() returned einen Cursor – ein Iterator-ähnliches
Objekt. Cursors haben nützliche Methoden:
toArray() – Alle Results in Array:
const users = db.users.find({ status: "active" }).toArray()
print(users.length)Dies lädt alle Results in Memory. Für große Result-Sets gefährlich (Out-of-Memory), aber praktisch für kleine Sets.
forEach() – Iterieren ohne Array:
db.users.find({ status: "active" }).forEach(user => {
print(user.username)
// Verarbeitung...
})Dies fetched Dokumente batch-weise (Default 101 Dokumente per Batch), verarbeitet sie und fetched den nächsten Batch. Memory-efficient für große Result-Sets.
count() – Anzahl Dokumente:
const count = db.users.find({ status: "active" }).count()Deprecated! Besser: countDocuments():
const count = db.users.countDocuments({ status: "active" })countDocuments() ist genauer und konsistenter mit
Transactions.
hasNext() und next() – Manuelle Iteration:
const cursor = db.users.find()
while (cursor.hasNext()) {
const user = cursor.next()
print(user.username)
}Dies gibt maximale Kontrolle – man kann Iterieren nach Bedarf pausieren, Conditions checken, etc.
Für eindeutige Werte eines Feldes nutzt man
distinct():
// Alle unterschiedlichen Status-Werte
db.users.distinct("status")
// ["active", "inactive", "banned"]
// Mit Filter
db.users.distinct("status", { age: { $gte: 18 } })Dies ist effizienter als find() + manual deduplication,
besonders wenn ein Index auf dem Feld existiert.
Wenn man nur ein Dokument braucht, ist findOne()
effizienter als find().limit(1):
const user = db.users.findOne({ username: "alice" })
if (user) {
print(user.email)
} else {
print("User not found")
}findOne() returned das Dokument direkt (oder
null wenn nichts matched), keinen Cursor. Es stoppt nach
dem ersten Match – self-limitierend.
Die wichtigste Performance-Tool: .explain():
db.users.find({ email: "alice@example.com" }).explain("executionStats")Die Output zeigt: - executionStages: Welche Strategie
(COLLSCAN, IXSCAN, etc.) - totalDocsExamined: Wie viele
Dokumente gescannt - totalKeysExamined: Wie viele
Index-Keys gescannt - executionTimeMillis: Wie lange
dauerte es
Ideale Query: - executionStages.stage:
“IXSCAN” (Index Scan) - totalDocsExamined: Gleich oder nahe
der Anzahl returned Documents - totalKeysExamined:
Minimal
Problematische Query: -
executionStages.stage: “COLLSCAN” (Collection Scan) -
totalDocsExamined: Millionen, obwohl nur 10 returned -
executionTimeMillis: Seconds
Die Lösung: Index auf das gefilterte Feld:
db.users.createIndex({ email: 1 })Danach: Query nutzt IXSCAN, scannt nur relevante Dokumente, ist 100-1000x schneller.
Compound Indexes für Multi-Field-Queries:
Für Queries auf mehreren Feldern:
db.users.find({ status: "active", age: { $gte: 18 } }).sort({ createdAt: -1 })Ein Compound-Index deckt alle ab:
db.users.createIndex({ status: 1, age: 1, createdAt: -1 })Die Index-Reihenfolge ist kritisch: Equality-Felder zuerst
(status), dann Range-Felder (age), dann
Sort-Fields (createdAt).
Die folgende Tabelle fasst Query-Performance-Patterns zusammen:
| Pattern | Ohne Index | Mit Index | Speedup |
|---|---|---|---|
Equality (field: value) |
COLLSCAN | IXSCAN | 100-1000x |
Range (field: {$gt: val}) |
COLLSCAN | IXSCAN | 50-500x |
| Sort | In-Memory Sort | Index Order | 10-100x |
| Count | Scan alle Docs | Index Count | 100x+ |
| Skip(N) mit großem N | Scan N Docs | Ineffizient auch mit Index | Use Cursor-Pagination |
Read-Operationen sind das Rückgrat von MongoDB-Anwendungen. Die
Query-Sprache ist mächtig und expressiv, aber Performance hängt
fundamental von Indexes ab. Eine Query ohne passenden Index kann
Production-Systeme zum Stillstand bringen. Die Best Practice: Für jede
häufige Query einen passenden Index haben, regelmäßig Slow-Query-Logs
analysieren, und .explain() für Performance-kritische
Queries nutzen. Mit dem richtigen Index-Design sind selbst
Millionen-Dokument-Collections blitzschnell query-bar. Ohne Indexes ist
selbst eine kleine Collection frustrierend langsam.