Documentation
Introduction
sorla-db is built to stay fast with 100 000 documents and usable well beyond that. This page explains what makes it fast and how to keep it that way.
What is fast
| Operation | Cost | Notes |
|---|---|---|
insertMany |
linear | 50 000 documents in well under a second |
any filter on _id ({ _id }, $eq, $in) |
constant per id | uses the index, in reads, updates, deletes and bulkWrite |
find(query) |
one pass over the documents | no copies: the cursor keeps references |
cursor.ids(), length, count(query) |
no copies at all | |
toArray() / iteration |
proportional to what you read | skip and limit before reading copy only one page |
updateOne by _id |
constant | copies only the changed path of the document |
bulkWrite of 100 000 $set by _id |
about 180 ms | one event per type at the end |
watch() after a change |
proportional to the changed documents | plus a re-sort of the result |
Why
- An
_idindex. Every collection keeps a map from_idto position. Filters on_idgo straight to the documents; other conditions in the same query are still checked. - Copy on read, not on find. A cursor holds references to stored documents and copies a document only when you read it. Filtering 100 000 documents to count them, or to get their ids, copies none.
- Immutable stored documents. sorla-db never changes a stored document in place: an update stores a new version. That makes cursors consistent snapshots for free, and lets an update copy only the objects along the changed path; the rest is shared with the previous version.
- Events copy only when someone listens. Without listeners, writes do not copy anything for events.
- Batches.
insertMany,updateManyandbulkWriteemit one event per operation type, so listeners and live queries do their work once.
Tips
// ✔ count without reading
orders.count({ status: 'open' });
// ✘ orders.find({ status: 'open' }).toArray().length
// ✔ ids when that is all you need
const ids = orders.find({ status: 'open' }).ids();
// ✔ page before reading
orders.find({}).sort({ due: 1 }).skip(200).limit(50).toArray();
// ✔ address documents by _id
orders.updateOne({ _id: id }, { $set: { status: 'done' } });
// ✔ many updates with different values: one bulkWrite
orders.bulkWrite(changes.map(c => ({ updateOne: { filter: { _id: c.id }, update: { $set: c.fields } } })));
// ✘ a loop of updateOne calls with a listener or a live query: one notification per call
// ✔ stop iterating early: the rest is never copied
for (const order of orders.find({ total: { $gt: 1000 } })) { if (found(order)) break; }
// ✔ project large documents when you only need a few fields
orders.find({}, { _id: 1, total: 1 }).toArray();
Memory
Each document is stored once. Copies exist only while you hold the results you read. A cursor of 100 000 matches costs an array of 100 000 references (under 1 MB). In a browser, a few hundred thousand documents of a dozen fields fit comfortably; for millions, load them in blocks with Server-Sent Events and keep only what the screen needs, or page from the server.
Measuring
console.time('query');
orders.find({ city: { $in: ['Recife', 'Natal'] }, total: { $gte: 100 } }).sort({ total: -1 }).limit(20).toArray();
console.timeEnd('query');
The test suite has performance tests with generous limits that catch accidental quadratic behavior
(test/feature_performance.test.js, test/feature_bulk_write.test.js, test/feature_live_query.test.js).