Type-Safe Queries
Full TypeScript inference
Powerful Filters
Complex where clauses
Pagination
Limit, offset, and cursor
Relations
Joins and nested queries
Create (Insert)
Insert single or multiple records:
import { db } from '@/lib/db'
import { users, posts } from '@/lib/db/schema'
// Insert single record
const newUser = await db.insert(users).values({
email: 'user@example.com',
name: 'John Doe',
}).returning()
console.log('Created user:', newUser[0].id)
// Insert multiple records
const newPosts = await db.insert(posts).values([
{ title: 'First Post', authorId: newUser[0].id },
{ title: 'Second Post', authorId: newUser[0].id },
]).returning()
// Insert with conflict handling (upsert)
const upserted = await db.insert(users)
.values({ email: 'user@example.com', name: 'Updated Name' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Updated Name' },
})
.returning()Always Use returning()
Call
.returning() to get back the inserted rows with generated fields likeid and createdAt.Read (Select)
Query records with type-safe selects:
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
// Select all records
const allUsers = await db.select().from(users)
// Select specific columns
const emails = await db.select({
email: users.email,
name: users.name,
}).from(users)
// Select with alias
const userSummary = await db.select({
id: users.id,
displayName: users.name,
contact: users.email,
}).from(users)
// First result only
const firstUser = await db.select().from(users).limit(1)
// firstUser[0] or undefinedUpdate
Update records with conditions:
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
// Update single record
const updated = await db.update(users)
.set({ name: 'Jane Doe' })
.where(eq(users.id, userId))
.returning()
// Update multiple fields
await db.update(users)
.set({
name: 'New Name',
updatedAt: new Date(),
})
.where(eq(users.email, 'user@example.com'))
// Increment a counter
import { sql } from 'drizzle-orm'
await db.update(posts)
.set({ viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, postId))Always Add Where Clause
Forgetting the
.where() clause will update ALL records in the table!Delete
Remove records from the database:
import { db } from '@/lib/db'
import { users, posts } from '@/lib/db/schema'
import { eq, lt } from 'drizzle-orm'
// Delete single record
const deleted = await db.delete(users)
.where(eq(users.id, userId))
.returning()
// Delete with condition
await db.delete(posts)
.where(eq(posts.authorId, userId))
// Delete old records
await db.delete(sessions)
.where(lt(sessions.expiresAt, new Date()))Where Clauses & Filters
Available filter operators:
| Property | Type | Description |
|---|---|---|
eq | (column, value) | Equal to value |
ne | (column, value) | Not equal to value |
gt | (column, value) | Greater than value |
gte | (column, value) | Greater than or equal to value |
lt | (column, value) | Less than value |
lte | (column, value) | Less than or equal to value |
like | (column, pattern) | SQL LIKE pattern matching |
ilike | (column, pattern) | Case-insensitive LIKE |
isNull | (column) | Check if value is NULL |
isNotNull | (column) | Check if value is NOT NULL |
inArray | (column, values[]) | Value in array of values |
notInArray | (column, values[]) | Value not in array |
import { db } from '@/lib/db'
import { users, posts } from '@/lib/db/schema'
import { eq, and, or, like, gt, inArray, isNull } from 'drizzle-orm'
// Simple equality
const user = await db.select().from(users)
.where(eq(users.email, 'user@example.com'))
// AND conditions
const activeAdmins = await db.select().from(users)
.where(and(
eq(users.role, 'admin'),
eq(users.status, 'active')
))
// OR conditions
const results = await db.select().from(users)
.where(or(
eq(users.role, 'admin'),
eq(users.role, 'moderator')
))
// Pattern matching
const matchingUsers = await db.select().from(users)
.where(like(users.email, '%@company.com'))
// IN array
const specificUsers = await db.select().from(users)
.where(inArray(users.id, [id1, id2, id3]))
// Complex nested conditions
const complexQuery = await db.select().from(posts)
.where(and(
eq(posts.published, true),
or(
gt(posts.viewCount, 1000),
isNull(posts.deletedAt)
)
))Pagination
Paginate results with limit/offset or cursor-based pagination:
Offset Pagination
import { db } from '@/lib/db'
import { posts } from '@/lib/db/schema'
// Basic limit/offset pagination
const pageSize = 20
const page = 2 // 0-indexed
const results = await db.select().from(posts)
.limit(pageSize)
.offset(page * pageSize)
.orderBy(posts.createdAt)
// With total count for pagination UI
import { count } from 'drizzle-orm'
const [items, totalResult] = await Promise.all([
db.select().from(posts).limit(pageSize).offset(page * pageSize),
db.select({ count: count() }).from(posts),
])
const totalPages = Math.ceil(totalResult[0].count / pageSize)Cursor Pagination (Recommended)
import { db } from '@/lib/db'
import { posts } from '@/lib/db/schema'
import { gt, desc } from 'drizzle-orm'
// Cursor-based pagination (better for large datasets)
async function getPosts(cursor?: string, limit = 20) {
const query = db.select().from(posts)
.orderBy(desc(posts.createdAt))
.limit(limit + 1) // Fetch one extra to check if there's more
if (cursor) {
query.where(gt(posts.id, cursor))
}
const results = await query
const hasMore = results.length > limit
const items = hasMore ? results.slice(0, -1) : results
const nextCursor = hasMore ? items[items.length - 1].id : null
return { items, nextCursor, hasMore }
}Cursor vs Offset Pagination
Cursor pagination is more efficient for large datasets and handles insertions/deletions better. Offset pagination is simpler but can skip or duplicate items when data changes.
Ordering Results
Sort results by one or more columns:
import { db } from '@/lib/db'
import { posts } from '@/lib/db/schema'
import { asc, desc } from 'drizzle-orm'
// Single column ascending
const oldest = await db.select().from(posts)
.orderBy(asc(posts.createdAt))
// Single column descending
const newest = await db.select().from(posts)
.orderBy(desc(posts.createdAt))
// Multiple columns
const sorted = await db.select().from(posts)
.orderBy(
desc(posts.pinned), // Pinned first
desc(posts.createdAt) // Then by date
)
// Dynamic ordering
function getPosts(sortBy: 'date' | 'views', order: 'asc' | 'desc') {
const column = sortBy === 'date' ? posts.createdAt : posts.viewCount
const direction = order === 'asc' ? asc : desc
return db.select().from(posts).orderBy(direction(column))
}Selecting Specific Columns
Select only the columns you need for better performance:
import { db } from '@/lib/db'
import { users, posts } from '@/lib/db/schema'
// Select specific columns
const userList = await db.select({
id: users.id,
email: users.email,
}).from(users)
// Type: { id: string, email: string }[]
// Omit sensitive fields
const publicUsers = await db.select({
id: users.id,
name: users.name,
avatarUrl: users.avatarUrl,
// Exclude: email, passwordHash, etc.
}).from(users)
// Computed columns with sql
import { sql } from 'drizzle-orm'
const userStats = await db.select({
id: users.id,
name: users.name,
fullName: sql<string>`${users.firstName} || ' ' || ${users.lastName}`,
}).from(users)Joins
Join tables to fetch related data:
import { db } from '@/lib/db'
import { users, posts, comments } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
// Inner join
const postsWithAuthors = await db.select({
post: posts,
author: users,
}).from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
// Left join (includes posts without authors)
const allPosts = await db.select({
post: posts,
author: users,
}).from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
// Multiple joins
const postsWithDetails = await db.select({
post: posts,
author: users,
commentCount: sql<number>`count(${comments.id})`,
}).from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.leftJoin(comments, eq(comments.postId, posts.id))
.groupBy(posts.id, users.id)Relations (Query API)
Use Drizzle's relational query API for cleaner nested data:
src/lib/db/schema.ts
import { relations } from 'drizzle-orm'
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name'),
})
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
authorId: uuid('author_id').references(() => users.id),
})
// Define relations
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}))Using Relations
import { db } from '@/lib/db'
// Query with nested relations
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
})
// Type: { id, email, name, posts: Post[] }[]
// Select specific relation columns
const postsWithAuthor = await db.query.posts.findMany({
with: {
author: {
columns: {
id: true,
name: true,
},
},
},
})
// Nested relations with filters
const result = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 10,
},
},
})Relations vs Joins
The relational query API (
db.query) provides cleaner nested data structures. Use raw joins when you need more control or aggregate functions.