Optimize CloudBase Cloud Database Query Performance
In one sentence: CloudBase's document database is based on the MongoDB protocol. This recipe walks through the sequence of identify → add Indexes → restructure → change Pagination, covering common Slow Query scenarios, explaining the mechanism behind each step (why it works) and its limits as data grows — without fabricated millisecond numbers.
Estimated time: 35 minutes | Difficulty: Advanced
Applicable Scenarios
- Applicable: Already able to read and write the database normally (completing add-database-wechat-miniprogram is sufficient) and starting to care about query performance
- Applicable: Data volume growing from a few thousand records to hundreds of thousands or millions, causing queries to slow down
- Applicable: Typical read-heavy scenarios like daily reports / lists / search
- Not applicable: Data exceeding tens of millions of records with complex cross-collection joins and strong relational requirements. Document databases are not suited for relational operations at this scale — see Step 7 "When to switch to a relational database"
- Not applicable: Scenarios where write performance is the bottleneck (this recipe focuses on reads). Adding too many Indexes actually slows down writes — Step 2 covers this
Prerequisites
| Dependency | Version |
|---|---|
| CloudBase environment | Any — document database is supported by default |
@cloudbase/js-sdk or @cloudbase/node-sdk | Any current version |
| Console access | "Document Database → Collection Management / Index Management / Slow Query" must be accessible |
Step 1: Find Slow Queries first, then decide what to optimize
The first principle of Performance Optimization: prove it's slow before optimizing. Blindly adding Indexes often targets the wrong place and actually degrades write performance.
Three entry points for identification:
1. Console Slow Query logs
Console → Environment Overview → Advanced → CLS Logs (ensure enabled) → search for module:database AND eventType:MongoSlowQuery. This shows query conditions, Index hit status, and documents scanned for all high-latency queries.
If you prefer pulling logs via SDK:
const CloudBase = require('@cloudbase/manager-node');
const manager = CloudBase.init({ secretId, secretKey, envId });
const res = await manager.log.searchClsLog({
queryString: 'module:database AND eventType:MongoSlowQuery',
StartTime: '2024-04-01 00:00:00',
EndTime: '2024-04-01 23:59:59',
Limit: 100,
Sort: 'desc',
});
for (const log of res.Results || []) {
console.log(log.Timestamp, log.Content);
}
Full interface signature: searchClsLog.
2. Measure individual call latency
In a Cloud Function, use Date.now() to bracket a time window and log the RequestId. If the response is slow, look up the specific execution details by that RequestId.
3. Console "Monitoring & Alerts"
Console → Database → Monitoring & Alerts shows read/write latency, Slow Query ratio, connection count, and other metrics as global trend charts — more useful than individual samples for seeing the scope of a problem.
How to define "slow": don't get hung up on "how many ms is slow". Look at relative values — if one type of query takes 5-10x longer than most queries on the same collection, or if the number of documents scanned is far greater than the number returned (e.g., returning 20 but scanning 100,000), it's worth investigating.
Step 2: Add Indexes
Without an Index on the field used in a where clause, a document database performs a Full Collection Scan — time grows linearly with collection size. With an Index, this drops to logarithmic time (B-Tree or similar structure lookup).
Adding Indexes in Console
Console → Database → Collection → Index Management → Create Index:
- Single-field Index: for queries/sorts on one field. Example:
where({status: 'pending'})— add a single-field Index onstatus - Compound Index: for queries on multiple fields. Example:
where({userId, status}).orderBy('createdAt', 'desc')— add a three-field compound Index on(userId, status, createdAt) - Unique Index: in addition to speeding up queries, enforces a uniqueness constraint on writes
Full rules: data-index.
The "leftmost prefix" rule for Compound Indexes
A compound Index on (a, b, c) can be used for:
| Query condition | Index used? |
|---|---|
where({a: x}) | Yes |
where({a: x, b: y}) | Yes |
where({a: x, b: y, c: z}) | Yes |
where({b: y}) | No |
where({a: x, c: z}) | Partial (only a) |
Mechanism: the Index is an ordered structure keyed by field concatenation; skipping a leading field makes it impossible to navigate the structure.
When designing compound Indexes, put equality conditions first and range conditions last (gt / lt / in):
// Recommended: userId equality first, createdAt range last
await db.collection('orders')
.where({
userId: 'u1', // equality
createdAt: db.command.gte(yesterday), // range
})
.orderBy('createdAt', 'desc')
.get();
// Index: (userId, createdAt)
More Indexes is not always better
- Every Index must be updated on write; Index count directly affects write latency
- Indexes consume storage; under memory pressure, overall performance degrades
- CloudBase documentation recommends no more than 20 Indexes per collection — see the Index count limit section in data-index
- Delete unused Indexes promptly. Console "Index Management" shows each Index's hit frequency
Sort direction must match
A compound Index on (age: ascending, score: descending) can be used for:
orderBy('age', 'asc').orderBy('score', 'desc')(exact match)orderBy('age', 'desc').orderBy('score', 'asc')(fully reversed — Index can be traversed in reverse)
Cannot be used for:
orderBy('age', 'asc').orderBy('score', 'asc')(mixed directions)
See the sort direction section in data-index.
Step 3: Avoid Full Collection Scans
The most common cause of Slow Queries is an unintentional Full Collection Scan:
Anti-pattern: missing limit
// Collection has 500,000 records — this pulls all of them
const res = await db.collection('logs').where({ level: 'error' }).get();
Correct approach — always limit:
const res = await db
.collection('logs')
.where({ level: 'error' })
.orderBy('createdAt', 'desc')
.limit(50)
.get();
Anti-pattern: large skip values
// Page 1000, 20 records per page
const res = await db.collection('items').skip(20000).limit(20).get();
skip still reads from the beginning and discards those records internally. Larger skip = slower query. Use cursor-based Pagination instead (Step 6).
Anti-pattern: regular expressions
// Regex cannot use Indexes
await db.collection('users').where({ name: /^张/ }).get();
For fuzzy search, switch to PostgreSQL full-text Index, or store the leading character / pinyin as a redundant field with its own Index at write time.
Step 4: Replace multiple queries with an Aggregation Pipeline
A common pattern in business code is: "query a set of A, use A's results to query B, then merge":
// Anti-pattern: N+1 queries
const orders = await db.collection('orders').where({ userId }).get();
const productIds = orders.data.map((o) => o.productId);
// One RPC per product — N network round trips
const products = [];
for (const id of productIds) {
const p = await db.collection('products').doc(id).get();
products.push(p.data[0]);
}
One Aggregation Pipeline is much more network-efficient:
// Run server-side (Web SDK does not support aggregate; Mini Programs don't either — wrap in a Cloud Function)
const cloudbase = require('@cloudbase/node-sdk');
const app = cloudbase.init();
const db = app.database();
const res = await db
.collection('orders')
.aggregate()
.match({ userId })
.lookup({
from: 'products',
localField: 'productId',
foreignField: '_id',
as: 'product',
})
.project({
_id: 1,
productId: 1,
product: { $arrayElemAt: ['$product', 0] },
amount: 1,
})
.end();
Key points:
aggregateis only supported in the Node SDK (Cloud Functions / server side) document database; the frontend SDK (@cloudbase/js-sdk) does not support it. To use it from the frontend, wrap it in a Cloud Function- If
matchis at the start of the Pipeline, it can use Indexes (same Index utilization rules aswhere) - Full Pipeline operators: aggregate API
- Aggregation is more complex to execute than a single
where. Uselookup(essentially a join) with caution on large data volumes
Sort memory limit is 100MB (MongoDB default). For large data volumes, either add match / limit before sort to reduce the working set, or add an Index on the sort field so sort can use the Index. See the FAQ in aggregate.md.
Step 5: Field projection — only fetch what you need
// Anti-pattern: document has 30 fields; only 3 are needed, but all are fetched
const res = await db
.collection('orders')
.where({ userId })
.limit(50)
.get();
// Recommended: use field to fetch only required fields
const res = await db
.collection('orders')
.where({ userId })
.field({ _id: true, status: true, amount: true, createdAt: true })
.limit(50)
.get();
Mechanism: network transfer bytes are directly proportional to the number and size of returned fields. If a document has large fields (description with thousands of characters, an images array, nested objects), not fetching them saves significant bandwidth and deserialization time.
Note: field is a whitelist — listed fields are included, others are excluded. _id is returned by default even if not listed.
Step 6: Cursor-based Pagination for large datasets
skip + limit is slow on large collections: page 1000 requires reading and discarding 19,999 records before returning 20.
Cursor-based Pagination uses the last record's ordered field value from the previous page as the starting point for the next:
async function listOrders(userId, lastCursor) {
let query = db
.collection('orders')
.where({ userId })
.orderBy('createdAt', 'desc')
.orderBy('_id', 'desc'); // add _id as tiebreaker to prevent missing/duplicate records when createdAt values are equal
if (lastCursor) {
const _ = db.command;
query = query.where({
// Full or-logic needed here — see below
...
});
}
return query.limit(20).get();
}
The full cursor logic handles the "same createdAt, break ties by _id" case — verbose but not complex:
import { db } from './cloudbase';
const _ = db.command;
async function listOrdersCursor(userId, cursor) {
const base = { userId };
// cursor = { createdAt, _id } from the last record of the previous page
let where;
if (cursor) {
where = _.or([
{ ...base, createdAt: _.lt(cursor.createdAt) },
{ ...base, createdAt: cursor.createdAt, _id: _.lt(cursor._id) },
]);
} else {
where = base;
}
const res = await db
.collection('orders')
.where(where)
.orderBy('createdAt', 'desc')
.orderBy('_id', 'desc')
.limit(20)
.get();
const last = res.data[res.data.length - 1];
return {
items: res.data,
nextCursor: last ? { createdAt: last.createdAt, _id: last._id } : null,
};
}
Key points:
- The Index must include
(userId, createdAt, _id)so thewhere+sortabove can fully use the Index - There is no "jump to page N" capability — only next/previous page. If the business requires page jumping, accept the slow
skip, or implement client-side caching
Step 7: When to switch to a relational database
Document databases are well-suited for:
- Self-contained documents (order + nested items / user profile + nested address list)
- Variable fields / frequently changing schema
- Read-heavy + single-document / single-collection queries
Not well-suited for:
- Multi-table joins with many related fields.
lookupis a performance sink in document databases — latency spikes sharply at scale - Strong cross-collection transactions (atomically credit A and debit B). Document databases have transactions but cross-collection performance is poor
- Complex analytics (group by + having + multi-level joins)
For these scenarios, CloudBase provides PostgreSQL / TDSQL integration. Enable it in Console under "Database → Relational Database" and access it via the PG RESTful API or direct SQL.
Hybrid setups are common: document database for primary data, relational database for analytics reports. But hybrid introduces complexity costs too. For small projects, start with a single choice; consider migration when data reaches millions of records.
Verification
- In Console → CLS Logs, run a search for
module:database AND eventType:MongoSlowQueryand note the current Slow Query count - Find a Slow Query, inspect its
whereconditions, and add the corresponding compound Index in Console "Index Management" - Wait a few minutes (Index builds in the background; may take tens of seconds for large collections), then trigger the same query — it should no longer appear in Slow Query logs
- Run the Aggregation-replacing-N+1 example; on the Cloud Function side, use
console.time/console.timeEndto measure latency — there should be an order-of-magnitude difference - After switching to cursor-based Pagination, the latency for fetching page N should no longer increase with N
Common Errors
| Error Symptom | Cause | Fix |
|---|---|---|
| Query still slow after adding Index | Index field order does not match query conditions, or sort direction is inconsistent | See Step 2 "leftmost prefix" and "sort direction must match" |
| Index creation is very slow or times out | Large collection (millions of records) | Build Indexes during off-peak hours; CloudBase builds in the background — collection remains readable and writable |
aggregate reports Sort exceeded memory limit of 104857600 bytes | Sort fields are large or the working set was not reduced first | Add match / limit earlier in the Pipeline to shrink the set; add Index on sort fields; reduce sort field count |
aggregate errors when called from the frontend | Frontend SDK does not support aggregate | Wrap in a Cloud Function; call from frontend via callFunction |
skip(N) Pagination gets slower and slower | Large skip is inherently slow | Switch to cursor-based Pagination (Step 6) |
| Added compound Index but some queries are still slow | Some query conditions are not in the Index prefix | Check specific query conditions — may need to add another Index with a different field order (keep total under 20) |
| Unique Index creation reports "duplicate values" | Existing data contains duplicates | Clean up duplicate data before creating the unique Index |
Related Documentation
- Index Management — Single-field / compound / unique / geospatial Index creation and limits
- Aggregation — Pipeline stages and operators
- Web SDK Database API — Full signatures for
where / orderBy / field / limit - Node SDK Aggregation API — Server-side Aggregation Pipeline
- searchClsLog — Slow Query log search
- add-database-wechat-miniprogram — Prerequisite: basic read/write
Next Steps
- Scheduled monitoring for Cloud Function errors: alert-cloud-function-errors-to-wecom
- Security Rules for multi-tenant scenarios (permission rules also affect query plans): secure-database-multi-tenant-rules
- Migration considerations across database types: migrate-wxcloud-to-cloudbase data migration section