Every app that reads data from a database eventually needs to answer the same three questions: which rows do I want, in what order, and how many at a time. Appwrite's Query API is the answer to all three.
The Query class gives you a set of methods you pass as an array to any listRows (or equivalent) call. Each method generates a filter, sort directive, or pagination instruction. Multiple queries are combined with AND logic, so every condition must match. If you need OR logic, pass multiple values to the same method instead.
Filtering rows
The most common filters are equality checks. Query.equal('status', ['active']) returns only rows where status is active. To match several values at once, pass them all in the array: Query.equal('status', ['active', 'pending']). That single call behaves like an OR across those values, while still being ANDed with any other queries in your array.
Beyond equality, you have:
Query.notEqual('status', ['banned'])— excludes specific valuesQuery.greaterThan('year', 1999)— numeric or date comparisonsQuery.lessThan('price', 50)Query.greaterThanEqual('score', 100)Query.lessThanEqual('age', 65)Query.contains('tags', ['javascript'])— checks if an array column contains a valueQuery.search('body', 'open source')— full-text search (requires a fulltext index on the column)Query.isNull('deletedAt')/Query.isNotNull('deletedAt')Query.startsWith('username', 'admin')/Query.endsWith('email', '.edu')Query.between('price', 10, 50)— inclusive range check
A practical example combining several filters:
import { Client, TablesDB, Query } from "appwrite";
const client = new Client()
.setEndpoint("https://cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>");
const tablesDB = new TablesDB(client);
const result = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [
Query.equal("status", ["published"]),
Query.greaterThan("year", 1999),
Query.contains("genres", ["sci-fi"]),
],
});
All three conditions must be true for a row to appear in the result.
Sorting results
Two methods control sort order: Query.orderAsc('fieldName') and Query.orderDesc('fieldName'). You can chain them to sort by multiple fields:
const result = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [
Query.equal("status", ["published"]),
Query.orderDesc("createdAt"),
Query.orderAsc("title"),
],
});
This returns published rows sorted newest-first, with ties broken alphabetically by title. Sorting on a column without an index will work for small datasets but becomes slow as the table grows. Adding an index on columns you frequently sort by is covered in the Appwrite Indexes guide.
Pagination: offset vs cursor
Appwrite supports two pagination strategies. Choose based on your use case.
Offset pagination
Offset pagination uses Query.limit(n) and Query.offset(n) together:
const page = 3;
const perPage = 25;
const result = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [
Query.limit(perPage),
Query.offset(perPage * (page - 1)),
],
});
The default limit when you omit Query.limit is 25. The maximum is 5000.
Offset pagination is easy to reason about and supports jumping directly to any page. The downside: for large datasets, a high offset forces the database to scan and discard many rows before returning results. Page 1 is fast; page 5000 is not. It also has consistency issues if rows are inserted or deleted between requests, which can cause items to appear twice or be skipped.
Use offset pagination when:
- Your dataset is small to medium (under a few thousand rows after filtering)
- You need numbered pages or a "go to page N" feature
- Your data changes infrequently
Cursor pagination
Cursor pagination uses Query.cursorAfter(rowId) or Query.cursorBefore(rowId) with a Query.limit:
// First page
const firstPage = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [Query.limit(25)],
});
// Get the last row ID from the first page
const lastId = firstPage.rows[firstPage.rows.length - 1].$id;
// Next page
const secondPage = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [
Query.limit(25),
Query.cursorAfter(lastId),
],
});
For going backwards, pass the first row ID of the current page to Query.cursorBefore.
Cursor pagination is significantly more efficient for large datasets because the database uses the cursor row as an anchor rather than scanning from the beginning. Performance stays consistent regardless of how deep into the dataset you are. The tradeoff is that you cannot jump to an arbitrary page and must navigate sequentially.
Use cursor pagination when:
- Your dataset is large
- You are building an infinite scroll or "next page / previous page" UI
- You want consistent performance across all pages
Mixing limit with filters and sort
Query.limit and pagination queries work alongside filter and sort queries. All queries in the same array apply together:
const result = await tablesDB.listRows({
databaseId: "<DATABASE_ID>",
tableId: "<TABLE_ID>",
queries: [
Query.equal("status", ["published"]),
Query.orderDesc("createdAt"),
Query.limit(10),
Query.cursorAfter(lastRowId),
],
});
Build fast, scale faster
Backend infrastructure and web hosting built for developers who ship.
Start for free
Open source
Support for over 13 SDKs
Managed cloud solution
Performance tips
A few practical rules when working with the Query API:
- Add indexes on columns used in
Query.equal,Query.orderAsc, andQuery.orderDesc. Without an index, Appwrite performs a full table scan for every request. Query.searchrequires a fulltext index on the column. Calling it without one returns an error.- Prefer cursor pagination for any list that could grow beyond a few hundred rows.
- Avoid combining a very high
Query.offsetwith a wideQuery.limit. This is the fastest way to make your database slow. - Be selective with
Query.betweenon unindexed columns. Index the column first if you use it frequently.
Start building with Appwrite Databases
The Query API covers the full range of what you need for data retrieval: precise filtering, flexible sorting, and two pagination strategies suited to different scales. The same Query class works across all Appwrite Database APIs.
To go further:



