backend

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

June 24, 2026

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.1

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.2

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 live docs show 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.1 Here is a blog-style schema — users, posts, comments, and a posts_to_tags junction table for tags:

// 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:

// 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:3

// 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:

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.1 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:

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:

// ❌ 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 } }).1

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:1

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:

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.1 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.4 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:

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.)4

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.1 For more complex data-access shapes, our guide on atomic transactions with Drizzle ORM and pg-boss 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 shipping in drizzle-orm@beta (v1.0.0-beta.1 and higher); the stable 0.45.x line you install by default is v1.4 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. fieldsfrom, referencesto (each accepts a single column or an array), and relationNamealias.
  • 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). That's why the live documentation shows db._query — those pages document the v1.0 release candidate. On the stable 0.45.x release, v1 relational queries are still db.query.4

If you're on stable today, use db.query as shown throughout this guide. When you upgrade to v1.0, you can migrate query-by-query: old code keeps working via db._query while new code uses the redesigned db.query.

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 v1.0, watch the db.querydb._query swap.

Next steps: pair these reads with cursor pagination on Postgres and Node.js for large result sets, and add safe retries to your writes with idempotency keys in Node.js and Postgres.

Footnotes

  1. Drizzle ORM — Query (relational queries) documentation. https://orm.drizzle.team/docs/rqb 2 3 4 5 6 7 8 9

  2. 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

  3. Drizzle ORM — Database connection overview (driver-specific drizzle() import paths). https://orm.drizzle.team/docs/connect-overview

  4. Drizzle ORM — Relational Queries v1 to v2 migration guide (applies to drizzle-orm@beta, v1.0.0-beta.1+). https://orm.drizzle.team/docs/relations-v1-v2 2 3 4 5 6 7 8

Frequently Asked Questions

Call db.query.&lt;table&gt;.findMany({ with: { relationName: true } }) , and nest another with inside to go deeper. Drizzle returns one typed, nested result set from a single SQL statement. 1