Version: 1.4.0
Status: ✅ Produktionsreif
Aktualisiert: Januar 2026
- Übersicht
- Query-Struktur
- Index-Nutzung
- Performance-Optimierung
- Sicherheit
- Edge Cases
- Häufige Fehler
- Testing und Debugging
Dieser Guide enthält Best Practices für das Schreiben von effizienten, sicheren und wartbaren AQL-Queries in ThemisDB.
// ✅ Gut: Filter VOR Sortierung
FOR doc IN users
FILTER doc.age > 25
SORT doc.name ASC
LIMIT 10
RETURN doc
// ❌ Schlecht: Filter NACH Sortierung
FOR doc IN users
SORT doc.name ASC
FILTER doc.age > 25
LIMIT 10
RETURN doc
Warum: Filtern reduziert die Datenmenge früh, was nachfolgende Operationen beschleunigt.
// ✅ Gut: Projektion
FOR doc IN users
RETURN {
name: doc.name,
email: doc.email
}
// ❌ Schlecht: Ganze Dokumente
FOR doc IN users
RETURN doc
Warum: Reduziert Netzwerk-Traffic und Serialisierung-Overhead.
// ✅ Gut: LIMIT direkt nach FILTER
FOR doc IN users
FILTER doc.status == "active"
LIMIT 100
SORT doc.created_at DESC
LIMIT 10
RETURN doc
Warum: Begrenzt Daten früh, bevor teure Operationen (wie SORT) ausgeführt werden.
// ✅ Gut: Wiederverwendbare Expression
FOR doc IN users
LET fullName = CONCAT(doc.firstName, " ", doc.lastName)
LET isAdult = doc.age >= 18
FILTER isAdult
RETURN {
fullName: fullName,
email: doc.email
}
// ❌ Schlecht: Redundante Berechnung
FOR doc IN users
FILTER doc.age >= 18
RETURN {
fullName: CONCAT(doc.firstName, " ", doc.lastName),
email: doc.email,
greeting: CONCAT("Hello ", CONCAT(doc.firstName, " ", doc.lastName))
}
// ❌ Schlecht: Kartesisches Produkt
FOR user IN users
FOR project IN projects
RETURN {user, project}
// ✅ Gut: Mit Filter
FOR user IN users
FOR project IN projects
FILTER project.owner == user._id
RETURN {user, project}
Warum: Ohne Filter entsteht ein kartesisches Produkt (users × projects), was zu Millionen Ergebnissen führen kann.
// Index erstellen
CREATE INDEX idx_user_email ON users(email) HASH UNIQUE
// Query nutzt Index automatisch
FOR doc IN users
FILTER doc.email == "john@example.com"
RETURN doc
// Composite Index
CREATE INDEX idx_user_status_age ON users(status, age) BTREE
// Query nutzt Index
FOR doc IN users
FILTER doc.status == "active" AND doc.age > 25
RETURN doc
FOR doc IN users
OPTIONS {indexHint: "idx_user_email"}
FILTER doc.email == @email
RETURN doc
Wann: Wenn der Query Optimizer den falschen Index wählt.
// ❌ Schlecht: Index auf Timestamp, das bei jedem Update ändert
CREATE INDEX idx_updated_at ON users(updated_at)
Warum: Index-Wartung wird teuer bei häufigen Updates.
// Partial Index nur für aktive Benutzer
CREATE INDEX idx_active_users ON users(email)
WHERE status == "active"
// Query auf aktive Benutzer
FOR doc IN users
FILTER doc.status == "active" AND doc.email LIKE "%@company.com"
RETURN doc
EXPLAIN
FOR doc IN users
FILTER doc.age > 25
SORT doc.name ASC
LIMIT 10
RETURN doc
Output:
{
"plan": {
"nodes": [
{"type": "IndexNode", "index": "idx_age"},
{"type": "FilterNode"},
{"type": "SortNode"},
{"type": "LimitNode"}
],
"estimatedCost": 150,
"estimatedNrItems": 10
}
}PROFILE
FOR doc IN users
FILTER doc.age > 25
RETURN doc
Output:
{
"stats": {
"executionTime": 45.2,
"scanned": 1000,
"filtered": 250,
"httpRequests": 0
}
}// ✅ Gut: Batch Update
FOR doc IN users
FILTER doc.status == "pending"
LIMIT 1000
UPDATE doc WITH {status: "active"} IN users
OPTIONS {waitForSync: false}
// ❌ Schlecht: Einzelne Updates
FOR doc IN users
FILTER doc.status == "pending"
UPDATE doc WITH {status: "active"} IN users
// ❌ Schlecht: Subquery in jedem Iteration
FOR user IN users
LET projects = (
FOR project IN projects
FILTER project.owner == user._id
RETURN project
)
RETURN {user, projects}
// ✅ Besser: JOIN mit COLLECT
FOR user IN users
FOR project IN projects
FILTER project.owner == user._id
COLLECT u = user INTO projectList = project
RETURN {user: u, projects: projectList}
// Enable streaming
FOR doc IN large_collection
OPTIONS {stream: true}
RETURN doc
Warum: Reduziert Memory-Verbrauch, da Ergebnisse schrittweise zurückgegeben werden.
// ✅ Gut: Bind Variables (AQL Injection sicher)
FOR doc IN users
FILTER doc.email == @email
RETURN doc
Bind Variables:
{
"email": "john@example.com"
}// ❌ Schlecht: String Concatenation (AQL Injection!)
LET email = "john@example.com"
FOR doc IN users
FILTER doc.email == email
RETURN doc
// Server-side validation
function validateEmail(email) {
if (typeof email !== 'string') {
throw new Error('Email must be a string');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error('Invalid email format');
}
return email;
}
const query = `
FOR doc IN users
FILTER doc.email == @email
RETURN doc
`;
const bindVars = {
email: validateEmail(userInput)
};// User hat nur READ-Rechte auf 'users' Kollektion
FOR doc IN users
FILTER doc._id == @userId
RETURN doc
Warum: Minimiert Schaden bei kompromittierten Credentials.
// ❌ Schlecht
console.log(`Query: FOR doc IN users FILTER doc.password == "${password}"`);
// ✅ Gut
console.log(`Query executed with bind variables`);// ✅ Gut: Explizite NULL-Checks
FOR doc IN users
FILTER doc.age != null AND doc.age > 25
RETURN doc
// Alternative: COALESCE
FOR doc IN users
LET age = COALESCE(doc.age, 0)
FILTER age > 25
RETURN doc
// ✅ Gut: Check Array Length
FOR doc IN users
FILTER LENGTH(doc.tags || []) > 0
RETURN doc
// ✅ Gut: HAS() Function
FOR doc IN users
FILTER HAS(doc, "email") AND doc.email != null
RETURN doc
// ✅ Gut: Guard gegen Division durch Zero
FOR doc IN metrics
LET avg = doc.total > 0 ? doc.sum / doc.total : 0
RETURN {doc, avg}
// ✅ Gut: LOWER() für Case-Insensitive Match
FOR doc IN users
FILTER LOWER(doc.email) == LOWER(@email)
RETURN doc
// ❌ Fehler: Gruppierte Daten verloren
FOR doc IN sales
COLLECT product = doc.product
RETURN product
// ✅ Richtig: Mit INTO
FOR doc IN sales
COLLECT product = doc.product INTO sales = doc
RETURN {product, sales}
// ❌ Fehler: LIMIT vor SORT
FOR doc IN users
LIMIT 10
SORT doc.age DESC
RETURN doc
// ✅ Richtig: SORT vor finalem LIMIT
FOR doc IN users
SORT doc.age DESC
LIMIT 10
RETURN doc
// ❌ Langsam: DISTINCT auf großer Datenmenge
FOR doc IN large_collection
RETURN DISTINCT doc.category
// ✅ Schneller: COLLECT für Unique Values
FOR doc IN large_collection
COLLECT category = doc.category
RETURN category
// ❌ Langsam: Full Collection Scan
FOR doc IN users
FILTER doc.email == @email
RETURN doc
Solution: Index erstellen
CREATE INDEX idx_email ON users(email) HASH
// ❌ Sehr langsam: N+1 Problem
FOR user IN users
LET projects = (FOR p IN projects FILTER p.owner == user._id RETURN p)
LET tasks = (FOR t IN tasks FILTER t.assignee == user._id RETURN t)
LET comments = (FOR c IN comments FILTER c.author == user._id RETURN c)
RETURN {user, projects, tasks, comments}
Solution: Denormalisieren oder separate Queries mit JOIN
// Jest Test
describe('User Queries', () => {
test('should find users by age', async () => {
const query = `
FOR doc IN users
FILTER doc.age > @minAge
SORT doc.age ASC
RETURN doc
`;
const result = await db.query(query, {minAge: 25});
expect(result.length).toBeGreaterThan(0);
expect(result[0].age).toBeGreaterThan(25);
});
});// 1. Check intermediate results
FOR doc IN users
LET step1 = doc.age > 25
LET step2 = doc.status == "active"
FILTER step1 AND step2
RETURN {
doc,
debug: {step1, step2}
}
// 2. Use WARN() for logging
FOR doc IN users
LET _ = WARN("Processing user:", doc._id)
FILTER doc.age > 25
RETURN doc
// Benchmark
const iterations = 1000;
const startTime = Date.now();
for (let i = 0; i < iterations; i++) {
await db.query(query, bindVars);
}
const endTime = Date.now();
const avgTime = (endTime - startTime) / iterations;
console.log(`Average query time: ${avgTime}ms`);- Frühe FILTER Klauseln verwenden
- Nur benötigte Felder zurückgeben (Projektion)
- Indizes für Filter-Felder erstellen
- EXPLAIN/PROFILE für Query-Analyse nutzen
- Bind Variables für Security verwenden
- NULL und Edge Cases behandeln
- Subqueries minimieren
- Batch Operations für große Updates
- LIMIT sinnvoll einsetzen
- Streaming für große Ergebnisse aktivieren
// Cursor-based Pagination
FOR doc IN users
FILTER doc._id > @cursor
SORT doc._id ASC
LIMIT @pageSize
RETURN doc
// Top 3 Produkte pro Kategorie
FOR doc IN products
COLLECT category = doc.category INTO products = doc
LET topProducts = (
FOR p IN products
SORT p.sales DESC
LIMIT 3
RETURN p
)
RETURN {category, topProducts}
FOR doc IN orders
COLLECT date = DATE_FORMAT(doc.created_at, "%Y-%m-%d")
AGGREGATE
total = SUM(doc.amount),
highValue = SUM(doc.amount > 100 ? doc.amount : 0),
lowValue = SUM(doc.amount <= 100 ? doc.amount : 0)
RETURN {date, total, highValue, lowValue}
WITH RECURSIVE tree AS (
FOR doc IN categories
FILTER doc.parent_id == null
RETURN {
id: doc._id,
name: doc.name,
level: 0,
path: [doc._id]
}
UNION
FOR doc IN categories
FOR parent IN tree
FILTER doc.parent_id == parent.id
RETURN {
id: doc._id,
name: doc.name,
level: parent.level + 1,
path: APPEND(parent.path, doc._id)
}
)
FOR node IN tree
SORT LENGTH(node.path) ASC
RETURN node