# Reads

> findMany, findFirst, findOne, findUnique, count, and exists - and how to choose between them.

URL: https://better-drizzle.com/docs/querying/reads

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.



**better-drizzle**

```ts  
const users = await client.users.findMany({
	where: { active: true },
	orderBy: [{ id: 'asc' }],
	take: 25,
});
```

**Drizzle**

```ts  
import { asc, eq } from 'drizzle-orm';

const rows = await db
	.select()
	.from(users)
	.where(eq(users.active, true))
	.orderBy(asc(users.id))
	.limit(25);
```

`findMany` accepts the full query surface: [`where`](/docs/querying/filters), [`select` / `include`](/docs/querying/selecting-fields), `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.



**better-drizzle**

```ts  
const newest = await client.users.findFirst({
	where: { active: true },
	orderBy: [{ id: 'desc' }],
});
```

**Drizzle**

```ts  
import { desc, eq } from 'drizzle-orm';

const [newest = null] = await db
	.select()
	.from(users)
	.where(eq(users.active, true))
	.orderBy(desc(users.id))
	.limit(1);
```

## `findOne`

An alias for `findFirst`, read for intent: a single nullable row where the **caller** owns the not-found decision.



**better-drizzle**

```ts  
const user = await client.users.findOne({
	where: { id: 42 },
});
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const [user = null] = await db
	.select()
	.from(users)
	.where(eq(users.id, 42))
	.limit(1);
```

## `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.



**better-drizzle**

```ts  
const user = await client.users.findUnique({
	where: { email: 'alice@example.com' },
});
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

const [user = null] = await db
	.select()
	.from(users)
	.where(eq(users.email, 'alice@example.com'))
	.limit(1);
```

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.



**better-drizzle**

```ts  
const activeCount = await client.users.count({
	where: { active: true },
});

const taken = await client.users.exists({
	where: { email: 'alice@example.com' },
});
```

**Drizzle**

```ts  
import { count, eq, sql } from 'drizzle-orm';

const [{ activeCount }] = await db
	.select({ activeCount: count() })
	.from(users)
	.where(eq(users.active, true));

const found = await db
	.select({ one: sql`1` })
	.from(users)
	.where(eq(users.email, 'alice@example.com'))
	.limit(1);
const taken = found.length > 0;
```

> **Note:** `count` and `exists` accept only `where`, `cursor`, and `meta` - projections
> and ordering do not apply to them.

## Ordering, limit, and offset



**better-drizzle**

```ts  
const page = await client.posts.findMany({
	orderBy: [{ published: 'desc' }, { id: 'desc' }],
	skip: 20,
	take: 10,
});
```

**Drizzle**

```ts  
import { desc } from 'drizzle-orm';

const page = await db
	.select()
	.from(posts)
	.orderBy(desc(posts.published), desc(posts.id))
	.offset(20)
	.limit(10);
```

* `orderBy` takes one field map or an array for multi-column ordering.
* Use `{ direction, nulls }` when NULL placement matters: `orderBy: { lastSeenAt: { direction: 'desc', nulls: 'last' } }`.
* `take` caps the number of rows. With `cursor`, a negative `take` reads the closest preceding rows and returns them in the requested order. Without `cursor`, the absolute value is used as the limit.
* `skip` offsets from the start.

### Ordering by relations

(Added in 0.3.0)

`orderBy` also takes relation keys, nested the same way as [relation filters](/docs/querying/relation-filters):

* A `one` relation takes a field map of the related table. It can nest to any depth, and its fields accept `'asc'` / `'desc'` or `{ direction, nulls }`.
* A `many` or many-to-many (`.through()`) relation takes `{ count: 'asc' | 'desc' }`.



**better-drizzle**

```ts  
const byAuthor = await client.posts.findMany({
	orderBy: [{ author: { name: 'asc' } }, { id: 'asc' }],
});

const mostActive = await client.users.findMany({
	orderBy: { posts: { count: 'desc' } },
	take: 10,
});
```

**Drizzle**

```ts  
import { asc, desc, eq, sql } from 'drizzle-orm';

const byAuthor = await db
	.select()
	.from(posts)
	.orderBy(
		asc(sql`(select ${users.name} from ${users} where ${eq(users.id, posts.authorId)} limit 1)`),
		asc(posts.id),
	);

const mostActive = await db
	.select()
	.from(users)
	.orderBy(desc(sql`(select count(*) from ${posts} where ${eq(posts.authorId, users.id)})`))
	.limit(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 `one` relation without a related record sorts as `NULL`. Use `nulls: 'first' | 'last'` to place these values; MySQL gets this through the same `IS NULL` term as scalar columns. A to-many `count` is `0` when no related rows match.
* Mix relation and scalar keys in the array form, and end with a unique key such as `id` so rows that tie on the relation value keep a stable order.
* Relation sorts work in `findMany`, `findFirst`, `findOne`, `findUnique`, and `paginate`, and inside nested `include` / `select` relation args, including ones with `take` / `skip`.
* A scalar sort on a to-many relation (`{ posts: { title: 'asc' } }`) or an aggregate `count` on a `one` relation is a compile error, and throws `INVALID_ARGS` for untyped callers. If the related table has a scalar column named `count`, a `one` relation 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 a `one` relation through a junction) cannot be sorted by either; they throw the same error as in `where` and `include`.

> **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()`](/docs/querying/explain).

For full offset pages with totals, use [`paginate`](/docs/querying/pagination). For feed-style navigation with cursors, use [`cursor`](/docs/querying/pagination).

## Throwing when a row is required

`findFirst`, `findOne`, and `findUnique` return a value that is both awaitable as `Row | null` **and** exposes `.throw()`:



**better-drizzle**

```ts  
// 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'));
```

**Drizzle**

```ts  
import { eq } from 'drizzle-orm';

// Treat not-found as null
const [maybeUser = null] = await db
	.select()
	.from(users)
	.where(eq(users.email, email))
	.limit(1);

// Treat not-found as exceptional
const [user] = await db
	.select()
	.from(users)
	.where(eq(users.email, email))
	.limit(1);
if (!user) throw new Error('User not found');
```

See [throwing results](/docs/writing/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          |
