Skip to content

Drizzle Advanced Patterns (80-20)#

Advanced queries, optimizations, and real-world patterns.

Complex Queries#

Subqueries#

import { sql, gt } from 'drizzle-orm'

// Find users with more than 10 posts
const activeUsers = await db
  .select()
  .from(users)
  .where(
    gt(
      sql`(SELECT COUNT(*) FROM ${posts} WHERE ${posts.userId} = ${users.id})`,
      10
    )
  )

Window Functions#

// Rank users by post count
const rankedUsers = await db.execute(sql`
  SELECT 
    ${users.id},
    ${users.name},
    COUNT(${posts.id}) as post_count,
    RANK() OVER (ORDER BY COUNT(${posts.id}) DESC) as rank
  FROM ${users}
  LEFT JOIN ${posts} ON ${users.id} = ${posts.userId}
  GROUP BY ${users.id}, ${users.name}
`)

Aggregate Queries#

import { count, avg, sum, max, min } from 'drizzle-orm'

// Group stats
const stats = await db
  .select({
    userId: posts.userId,
    totalPosts: count(posts.id),
    avgLength: avg(posts.contentLength),
    totalViews: sum(posts.views),
  })
  .from(posts)
  .groupBy(posts.userId)

Advanced Joins#

Multiple Joins#

const usersWithPostsAndComments = await db
  .select({
    user: users,
    post: posts,
    comment: comments,
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .leftJoin(comments, eq(posts.id, comments.postId))

Self Join#

const employees = pgTable('employees', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  managerId: integer('manager_id'),
})

// Get employee with manager
const employeesWithManager = await db
  .select({
    employee: employees,
    manager: sql`manager`,
  })
  .from(employees)
  .leftJoin(
    sql`${employees} as manager`,
    sql`${employees.managerId} = manager.id`
  )

Batch Operations#

Bulk Insert#

// Insert many records efficiently
const users = Array.from({ length: 1000 }, (_, i) => ({
  name: `User ${i}`,
  email: `user${i}@example.com`,
}))

// Insert in batches of 100
for (let i = 0; i < users.length; i += 100) {
  await db.insert(users).values(users.slice(i, i + 100))
}

Upsert (Insert or Update)#

// PostgreSQL
await db
  .insert(users)
  .values({ id: 1, name: 'John', email: 'john@example.com' })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: 'John Updated' },
  })

// MySQL
await db
  .insert(users)
  .values({ id: 1, name: 'John', email: 'john@example.com' })
  .onDuplicateKeyUpdate({
    set: { name: 'John Updated' },
  })

Prepared Statements#

// Prepare once, execute many times (faster)
const getUserById = db
  .select()
  .from(users)
  .where(eq(users.id, sql.placeholder('id')))
  .prepare('get_user_by_id')

// Execute
const user1 = await getUserById.execute({ id: 1 })
const user2 = await getUserById.execute({ id: 2 })

Query Builders#

Dynamic Filters#

function getUsers(filters: {
  search?: string
  minAge?: number
  isActive?: boolean
}) {
  const conditions = []

  if (filters.search) {
    conditions.push(like(users.name, `%${filters.search}%`))
  }

  if (filters.minAge !== undefined) {
    conditions.push(gte(users.age, filters.minAge))
  }

  if (filters.isActive !== undefined) {
    conditions.push(eq(users.isActive, filters.isActive))
  }

  return db
    .select()
    .from(users)
    .where(conditions.length > 0 ? and(...conditions) : undefined)
}

// Usage
const users1 = await getUsers({ search: 'John', minAge: 18 })
const users2 = await getUsers({ isActive: true })

Dynamic Sorting#

import { asc, desc } from 'drizzle-orm'

function getUsers(sortBy?: string, order: 'asc' | 'desc' = 'asc') {
  const orderFn = order === 'asc' ? asc : desc

  let query = db.select().from(users)

  if (sortBy === 'name') {
    query = query.orderBy(orderFn(users.name))
  } else if (sortBy === 'createdAt') {
    query = query.orderBy(orderFn(users.createdAt))
  }

  return query
}

Pagination#

Offset-based#

async function getPaginatedUsers(page: number, pageSize: number = 10) {
  const offset = (page - 1) * pageSize

  const [users, totalCount] = await Promise.all([
    db.select().from(users).limit(pageSize).offset(offset),
    db.select({ count: count() }).from(users),
  ])

  return {
    data: users,
    pagination: {
      page,
      pageSize,
      totalPages: Math.ceil(totalCount[0].count / pageSize),
      totalCount: totalCount[0].count,
    },
  }
}

Cursor-based (better performance)#

async function getCursorPaginatedUsers(
  cursor?: string,
  limit: number = 10
) {
  const query = db
    .select()
    .from(users)
    .limit(limit + 1)
    .orderBy(desc(users.id))

  if (cursor) {
    query.where(lt(users.id, parseInt(cursor)))
  }

  const results = await query

  const hasMore = results.length > limit
  const data = hasMore ? results.slice(0, -1) : results
  const nextCursor = hasMore ? data[data.length - 1].id.toString() : null

  return { data, nextCursor, hasMore }
}

Optimistic Locking#

const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title'),
  version: integer('version').default(0).notNull(),
})

async function updatePost(id: number, title: string, version: number) {
  const result = await db
    .update(posts)
    .set({ 
      title, 
      version: sql`${posts.version} + 1` 
    })
    .where(
      and(
        eq(posts.id, id),
        eq(posts.version, version)
      )
    )
    .returning()

  if (result.length === 0) {
    throw new Error('Post was modified by another user')
  }

  return result[0]
}

Soft Deletes#

const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name'),
  deletedAt: timestamp('deleted_at'),
})

// Soft delete
async function softDeleteUser(id: number) {
  return await db
    .update(users)
    .set({ deletedAt: new Date() })
    .where(eq(users.id, id))
}

// Get active users only
async function getActiveUsers() {
  return await db
    .select()
    .from(users)
    .where(isNull(users.deletedAt))
}

// Restore
async function restoreUser(id: number) {
  return await db
    .update(users)
    .set({ deletedAt: null })
    .where(eq(users.id, id))
}

Indexes#

import { index, uniqueIndex } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: text('email').notNull(),
  name: text('name'),
  createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
  emailIdx: uniqueIndex('email_idx').on(table.email),
  nameIdx: index('name_idx').on(table.name),
  createdAtIdx: index('created_at_idx').on(table.createdAt),
}))

Full-Text Search (PostgreSQL)#

import { sql } from 'drizzle-orm'

// Add tsvector column
const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title'),
  content: text('content'),
  searchVector: sql`tsvector`,
})

// Create index
await db.execute(sql`
  CREATE INDEX posts_search_idx ON ${posts}
  USING GIN (search_vector)
`)

// Update trigger for auto-updating search_vector
await db.execute(sql`
  CREATE TRIGGER posts_search_update
  BEFORE INSERT OR UPDATE ON ${posts}
  FOR EACH ROW
  EXECUTE FUNCTION
    tsvector_update_trigger(search_vector, 'pg_catalog.english', title, content);
`)

// Search
const searchResults = await db
  .select()
  .from(posts)
  .where(
    sql`${posts.searchVector} @@ to_tsquery('english', ${query})`
  )
  .orderBy(
    sql`ts_rank(${posts.searchVector}, to_tsquery('english', ${query})) DESC`
  )

Computed Columns#

const users = pgTable('users', {
  id: serial('id').primaryKey(),
  firstName: text('first_name'),
  lastName: text('last_name'),
  fullName: text('full_name').generatedAlwaysAs(
    sql`${users.firstName} || ' ' || ${users.lastName}`
  ),
})

JSON Columns#

const users = pgTable('users', {
  id: serial('id').primaryKey(),
  metadata: jsonb('metadata').$type<{
    preferences: {
      theme: 'light' | 'dark'
      language: string
    }
    settings: Record<string, any>
  }>(),
})

// Query
const darkThemeUsers = await db
  .select()
  .from(users)
  .where(
    sql`${users.metadata}->>'preferences'->>'theme' = 'dark'`
  )

// Update
await db
  .update(users)
  .set({
    metadata: {
      preferences: { theme: 'dark', language: 'en' },
      settings: {},
    },
  })
  .where(eq(users.id, 1))

Connection Pooling#

import { Pool } from 'pg'
import { drizzle } from 'drizzle-orm/node-postgres'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
})

export const db = drizzle(pool)

Repository Pattern#

class UserRepository {
  constructor(private db: typeof db) {}

  async findById(id: number) {
    const result = await this.db
      .select()
      .from(users)
      .where(eq(users.id, id))
    return result[0]
  }

  async findByEmail(email: string) {
    const result = await this.db
      .select()
      .from(users)
      .where(eq(users.email, email))
    return result[0]
  }

  async create(data: NewUser) {
    const result = await this.db
      .insert(users)
      .values(data)
      .returning()
    return result[0]
  }

  async update(id: number, data: Partial<NewUser>) {
    const result = await this.db
      .update(users)
      .set(data)
      .where(eq(users.id, id))
      .returning()
    return result[0]
  }

  async delete(id: number) {
    await this.db
      .delete(users)
      .where(eq(users.id, id))
  }
}

export const userRepository = new UserRepository(db)

Performance Tips

  • Use prepared statements for repeated queries
  • Add indexes on frequently queried columns
  • Use cursor pagination for large datasets
  • Batch insert operations
  • Use connection pooling in production
  • Monitor slow queries with EXPLAIN