Drizzle ORM
Drizzle ORM cheat sheet with schema definition, queries, relations, migrations, and TypeScript-first database management examples.
Database Setup
Connect Drizzle ORM to PostgreSQL, MySQL, or SQLite using the unified drizzle() API.
Connect to PostgreSQL using node-postgres or postgres.js drivers.
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';
// Connection string — recommended
const db = drizzle(process.env.DATABASE_URL!, { schema });Connect to MySQL using mysql2 driver.
import { drizzle } from 'drizzle-orm/mysql2';
import * as schema from './schema';
const db = drizzle(process.env.DATABASE_URL!, { schema });Connect to SQLite using better-sqlite3 or libsql drivers.
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';
const db = drizzle('./sqlite.db', { schema });Configure drizzle-kit for migrations and studio.
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './drizzle',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Schema Definition
Define tables, columns, types, and constraints using Drizzle's type-safe schema builders.
Define a table with common column types and constraints.
import { pgTable, serial, text, integer,
boolean, timestamp, varchar } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: varchar('email', { length: 255 }).unique().notNull(),
age: integer('age'),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});Define PostgreSQL enums and use them in table schemas.
import { pgTable, serial, text,
pgEnum } from 'drizzle-orm/pg-core';
export const roleEnum = pgEnum('role', [
'admin', 'user', 'moderator'
]);
export const statusEnum = pgEnum('status', [
'active', 'inactive', 'suspended'
]);
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
role: roleEnum('role').default('user').notNull(),
status: statusEnum('status').default('active').notNull(),
});Add indexes, unique constraints, and composite keys to tables.
import { pgTable, serial, text, integer,
timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
slug: text('slug').unique().notNull(),
authorId: integer('author_id').notNull(),
category: text('category'),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ([
index('author_idx').on(table.authorId),
uniqueIndex('slug_idx').on(table.slug),
index('category_author_idx')
.on(table.category, table.authorId),
]));Define foreign key references between tables.
import { pgTable, serial, text, integer,
timestamp } 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(),
authorId: integer('author_id')
.references(() => users.id, {
onDelete: 'cascade',
})
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});Extract TypeScript types from your Drizzle schema definitions.
import type { InferSelectModel,
InferInsertModel } from 'drizzle-orm';
import { users } from './schema';
// Infer the SELECT type (what you read from DB)
type User = InferSelectModel<typeof users>;
// Infer the INSERT type (what you write to DB)
type NewUser = InferInsertModel<typeof users>;🔗 Relations
Define relationships between tables for Drizzle's relational query builder.
Define a one-to-many relationship between users and posts.
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
integer } 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(),
authorId: integer('author_id').notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));Define a one-to-one relationship between users and profiles.
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
export const profiles = pgTable('profiles', {
id: serial('id').primaryKey(),
bio: text('bio'),
avatarUrl: text('avatar_url'),
userId: integer('user_id').unique().notNull(),
});
export const usersRelations = relations(users, ({ one }) => ({
profile: one(profiles),
}));
export const profilesRelations = relations(profiles, ({ one }) => ({
user: one(users, {
fields: [profiles.userId],
references: [users.id],
}),
}));Define a many-to-many relationship using a junction table.
import { relations } from 'drizzle-orm';
import { pgTable, serial, text,
integer, primaryKey } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
});
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: text('name').unique().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] }),
]));
export const postsRelations = relations(posts, ({ many }) => ({
postsToTags: many(postsToTags),
}));
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],
}),
}));Use the relational query builder to fetch nested data.
// Fetch users with their posts
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});New unified relations builder — replaces per-table `relations()` calls
import { defineRelations } from 'drizzle-orm'
import * as schema from './schema'
export const relations = defineRelations(schema, (r) => ({
users: {
posts: r.many.posts(),
profile: r.one.profiles({
from: r.users.id,
to: r.profiles.userId,
}),
},
posts: {
author: r.one.users({
from: r.posts.authorId,
to: r.users.id,
}),
tags: r.many.tags({
from: r.posts.id.through(r.postTags.postId),
to: r.tags.id.through(r.postTags.tagId),
}),
},
}))
// Query (the v2 relational API)
const db = drizzle(client, { schema, relations })
const user = await db.query.users.findFirst({
where: { id: 1 },
with: { posts: { with: { tags: true } } },
})Select Queries
Build type-safe SELECT queries with filters, ordering, and pagination.
Select all or specific columns from a table.
import { eq } from 'drizzle-orm';
// Select all columns
const allUsers = await db.select().from(users);
// Select specific columns
const names = await db.select({
id: users.id,
name: users.name,
}).from(users);Filter queries with comparison and logical operators.
import { eq, ne, gt, gte, lt, lte, like, ilike,
and, or, not, inArray, notInArray,
between, isNull, isNotNull } from 'drizzle-orm';
// Equality
await db.select().from(users).where(eq(users.id, 1));
// Comparison
await db.select().from(users).where(gt(users.age, 18));
// Pattern matching (case-insensitive)
await db.select().from(users)
.where(ilike(users.name, '%john%'));
// Logical AND / OR
await db.select().from(users).where(
and(
eq(users.isActive, true),
or(
gte(users.age, 18),
eq(users.role, 'admin')
)
)
);Sort results and implement pagination with limit/offset.
import { asc, desc } from 'drizzle-orm';
// Order by single column
await db.select().from(users)
.orderBy(asc(users.name));
// Order by multiple columns
await db.select().from(users)
.orderBy(desc(users.createdAt), asc(users.name));
// Pagination with limit and offset
const page = 2;
const pageSize = 20;
await db.select().from(users)
.orderBy(desc(users.createdAt))
.limit(pageSize)
.offset((page - 1) * pageSize);✏️ Insert / Update / Delete
Mutate data with type-safe insert, update, and delete operations.
Insert single or multiple rows with optional returning clause.
// Insert single row
await db.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
});
// Insert with returning
const [newUser] = await db.insert(users).values({
name: 'Bob',
email: 'bob@example.com',
}).returning();Insert or update on conflict using onConflictDoUpdate or onConflictDoNothing.
// Upsert — update on conflict
await db.insert(users).values({
email: 'alice@example.com',
name: 'Alice Updated',
}).onConflictDoUpdate({
target: users.email,
set: { name: 'Alice Updated' },
});Update existing rows with type-safe set and where clauses.
import { eq } from 'drizzle-orm';
// Update single field
await db.update(users)
.set({ name: 'Alice Smith' })
.where(eq(users.id, 1));
// Update with returning
const [updated] = await db.update(users)
.set({ isActive: false })
.where(eq(users.id, 1))
.returning();Delete rows from tables with type-safe conditions.
import { eq, lt, and } from 'drizzle-orm';
// Delete by ID
await db.delete(users).where(eq(users.id, 1));
// Delete with returning
const [deleted] = await db.delete(users)
.where(eq(users.id, 1))
.returning();
// Delete with complex condition
await db.delete(users).where(
and(
eq(users.isActive, false),
lt(users.createdAt, new Date('2024-01-01'))
)
);🔗 Joins
Combine data from multiple tables using SQL-level joins.
Return rows that have matching values in both tables.
import { eq } from 'drizzle-orm';
const result = await db.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name,
}).from(posts)
.innerJoin(users, eq(posts.authorId, users.id));Return all rows from one or both tables, even without matches.
import { eq } from 'drizzle-orm';
// Left join — all users, even without posts
const result = await db.select({
userName: users.name,
postTitle: posts.title, // nullable!
}).from(users)
.leftJoin(posts, eq(users.id, posts.authorId));📊 Aggregations
Perform aggregate calculations with groupBy and having clauses.
Use count, sum, avg, min, and max for data aggregation.
import { eq, count, sum, avg, min, max } from 'drizzle-orm';
// Count all rows
const [{ total }] = await db.select({
total: count(),
}).from(users);
// Sum
const [{ totalRevenue }] = await db.select({
totalRevenue: sum(orders.amount),
}).from(orders);Group results and filter aggregated data.
import { eq, gt, count } from 'drizzle-orm';
// Group by with count
const postsByAuthor = await db.select({
authorId: posts.authorId,
postCount: count(),
}).from(posts)
.groupBy(posts.authorId);
// Having — filter groups
const prolificAuthors = await db.select({
authorId: posts.authorId,
postCount: count(),
}).from(posts)
.groupBy(posts.authorId)
.having(gt(count(), 5));🔒 Transactions
Execute multiple operations atomically with transaction support.
Wrap multiple queries in an ACID transaction that auto-rolls back on error.
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
}).returning();
await tx.insert(profiles).values({
userId: user.id,
bio: 'Hello world',
});
return user;
});Use nested transactions that create savepoints for partial rollback.
await db.transaction(async (tx) => {
await tx.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
});
// Nested transaction — creates a SAVEPOINT
try {
await tx.transaction(async (tx2) => {
await tx2.insert(posts).values({
title: 'First Post',
authorId: 1,
});
// This will rollback only the nested tx
throw new Error('Oops');
});
} catch {
// Nested tx rolled back, outer tx continues
console.log('Nested transaction failed');
}
// This still commits — Alice is created
await tx.insert(profiles).values({
userId: 1,
bio: 'No posts yet',
});
});Prepared Statements & Raw SQL
Optimize query performance with prepared statements and raw SQL expressions.
Pre-compile queries for repeated execution with different parameters.
import { eq, placeholder } from 'drizzle-orm';
// Create a prepared statement
const getUserById = db.select()
.from(users)
.where(eq(users.id, placeholder('id')))
.prepare('get_user_by_id');
// Execute with different params
const user1 = await getUserById.execute({ id: 1 });
const user2 = await getUserById.execute({ id: 2 });Use raw SQL expressions and build dynamic queries conditionally.
import { sql, eq } from 'drizzle-orm';
// Raw SQL query
const result = await db.execute(
sql`SELECT * FROM users WHERE id = ${userId}`
);📦 Migrations
Manage database schema changes with drizzle-kit CLI commands.
Generate SQL migration files from schema changes and apply them.
# Generate migration from schema diff
npx drizzle-kit generate
# Apply pending migrations
npx drizzle-kit migrate
# Push schema directly (no migration files)
npx drizzle-kit push
# Open Drizzle Studio (DB GUI)
npx drizzle-kit studioTypical development workflow with schema changes and migrations.
# 1. Edit your schema file (e.g., src/db/schema.ts)
# Add/modify tables, columns, indexes
# 2. Generate a migration
npx drizzle-kit generate
# Creates: drizzle/0001_add_user_table.sql
# 3. Review the generated SQL
cat drizzle/0001_add_user_table.sql
# 4. Apply the migration
npx drizzle-kit migrate
# 5. Check current schema status
npx drizzle-kit check
# 6. Drop a migration (if needed before applying)
npx drizzle-kit dropRun migrations from your application code at startup.
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
const db = drizzle(process.env.DATABASE_URL!);
// Run migrations on app startup
await migrate(db, {
migrationsFolder: './drizzle',
});
console.log('Migrations complete');When to use push (fast iteration) vs generate + migrate (versioned, prod-safe)
# Fast iteration — diff schema and push to the DB directly
npx drizzle-kit push # no SQL files, no history; great for local/dev
# Versioned migrations — generate SQL, commit it, then apply
npx drizzle-kit generate # writes drizzle/<timestamp>_<name>.sql
npx drizzle-kit migrate # applies pending migration files
# Other useful kit commands
npx drizzle-kit studio # browse + edit data at localhost:4983
npx drizzle-kit check # verify migration files are consistent
npx drizzle-kit up # rewrite drizzle metadata after manual edits
# DETAILED_TAB:
# ===== drizzle-kit push =====
# What it does: diffs your schema.ts against the live DB and runs the
# resulting ALTERs immediately. No SQL files written. No history.
#
# Use when:
# - Local dev where the schema is still in flux
# - A throwaway preview branch
# - You don't need an audit trail of how the schema evolved
#
# Don't use in production — there's no replayable record of changes.
npx drizzle-kit push
npx drizzle-kit push --verbose # show every SQL statement
npx drizzle-kit push --strict # confirm potentially destructive ops
# ===== drizzle-kit generate + migrate =====
# What it does:
# generate → diffs schema.ts and writes a numbered .sql file
# into your migrations folder
# migrate → applies all unapplied files in order, tracked in a
# __drizzle_migrations table
#
# This is the production workflow. Commit the SQL files to git so
# every deploy sees the same migration history.
# drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: { url: process.env.DATABASE_URL! },
})
# Workflow
npx drizzle-kit generate --name=add_posts_table # writes 0001_add_posts_table.sql
git add drizzle/
git commit -m 'migration: add posts table'
npx drizzle-kit migrate # apply locally / in CI
# Programmatic migrate (e.g. in a CI step or app startup)
import { migrate } from 'drizzle-orm/node-postgres/migrator'
await migrate(db, { migrationsFolder: './drizzle' })
# ===== Hybrid pattern that works for most teams =====
# - Local dev: npx drizzle-kit push for fast iteration
# - Before PR: npx drizzle-kit generate to capture the final shape
# - CI / prod: npx drizzle-kit migrate from the generated SQL files