Docs

Querying documents

Filter collections by any field, sort by several fields, page with cursors, count matches, and index what you query.

Updated Oct 17, 2026Open in console

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.

POST _querySQL as the callerYour appwhere · orderBy ·limit · cursorGatewayauth · rate limit· readGuardnosql:read on thecollectionProject Postgressov_documents ·row policies ·indexes
Values are always bound parameters; field paths and the collection are quoted by the database driver so Postgres can use your indexes.

Filters

OperatorMatches 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-inequals one of / exists and equals none of up to 30 values
array-containsis an array holding the value (objects must match exactly)
array-contains-anyis 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; limit up 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 | failed

Creating 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

LimitFreeProUltraTeamEnterprise
Documents per query (nosql.max_scan_rows)1,0005,00010,00025,00050,000
Document indexes per project (docs.max_indexes)101003001,0005,000
Query timeout (sql.statement_timeout_ms)3 s8 s15 s30 s60 s
Something unclear or missing on this page?Tell us