---
title: 'Drizzle ORM Relational Queries: db.query Guide (2026)'
description: 'Query nested relations in Drizzle ORM with db.query and the with operator: filter and limit child rows, run many-to-many joins, plus the v1 vs v2 changes.'
url: https://nerdleveltech.com/drizzle-orm-relational-queries-nested-with-tutorial
language: en
date: '2026-06-24'
category: backend
tags:
  - drizzle orm
  - relational queries
  - postgres
  - typescript
  - node.js
  - orm
  - backend
markdown_url: https://nerdleveltech.com/drizzle-orm-relational-queries-nested-with-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/drizzle-orm-relational-queries-nested-with-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Drizzle ORM Relational Queries: db.query Guide (2026)

**Drizzle ORM relational queries** let you read nested, related data in one type-safe call instead of writing manual joins and reshaping rows. You define relations with `relations()`, pass your full schema to `drizzle()`, then call `db.query.<table>.findMany({ with: { ... } })`. Drizzle emits a single SQL statement and returns properly nested objects.[^rqb]

## TL;DR

In the current stable release (`drizzle-orm@0.45.2`), relational queries live on `db.query`. Declare `relations()` for each table, initialize the client with `drizzle(client, { schema })`, and use the `with` operator to pull related rows — nested as deep as you need. You can filter, sort, and limit the **child** rows, but in v1 you cannot filter the **parent** rows by a child's column (that arrives in v2). Every query pattern in this guide was run on `drizzle-orm@0.45.2` against a real Postgres instance (PGlite) and type-checked with `tsc --noEmit`.[^versions]

## What you'll learn

- How to define relations and wire up `db.query` correctly
- How to query nested relations with the `with` operator (one-to-many, deep nesting)
- Why `db.query` is `undefined` — and the one-line fix
- How to select partial columns on parents and children
- How to filter, order, and limit child rows (and the v1 limitation on filtering parents)
- How to run a many-to-many query through a junction table
- When to use relational queries vs. a plain `select` + join
- What changes in Relational Queries v2 (`defineRelations`), and why the official docs already show v2 syntax on `db.query`

## Setup: define relations and pass the schema

Relational queries are an extension to Drizzle's query builder, so they need two things: a `relations()` declaration per table, and the **whole schema** handed to `drizzle()` at init.[^rqb] Here is a blog-style schema — users, posts, comments, and a `posts_to_tags` junction table for tags:

```typescript
// schema.ts
import { relations } from 'drizzle-orm';
import { pgTable, serial, text, integer, primaryKey } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  published: integer('published').notNull().default(0),
  authorId: integer('author_id').notNull().references(() => users.id),
});

export const comments = pgTable('comments', {
  id: serial('id').primaryKey(),
  body: text('body').notNull(),
  postId: integer('post_id').notNull().references(() => posts.id),
});

export const tags = pgTable('tags', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const postsToTags = pgTable('posts_to_tags', {
  postId: integer('post_id').notNull().references(() => posts.id),
  tagId: integer('tag_id').notNull().references(() => tags.id),
}, (t) => [primaryKey({ columns: [t.postId, t.tagId] })]);
```

Now declare the relations. The `one` side needs `fields` and `references`; the `many` side just names the target table:

```typescript
// schema.ts (continued)
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
  comments: many(comments),
  postsToTags: many(postsToTags),
}));

export const commentsRelations = relations(comments, ({ one }) => ({
  post: one(posts, { fields: [comments.postId], references: [posts.id] }),
}));

export const tagsRelations = relations(tags, ({ many }) => ({
  postsToTags: many(postsToTags),
}));

export const postsToTagsRelations = relations(postsToTags, ({ one }) => ({
  post: one(posts, { fields: [postsToTags.postId], references: [posts.id] }),
  tag: one(tags, { fields: [postsToTags.tagId], references: [tags.id] }),
}));
```

Finally, initialize the database with the schema imported as a namespace. The exact `drizzle` import and arguments depend on your driver, but the shape is the same — you must pass `schema`:[^connect]

```typescript
// db.ts
import { drizzle } from 'drizzle-orm/node-postgres'; // or /pglite, /postgres-js, etc.
import * as schema from './schema';

export const db = drizzle(client, { schema });
```

## How do I query nested relations in Drizzle ORM?

To query nested relations, call `db.query.<table>.findMany()` (or `findFirst()`) and pass a `with` object naming each relation you want included. To go deeper, nest another `with` inside. This returns users, each with their posts, each post with its comments:

```typescript
const usersWithPostsAndComments = await db.query.users.findMany({
  with: {
    posts: {
      with: {
        comments: true,
      },
    },
  },
});
// usersWithPostsAndComments[0].posts[0].comments -> Comment[]
```

Drizzle generates exactly **one** SQL statement for this (using lateral joins of subqueries under the hood) and returns fully typed, nested objects — no manual row mapping.[^rqb] In the verified run, the first user's first post returned an array of two comments, and the result type carries through every level of nesting.

`findFirst()` is the same API with `LIMIT 1` applied, returning a single object instead of an array:

```typescript
const onePost = await db.query.posts.findFirst({
  with: { comments: true },
});
```

## Why is db.query undefined in Drizzle ORM?

`db.query` is `undefined` (or empty) when you don't pass your `schema` — including its `relations()` exports — to `drizzle()`. Relational queries are built entirely from that schema object, so without it there is nothing to attach to `db.query`. The fix is one line:

```typescript
// ❌ db.query is empty — no schema
const db = drizzle(client);

// ✅ db.query.<table> now exists
import * as schema from './schema';
const db = drizzle(client, { schema });
```

Make sure your namespace import (`import * as schema`) picks up **both** the tables and the `relations()` declarations from the same file(s). If you split schema across files, spread them all: `drizzle(client, { schema: { ...schema1, ...schema2 } })`.[^rqb]

## How do I select only certain columns?

Use the `columns` option to include or exclude fields, on the parent and on any nested relation. Drizzle pushes this into the SQL, so unselected columns are never transferred:[^rqb]

```typescript
const posts = await db.query.posts.findMany({
  columns: { id: true, title: true },
  with: {
    author: { columns: { name: true } },
  },
});
// each row: { id, title, author: { name } }
```

When you mix `true` and `false` keys, the `false` entries are ignored — listing any `true` column means everything else is excluded automatically. To drop a single field instead, use only `false`: `columns: { content: false }`.

## How do I filter, order, and limit the child rows?

You can apply `where`, `orderBy`, `limit`, and `offset` to the top-level query, and `where`, `orderBy`, and `limit` to nested relations. This fetches one user, then only their **published** posts, newest first, capped at one:

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

const user = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      where: eq(posts.published, 1),
      orderBy: [desc(posts.id)],
      limit: 1,
    },
  },
});
```

Two limitations are worth knowing in v1. First, **`offset` works only on the top-level query**, not on nested relations.[^rqb] Second — and this trips people up — a nested `where` filters which **child** rows come back, not which **parents** do. There is no way in v1 to say "return only users who have a post matching X" through the relational API; filtering the top-level rows by a related table's column is a v2 feature.[^v1v2] If you need that today on the stable release, drop to a `select()` with an explicit join, or filter in a subquery.

One reassuring detail: when nothing matches, Drizzle returns an **empty array**, not `null`. A parent with no children comes back with `children: []`, and a query with no matching rows returns `[]`. Both were confirmed in the verified run.

## How do I run a many-to-many query in Drizzle ORM?

In v1 there is no dedicated many-to-many helper, so you query **through** the junction table: include the junction relation, blank out its own columns with `columns: {}`, and nest the far-side relation inside it. This returns a post with a flat-ish path to its tags:

```typescript
const post = await db.query.posts.findFirst({
  where: eq(posts.id, 1),
  with: {
    postsToTags: {
      columns: {}, // omit junction columns from the result
      with: {
        tag: true,
      },
    },
  },
});

// tag names:
const tagNames = post?.postsToTags.map((pt) => pt.tag.name); // ['orm', 'sql']
```

The `columns: {}` keeps the junction rows out of your payload while still letting you reach `tag`. The verified run returned `['orm', 'sql']` for a post linked to two tags. (Relational Queries v2 replaces this whole pattern with a single `through` declaration — see below.)[^v1v2]

## Relational queries vs. select + join: which should I use?

Use **relational queries** (`db.query`) when you want nested objects shaped like your domain — a user with posts with comments — and you're reading data. Use the **SQL-like builder** (`db.select()` with `leftJoin`) when you need flat rows, aggregations (`count`, `sum`), set operations, or filtering/sorting parents by a joined column that v1's relational API can't express. They share the same schema and can live side by side; relational queries are an opt-in layer on top of the core builder, not a replacement.[^rqb] For more complex data-access shapes, our guide on [atomic transactions with Drizzle ORM and pg-boss](https://nerdleveltech.com/drizzle-orm-pg-boss-atomic-transactions-tutorial) shows the core builder in a write-heavy workflow.

## What is the difference between Drizzle relational queries v1 and v2?

Relational Queries **v2** is a redesign for the upcoming `drizzle-orm` v1.0; it has progressed from `drizzle-orm@beta` (v1.0.0-beta.1 and higher) to `drizzle-orm@rc` (v1.0.0-rc.1 and higher as of this writing), with `drizzle-orm@beta` still tracking a newer beta build in parallel. Neither has shipped as the stable `latest` release yet — the stable `0.45.x` line you install by default is still **v1**.[^v1v2] The headline changes:

- **One place for relations.** v2 replaces per-table `relations()` calls with a single `defineRelations(schema, (r) => ({ ... }))`, and you pass `{ relations }` to `drizzle()` instead of `{ schema }`.
- **Renamed keys.** `fields` → `from`, `references` → `to` (each accepts a single column or an array), and `relationName` → `alias`.
- **Filter parents by relations.** v2 adds object-style `where`/`orderBy` and lets you filter top-level rows by a related table's columns — the exact thing v1 can't do.
- **Native many-to-many.** A `through` helper removes the junction-table boilerplate entirely.
- **The accessor swap.** This is the big gotcha: in v2 the clean `db.query` is **reassigned** to the new syntax, and the old v1 API moves to `db._query` (with `relations` imported from `drizzle-orm/_relations`). On the stable `0.45.x` release, v1 relational queries are still `db.query` — the syntax in this guide. The official docs have already moved their main [Query](https://orm.drizzle.team/docs/rqb) page over to v2 syntax (`db.query` with `defineRelations` and object-style `where`/`orderBy`), even though `drizzle-orm` v1.0 itself is still on `drizzle-orm@rc` and hasn't shipped as the stable `latest` package yet — so some dialect-specific pages still show the older `db._query` form from earlier in the migration.[^v1v2]

If you're on stable today, use `db.query` as shown throughout this guide — the v1 syntax with `relations()` and callback-style `where`. When you install `drizzle-orm@rc` (or a later `@beta`), you get the v2 API on `db.query` and your old v1-style queries keep working via `db._query` during the migration window.

## FAQ

**How do I query nested relations in Drizzle ORM?**
Call `db.query.<table>.findMany({ with: { relationName: true } })`, and nest another `with` inside to go deeper. Drizzle returns one typed, nested result set from a single SQL statement.[^rqb]

**Why is `db.query` undefined?**
You didn't pass `schema` to `drizzle()`. Relational queries are built from the schema object, so use `drizzle(client, { schema })` with a namespace import that includes your `relations()` exports.

**Can I filter parent rows by a child's column in v1?**
No. In v1 a nested `where` filters only the included child rows, not the parents. Filtering parents by a related table is a Relational Queries v2 feature; on stable, use `db.select()` with a join instead.[^v1v2]

**Does a missing relation return null or an empty array?**
An empty array. A parent with no children returns `children: []`, and a query with no matches returns `[]` — verified on `drizzle-orm@0.45.2`.

**How do I do a many-to-many query in v1?**
Query through the junction table: include the junction relation with `columns: {}` and nest the far-side relation (e.g. `tag: true`) inside it.[^v1v2]

**Is `db._query` the same as `db.query`?**
Only in v2. On stable `0.45.x`, `db._query` is `undefined` and `db.query` is the relational API. On `drizzle-orm@rc`/`@beta` (the pre-v1.0 packages) they swap: `db._query` runs the old v1 syntax and `db.query` runs v2.[^v1v2]

**Can I use `limit` and `offset` on nested relations?**
`limit` and `orderBy` work on nested relations in v1; `offset` works only on the top-level query.[^rqb] v2 adds nested `offset`.[^v1v2]

## Bottom line

Drizzle ORM relational queries give you nested, typed reads with `db.query` and the `with` operator, while keeping the SQL-like builder available for joins and aggregations. On the stable `0.45.x` release, define `relations()`, pass your full `schema` to `drizzle()`, and remember that v1 filters children but not parents. When you move to `drizzle-orm@rc`/`v1.0`, watch the `db.query` → `db._query` swap.

Next steps: pair these reads with [cursor pagination on Postgres and Node.js](https://nerdleveltech.com/cursor-pagination-postgres-nodejs-tutorial) for large result sets, and add safe retries to your writes with [idempotency keys in Node.js and Postgres](https://nerdleveltech.com/idempotency-keys-nodejs-postgres-api-tutorial).

[^rqb]: Drizzle ORM — Query (relational queries) documentation. https://orm.drizzle.team/docs/rqb
[^versions]: drizzle-orm `0.45.2` (npm latest, June 2026); verified against Postgres via `@electric-sql/pglite` `0.5.3` and type-checked with `tsc --noEmit` (strict, nodenext). https://www.npmjs.com/package/drizzle-orm
[^connect]: Drizzle ORM — Database connection overview (driver-specific `drizzle()` import paths). https://orm.drizzle.team/docs/connect-overview
[^v1v2]: Drizzle ORM — Relational Queries v1 to v2 migration guide (applies to the pre-v1.0 `drizzle-orm@beta` / `drizzle-orm@rc` packages, v1.0.0-beta.1 and later, now including v1.0.0-rc.1+). https://orm.drizzle.team/docs/relations-v1-v2
