Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/drizzle-team/drizzle-orm/llms.txt

Use this file to discover all available pages before exploring further.

Drizzle ORM provides powerful support for subqueries, allowing you to nest queries within other queries for complex data retrieval and manipulation.

Basic Subquery

Create and use a subquery:
import { db } from './db';
import { users, posts } from './schema';
import { eq } from 'drizzle-orm';

// Create a subquery
const activeUsers = db.select()
  .from(users)
  .where(eq(users.active, true))
  .as('active_users');

// Use the subquery
const result = await db.select()
  .from(activeUsers);

Subquery in WHERE Clause

Use subqueries for filtering:
import { sql, inArray } from 'drizzle-orm';

// Users who have posts
const usersWithPosts = await db.select()
  .from(users)
  .where(
    sql`${users.id} IN (
      SELECT DISTINCT ${posts.userId} FROM ${posts}
    )`
  );

// Or using inArray with subquery
const userIds = db.select({ id: posts.userId })
  .from(posts)
  .as('user_ids');

Subquery in SELECT

Include subquery results in select:
import { sql, eq } from 'drizzle-orm';

// Select with correlated subquery
const result = await db.select({
  id: users.id,
  name: users.name,
  postCount: sql<number>`(
    SELECT COUNT(*)
    FROM ${posts}
    WHERE ${posts.userId} = ${users.id}
  )`,
})
  .from(users);

Common Table Expressions (CTE)

Use WITH clauses for readable complex queries:
import { sql } from 'drizzle-orm';

// Define CTE
const activeUsers = db.$with('active_users').as(
  db.select({
    id: users.id,
    name: users.name,
  })
    .from(users)
    .where(eq(users.active, true))
);

// Use CTE in query
const result = await db.with(activeUsers)
  .select()
  .from(activeUsers);

Subquery Joins

Join with subquery results:
import { sql, eq, desc } from 'drizzle-orm';

// Get users with their latest post
const latestPosts = db.select({
  userId: posts.userId,
  title: posts.title,
  createdAt: posts.createdAt,
  rowNum: sql<number>`ROW_NUMBER() OVER (PARTITION BY ${posts.userId} ORDER BY ${posts.createdAt} DESC)`.as('row_num'),
})
  .from(posts)
  .as('latest_posts');

const result = await db.select({
  userName: users.name,
  latestPostTitle: latestPosts.title,
})
  .from(users)
  .innerJoin(latestPosts, eq(users.id, latestPosts.userId))
  .where(eq(latestPosts.rowNum, 1));

Correlated Subqueries

Subqueries that reference outer query:
import { sql, gt } from 'drizzle-orm';

// Users with above-average post count
const result = await db.select({
  id: users.id,
  name: users.name,
  postCount: sql<number>`(
    SELECT COUNT(*)
    FROM ${posts}
    WHERE ${posts.userId} = ${users.id}
  )`,
})
  .from(users)
  .where(
    sql`(
      SELECT COUNT(*)
      FROM ${posts}
      WHERE ${posts.userId} = ${users.id}
    ) > (
      SELECT AVG(post_count)
      FROM (
        SELECT COUNT(*) as post_count
        FROM ${posts}
        GROUP BY ${posts.userId}
      ) as counts
    )`
  );

Subquery with Aggregations

Combine subqueries with aggregate functions:
import { count, sum, avg, sql } from 'drizzle-orm';

// Department statistics
const departmentStats = db.select({
  department: employees.department,
  avgSalary: avg(employees.salary),
  employeeCount: count(),
})
  .from(employees)
  .groupBy(employees.department)
  .as('dept_stats');

const result = await db.select({
  department: departmentStats.department,
  avgSalary: departmentStats.avgSalary,
  employeeCount: departmentStats.employeeCount,
  aboveCompanyAvg: sql<boolean>`${departmentStats.avgSalary} > (
    SELECT AVG(${employees.salary}) FROM ${employees}
  )`,
})
  .from(departmentStats);

Lateral Joins (PostgreSQL)

Use LATERAL for more powerful correlated subqueries:
import { sql, desc } from 'drizzle-orm';

// Get each user with their 3 most recent posts
const recentPosts = db.select({
  id: posts.id,
  title: posts.title,
  createdAt: posts.createdAt,
})
  .from(posts)
  .where(eq(posts.userId, users.id))
  .orderBy(desc(posts.createdAt))
  .limit(3)
  .as('recent_posts');

const result = await db.select({
  userId: users.id,
  userName: users.name,
  postTitle: recentPosts.title,
})
  .from(users)
  .leftJoinLateral(recentPosts, sql`true`);

Subquery in FROM Clause

Use subquery as a data source:
import { sql } from 'drizzle-orm';

// Aggregate over grouped results
const userPostCounts = db.select({
  userId: posts.userId,
  postCount: count(),
})
  .from(posts)
  .groupBy(posts.userId)
  .as('user_post_counts');

const result = await db.select({
  avgPostsPerUser: avg(userPostCounts.postCount),
  maxPostsPerUser: max(userPostCounts.postCount),
})
  .from(userPostCounts);

Nested Subqueries

Subqueries within subqueries:
import { sql } from 'drizzle-orm';

// Multi-level nesting
const topUsers = db.select({
  userId: posts.userId,
  postCount: count(),
})
  .from(posts)
  .groupBy(posts.userId)
  .having(({ postCount }) => gt(postCount, 10))
  .as('top_users');

const topUsersWithDetails = db.select({
  id: users.id,
  name: users.name,
  postCount: topUsers.postCount,
})
  .from(users)
  .innerJoin(topUsers, eq(users.id, topUsers.userId))
  .as('top_users_details');

const result = await db.select()
  .from(topUsersWithDetails)
  .where(gt(topUsersWithDetails.postCount, 20));

Scalar Subqueries

Subqueries returning a single value:
import { sql } from 'drizzle-orm';

// Compare to scalar value from subquery
const result = await db.select()
  .from(products)
  .where(
    sql`${products.price} > (
      SELECT AVG(${products.price}) FROM ${products}
    )`
  );

Insert from Subquery

Insert data from a select query:
import { sql } from 'drizzle-orm';

// Archive old records
await db.insert(archivedUsers)
  .select(
    db.select()
      .from(users)
      .where(lt(users.lastLoginAt, sql`now() - interval '1 year'`))
  );

Update with Subquery

Update using subquery results:
import { sql } from 'drizzle-orm';

// Update with calculated value
await db.update(users)
  .set({
    postCount: sql`(
      SELECT COUNT(*)
      FROM ${posts}
      WHERE ${posts.userId} = ${users.id}
    )`,
  });

Delete with Subquery

Delete based on subquery:
import { sql, inArray } from 'drizzle-orm';

// Delete users who haven't posted
await db.delete(users)
  .where(
    notExists(
      db.select().from(posts).where(eq(posts.userId, users.id))
    )
  );

Union with Subqueries

Combine multiple subquery results:
// Combine different user types
const admins = db.select({
  id: users.id,
  name: users.name,
  type: sql<string>`'admin'`,
})
  .from(users)
  .where(eq(users.role, 'admin'))
  .as('admins');

const moderators = db.select({
  id: users.id,
  name: users.name,
  type: sql<string>`'moderator'`,
})
  .from(users)
  .where(eq(users.role, 'moderator'))
  .as('moderators');

const result = await db.select().from(admins)
  .unionAll(db.select().from(moderators));

Type Safety

Subqueries maintain full type safety:
const activeUsers = db.select({
  id: users.id,
  name: users.name,
  email: users.email,
})
  .from(users)
  .where(eq(users.active, true))
  .as('active_users');

const result = await db.select()
  .from(activeUsers);

// TypeScript knows the shape:
// result: Array<{
//   id: number;
//   name: string;
//   email: string;
// }>

Performance Tips

1

Use CTEs for Readability

CTEs make complex queries easier to understand and maintain
2

Index Subquery Columns

Ensure columns used in subquery joins and WHERE clauses are indexed
3

Limit Subquery Results

Use LIMIT in subqueries when you don’t need all rows
4

Consider Materialized CTEs

For expensive subqueries used multiple times, consider materialized views
5

Test Query Plans

Use EXPLAIN to understand how the database executes your subqueries

Common Patterns

Top N per Group

const ranked = db.select({
  ...posts,
  rank: sql`ROW_NUMBER() OVER (
    PARTITION BY ${posts.userId} 
    ORDER BY ${posts.createdAt} DESC
  )`,
})
  .from(posts)
  .as('ranked');

await db.select()
  .from(ranked)
  .where(eq(ranked.rank, 1));

Deduplication

await db.delete(users)
  .where(sql`id NOT IN (
    SELECT MIN(id)
    FROM ${users}
    GROUP BY email
  )`);

Running Totals

const sales = db.select({
  date: orders.date,
  amount: orders.amount,
  runningTotal: sql`SUM(${orders.amount}) OVER (
    ORDER BY ${orders.date}
  )`,
})
  .from(orders);

Pivot Tables

const pivot = db.select({
  category: products.category,
  q1Sales: sum(sql`CASE WHEN quarter = 1 THEN amount END`),
  q2Sales: sum(sql`CASE WHEN quarter = 2 THEN amount END`),
})
  .from(products)
  .groupBy(products.category);

Next Steps

Joins

Combine subqueries with joins

Aggregations

Use aggregations in subqueries

Select Queries

Master the select query builder

Performance

Optimize subquery performance