Reads
findMany, findFirst, findOne, findUnique, count, and exists - and how to choose between them.
Every table delegate exposes the same read methods. They differ in how many rows they return and how they handle "not found".
| Method | Returns | Not-found behavior |
|---|---|---|
findMany | Row[] | empty array |
findFirst | Row | null | null, or .throw() |
findOne | Row | null | null, or .throw() (alias of findFirst) |
findUnique | Row | null | null, or .throw() |
count | number | 0 |
exists | boolean | false |
findMany
Returns every matching row. The default when you want a list, a feed, or relation loading.
const users = await client.users.findMany({
where: { active: true },
orderBy: [{ id: 'asc' }],
take: 25,
});findMany accepts the full query surface: where, select / include, orderBy, take, skip, and cursor.
findFirst
The first row after filtering and ordering. Use it when the query can match many rows but you only want one.
const newest = await client.users.findFirst({
where: { active: true },
orderBy: [{ id: 'desc' }],
});findOne
An alias for findFirst, read for intent: a single nullable row where the caller owns the not-found decision.
const user = await client.users.findOne({
where: { id: 42 },
});findUnique
Looks up one row by a key. where must pin a single row with an equality on every primary key column, on a column declared .unique(), or on every column of a composite unique() / uniqueIndex(). Other filters may sit next to the key, and a key inside AND counts.
const user = await client.users.findUnique({
where: { email: 'alice@example.com' },
});Any other where throws UNIQUE_WHERE_REQUIRED before any SQL runs, including from .explain() and .prepare(). An undefined value does not pin, so findUnique({ where: { id: undefined } }) throws instead of returning an arbitrary row. A param() counts as a value in prepared statements. Use findFirst for lookups by non-unique columns.
count and exists
count returns the number of matching rows. exists is a cheap existence check that never materializes a row.
const activeCount = await client.users.count({
where: { active: true },
});
const taken = await client.users.exists({
where: { email: 'alice@example.com' },
});count and exists accept only where, cursor, and meta - projections
and ordering do not apply to them.
Ordering, limit, and offset
const page = await client.posts.findMany({
orderBy: [{ published: 'desc' }, { id: 'desc' }],
skip: 20,
take: 10,
});orderBytakes one field map or an array for multi-column ordering.- Use
{ direction, nulls }when NULL placement matters:orderBy: { lastSeenAt: { direction: 'desc', nulls: 'last' } }. takecaps the number of rows. Withcursor, a negativetakereads the closest preceding rows and returns them in the requested order. Withoutcursor, the absolute value is used as the limit.skipoffsets from the start.
Ordering by relations
Added in 0.3.0orderBy also takes relation keys, nested the same way as relation filters:
- A
onerelation takes a field map of the related table. It can nest to any depth, and its fields accept'asc'/'desc'or{ direction, nulls }. - A
manyor many-to-many (.through()) relation takes{ count: 'asc' | 'desc' }.
const byAuthor = await client.posts.findMany({
orderBy: [{ author: { name: 'asc' } }, { id: 'asc' }],
});
const mostActive = await client.users.findMany({
orderBy: { posts: { count: 'desc' } },
take: 10,
});Each relation key compiles to a correlated subquery in the ORDER BY of the same statement, so the row count and the number of queries stay the same.
- Deeper paths nest:
orderBy: { author: { profile: { bio: { direction: 'desc', nulls: 'last' } } } }. - A
onerelation without a related record sorts asNULL. Usenulls: 'first' | 'last'to place these values; MySQL gets this through the sameIS NULLterm as scalar columns. A to-manycountis0when no related rows match. - Mix relation and scalar keys in the array form, and end with a unique key such as
idso rows that tie on the relation value keep a stable order. - Relation sorts work in
findMany,findFirst,findOne,findUnique, andpaginate, and inside nestedinclude/selectrelation args, including ones withtake/skip. - A scalar sort on a to-many relation (
{ posts: { title: 'asc' } }) or an aggregatecounton aonerelation is a compile error, and throwsINVALID_ARGSfor untyped callers. If the related table has a scalar column namedcount, aonerelation can sort by that column normally. - The subqueries do not apply soft-delete filters, the same as relation loads.
- Relations that cannot be loaded (a relation-level
where, or aonerelation through a junction) cannot be sorted by either; they throw the same error as inwhereandinclude.
No relation sorts with cursors
cursor() and the cursor read argument throw INVALID_ARGS when orderBy
contains a relation key, because cursor tokens hold only the row's own column
values. Use paginate() or skip / take for pages sorted by a relation.
Index the foreign keys you sort by
Each subquery looks up related rows by the relation's key for every sorted
row. Primary keys are indexed already; add an index on the foreign key column
(such as posts.authorId) used by a count sort or by a one relation
whose foreign key sits on the related table. For .through() counts, index
the junction columns used to link both tables. Sorting still requires the
database to evaluate these expressions; one statement does not imply the
same cost as scalar ordering. Check the plan with
.explain().
For full offset pages with totals, use paginate. For feed-style navigation with cursors, use cursor.
Throwing when a row is required
findFirst, findOne, and findUnique return a value that is both awaitable as Row | null and exposes .throw():
// Treat not-found as null
const maybeUser = await client.users.findUnique({
where: { email },
});
// Treat not-found as exceptional
const user = await client.users
.findUnique({ where: { email } })
.throw(() => new Error('User not found'));See throwing results for the full pattern.
Choosing a read method
| Use… | When |
|---|---|
findMany | lists, feeds, admin tables, relation loading |
findFirst | the first match after ordering/filtering |
findOne | a single nullable row, caller handles not-found |
findUnique | lookup by a unique field like email or slug |
count | an aggregate count for a filtered set |
exists | a cheap "is there at least one?" check |