Querying documents
Filter collections by any field, sort by several fields, page with cursors, count matches, and index what you query.
A query reads one collection (or subcollection, like users/alice/posts) and returns the documents whose fields match its filters, in the order you ask for. It runs in your project's Postgres as the caller, so collection rules and row policies decide exactly what each user gets back — the same as reading one document.
Filters
| Operator | Matches documents where the field… |
|---|---|
==, != | equals / exists and does not equal the value |
<, <=, >, >= | compares with the value — only values of the same type match ("1999" is not > 1900) |
in, not-in | equals one of / exists and equals none of up to 30 values |
array-contains | is an array holding the value (objects must match exactly) |
array-contains-any | is an array holding at least one of up to 30 values |
- Use dotted paths for nested fields:
address.city. A key with other characters goes in backticks: `meta.first-seen - The document id is
__key__:where("__key__", "in", ["a", "b"]). - Ordering by a field leaves out documents that don't have it. Ties are broken by document id.
- Up to 30 filters and 8 order-by fields per query;
limitup to your plan's documents per query.
const orders = sov.collection('orders')
const page = await orders
.where('status', '==', 'paid')
.where('total', '>=', 50)
.orderBy('total', 'desc')
.limit(20)
.get()
page.docs.forEach((d) => console.log(d.id, d.data, d.version))
// Next page: pass the last snapshot (or page.nextCursor)
const next = await orders.where('status', '==', 'paid').where('total', '>=', 50)
.orderBy('total', 'desc').limit(20).startAfter(page.docs.at(-1)).get()
const howMany = await orders.where('status', '==', 'paid').count()Cursors and pages
startAt / startAfter / endAt / endBefore take either a document snapshot (or {"key": "id"} over HTTP — the server reads that document, as you, for its values) or the values of the order-by fields in order, optionally followed by the document id. limitToLast(n) returns the last n in the same order. Every answer says has_more and gives next_cursor for the next page.
Indexes
Equality and array filters use the document table's JSON index on every project. For sorting and ranges, Sovaria indexes the first order-by or range field automatically once a collection has 1,000 documents. Queries that combine several fields run fastest on a composite index: the answer's index_hint names it, and Collections › Indexes (or the API) creates it in the background without blocking writes.
await sov.data.createDocIndex('orders', ['status', { path: 'total', order: 'desc' }])
const { indexes, limit } = await sov.data.docIndexes('orders') // state: building | ready | failedCreating and dropping indexes is for project team members and the service key. A query slower than your plan's statement timeout returns 504 DEADLINE_EXCEEDED with the index that would help.
In the console
- Collections › Query: build filters and sorting, run, load more, and export the results as JSON, JSON Lines or CSV.
- Collections › Indexes: every automatic and composite index with its state and size, and your plan's cap.
- Collections › Data: large collections load 100 documents at a time; import JSON, JSON Lines or CSV (dotted headers become nested fields).
Plan limits
| Limit | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
Documents per query (nosql.max_scan_rows) | 1,000 | 5,000 | 10,000 | 25,000 | 50,000 |
Document indexes per project (docs.max_indexes) | 10 | 100 | 300 | 1,000 | 5,000 |
Query timeout (sql.statement_timeout_ms) | 3 s | 8 s | 15 s | 30 s | 60 s |