NoSQL Databases (MongoDB, Convex, Document Stores)
Expertise: Senior database administrator with 20+ years of experience in document stores, key-value systems, and non-relational data modeling. Focus on query optimization, indexing strategy, and data access best practices.
General NoSQL Principles
Document Design
- Embed vs Reference: Embed when data is always read together and rarely grows unbounded; reference when data is shared, large, or updated independently
- Avoid unbounded arrays: Documents with arrays that grow without limit cause performance degradation; use separate collections with references
- Denormalize for read patterns: Optimize for how data is read; duplicate when it improves query performance and consistency is acceptable
Query Patterns
- Index every query path: Queries without indexes cause full collection scans; at scale, indexed queries are orders of magnitude faster
- Project only needed fields: Reduce network and memory by projecting only required fields (
projection in MongoDB, selective fields in Convex)
- Paginate large result sets: Never
.collect() or .find() without limits when result sets can be large (e.g. >1000 documents)
Consistency
- Understand read-your-writes: Document stores often offer eventual consistency; use appropriate read concern when strong consistency is required
- Design for idempotency: Retries and eventual consistency make duplicate operations possible; design mutations to be idempotent
MongoDB
Index Types and When to Use
| Type |
Use Case |
Example |
| Single-field |
Equality, sort on one field |
{ userId: 1 } |
| Compound |
Multi-field queries; order matters |
{ channel: 1, createdAt: -1 } |
| Multikey |
Arrays (one index entry per array element) |
{ tags: 1 } |
| Text |
Full-text search |
{ content: "text" } |
| Geospatial |
Location queries |
2dsphere, 2d |
Index Rules
- Index fields in WHERE, sort, and projection—avoid full collection scans
- Compound index order: equality → sort → range; put most selective fields first
// Good for db.collection.find({ channel: "x" }).sort({ createdAt: -1 })
db.collection.createIndex({ channel: 1, createdAt: -1 });
- Covered queries: When query + projection use only indexed fields, MongoDB reads only the index (no document fetch)
- Avoid low-selectivity operators:
$nin, $ne, $exists: false often match large portions of the index
- Limit indexes per collection: Max 64 indexes; each index adds write cost—measure before adding
Aggregation Pipeline Optimization
- Use
$match and $project early to reduce documents and fields early in the pipeline
- Use
$indexStats and $queryStats to analyze query patterns and index usage
- Prefer
$lookup with pipeline and let for complex joins; avoid unbounded $lookup on large collections
Explain and Profiling
db.collection.find({ userId: "x" }).explain("executionStats");
// Check: stage "IXSCAN" (index scan) vs "COLLSCAN" (full scan)
// Review: docsExamined, nReturned, executionTimeMillis
Security
- Use parameterized queries; never concatenate user input into queries
- Apply principle of least privilege for database users
- Validate and sanitize
$where and aggregation $function inputs
Convex
Schema and Indexes
Indexes are defined in the schema; every query should use an index via .withIndex():
// schema.ts
defineSchema({
messages: defineTable({
channel: v.string(),
userId: v.id("users"),
text: v.string(),
createdAt: v.number(),
})
.index("by_channel", ["channel"])
.index("by_channel_created", ["channel", "createdAt"])
.index("by_user", ["userId"]),
});
Query Best Practices
Use .withIndex() instead of .filter(): Index-based queries are efficient; .filter() scans the table
// Good: uses index
const messages = await ctx.db.query("messages").withIndex("by_channel", q => q.eq("channel", channelId)).collect();
// Avoid: full table scan
const messages = await ctx.db.query("messages").filter(q => q.eq(q.field("channel"), channelId)).collect();
Use .withSearchIndex() for full-text: When you need search, define and use search indexes
Paginate with .paginate(): For large result sets, use pagination; avoid .collect() on unbounded queries
Staged indexes for large tables: When adding indexes to tables with substantial data, use staged indexes to avoid slow backfill during deployment
Index Removal
- Ensure an index is completely unused before removing it; deployments will delete unused indexes
Other Document Stores (Firestore, DynamoDB, etc.)
Firestore
- Composite indexes for multi-field queries; define in Firebase Console or
firestore.indexes.json
- Batch reads with
getAll() to reduce round trips
- Use
limit() and startAfter() for pagination
DynamoDB
- Design for single-table access patterns; partition key + sort key define access
- Use GSIs (Global Secondary Indexes) for alternate query patterns
- Avoid scans; use
Query with key conditions
Performance Checklist
Source: iamjcabalejo/payoys-cursor-sub-agents — distributed by TomeVault.
1---2name: nosql-databases3description: Apply NoSQL best practices for MongoDB, Convex, and document databases. Use when designing schemas, writing queries, optimizing performance, or building applications with non-relational databases. Use with database-expert for query optimization and DBA-level tuning (20+ years experience). Use when this capability is needed.4---56# NoSQL Databases (MongoDB, Convex, Document Stores)78**Expertise**: Senior database administrator with 20+ years of experience in document stores, key-value systems, and non-relational data modeling. Focus on query optimization, indexing strategy, and data access best practices.910---1112## General NoSQL Principles1314### Document Design15- **Embed vs Reference**: Embed when data is always read together and rarely grows unbounded; reference when data is shared, large, or updated independently16- **Avoid unbounded arrays**: Documents with arrays that grow without limit cause performance degradation; use separate collections with references17- **Denormalize for read patterns**: Optimize for how data is read; duplicate when it improves query performance and consistency is acceptable1819### Query Patterns20- **Index every query path**: Queries without indexes cause full collection scans; at scale, indexed queries are orders of magnitude faster21- **Project only needed fields**: Reduce network and memory by projecting only required fields (`projection` in MongoDB, selective fields in Convex)22- **Paginate large result sets**: Never `.collect()` or `.find()` without limits when result sets can be large (e.g. >1000 documents)2324### Consistency25- **Understand read-your-writes**: Document stores often offer eventual consistency; use appropriate read concern when strong consistency is required26- **Design for idempotency**: Retries and eventual consistency make duplicate operations possible; design mutations to be idempotent2728---2930## MongoDB3132### Index Types and When to Use3334| Type | Use Case | Example |35|------|----------|---------|36| **Single-field** | Equality, sort on one field | `{ userId: 1 }` |37| **Compound** | Multi-field queries; order matters | `{ channel: 1, createdAt: -1 }` |38| **Multikey** | Arrays (one index entry per array element) | `{ tags: 1 }` |39| **Text** | Full-text search | `{ content: "text" }` |40| **Geospatial** | Location queries | `2dsphere`, `2d` |4142### Index Rules43441. **Index fields in WHERE, sort, and projection**—avoid full collection scans452. **Compound index order**: equality → sort → range; put most selective fields first46 ```javascript47 // Good for db.collection.find({ channel: "x" }).sort({ createdAt: -1 })48 db.collection.createIndex({ channel: 1, createdAt: -1 });49 ```503. **Covered queries**: When query + projection use only indexed fields, MongoDB reads only the index (no document fetch)514. **Avoid low-selectivity operators**: `$nin`, `$ne`, `$exists: false` often match large portions of the index525. **Limit indexes per collection**: Max 64 indexes; each index adds write cost—measure before adding5354### Aggregation Pipeline Optimization55- Use `$match` and `$project` early to reduce documents and fields early in the pipeline56- Use `$indexStats` and `$queryStats` to analyze query patterns and index usage57- Prefer `$lookup` with `pipeline` and `let` for complex joins; avoid unbounded `$lookup` on large collections5859### Explain and Profiling60```javascript61db.collection.find({ userId: "x" }).explain("executionStats");62// Check: stage "IXSCAN" (index scan) vs "COLLSCAN" (full scan)63// Review: docsExamined, nReturned, executionTimeMillis64```6566### Security67- Use parameterized queries; never concatenate user input into queries68- Apply principle of least privilege for database users69- Validate and sanitize `$where` and aggregation `$function` inputs7071---7273## Convex7475### Schema and Indexes7677Indexes are defined in the schema; every query should use an index via `.withIndex()`:7879```typescript80// schema.ts81defineSchema({82 messages: defineTable({83 channel: v.string(),84 userId: v.id("users"),85 text: v.string(),86 createdAt: v.number(),87 })88 .index("by_channel", ["channel"])89 .index("by_channel_created", ["channel", "createdAt"])90 .index("by_user", ["userId"]),91});92```9394### Query Best Practices95961. **Use `.withIndex()` instead of `.filter()`**: Index-based queries are efficient; `.filter()` scans the table97 ```typescript98 // Good: uses index99 const messages = await ctx.db.query("messages").withIndex("by_channel", q => q.eq("channel", channelId)).collect();100 // Avoid: full table scan101 const messages = await ctx.db.query("messages").filter(q => q.eq(q.field("channel"), channelId)).collect();102 ```1031042. **Use `.withSearchIndex()` for full-text**: When you need search, define and use search indexes1051063. **Paginate with `.paginate()`**: For large result sets, use pagination; avoid `.collect()` on unbounded queries1071084. **Staged indexes for large tables**: When adding indexes to tables with substantial data, use staged indexes to avoid slow backfill during deployment109110### Index Removal111- Ensure an index is completely unused before removing it; deployments will delete unused indexes112113---114115## Other Document Stores (Firestore, DynamoDB, etc.)116117### Firestore118- Composite indexes for multi-field queries; define in Firebase Console or `firestore.indexes.json`119- Batch reads with `getAll()` to reduce round trips120- Use `limit()` and `startAfter()` for pagination121122### DynamoDB123- Design for single-table access patterns; partition key + sort key define access124- Use GSIs (Global Secondary Indexes) for alternate query patterns125- Avoid scans; use `Query` with key conditions126127---128129## Performance Checklist130131- [ ] Every query path has a supporting index132- [ ] No full collection/table scans in hot paths (verify with explain/profiler)133- [ ] Projections limit returned fields134- [ ] Large result sets use pagination135- [ ] Unbounded arrays avoided in document design136- [ ] Read/write patterns inform embedding vs referencing137- [ ] Mutations are idempotent where retries are possible138139---140> Source: [iamjcabalejo/payoys-cursor-sub-agents](https://github.com/iamjcabalejo/payoys-cursor-sub-agents) — distributed by [TomeVault](https://tomevault.io).141<!-- tomevault:4.0:skill_md:2026-06-04 -->