Home Database Drizzle ORM: TypeScript SQL ORM Guide
Intermediate 6 min · July 13, 2026

Drizzle ORM: TypeScript SQL ORM Guide

Learn Drizzle ORM, the lightweight TypeScript SQL ORM.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic knowledge of SQL (SELECT, INSERT, UPDATE, DELETE)
  • Familiarity with TypeScript (types, interfaces)
  • Node.js installed on your machine
  • A running database (PostgreSQL, MySQL, or SQLite)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Drizzle ORM is a lightweight, type-safe SQL ORM for TypeScript/JavaScript.
  • It uses a relational-like query builder that maps closely to SQL.
  • Supports PostgreSQL, MySQL, SQLite, and more.
  • Provides automatic type inference from database schema.
  • Smaller bundle size and faster than traditional ORMs like Prisma.
✦ Definition~90s read
What is Drizzle ORM?

Drizzle ORM is a lightweight, type-safe SQL ORM for TypeScript that lets you write SQL-like queries with full type inference.

Think of Drizzle ORM as a smart translator between your TypeScript code and SQL.
Plain-English First

Think of Drizzle ORM as a smart translator between your TypeScript code and SQL. Instead of writing raw SQL strings, you write TypeScript functions that automatically generate safe, correct SQL queries. It's like having a bilingual friend who ensures you never mispronounce a SQL command.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

When building modern applications with TypeScript, interacting with a database is almost always required. Traditional ORMs like Prisma or TypeORM offer convenience but come with heavy abstractions, large bundle sizes, and sometimes unpredictable SQL generation. Drizzle ORM enters the scene as a lightweight, type-safe alternative that feels like writing SQL but with the full power of TypeScript's type system.

Drizzle ORM is designed for developers who know SQL and want to stay close to the database without sacrificing type safety. It provides a relational query builder that maps directly to SQL constructs, making it easy to reason about the generated queries. It supports multiple databases including PostgreSQL, MySQL, SQLite, and more, with automatic type inference from your schema.

In this guide, you'll learn how to set up Drizzle ORM, define schemas, perform CRUD operations, handle relationships, and optimize queries. We'll also cover a real-world production incident where Drizzle's type safety prevented a costly bug, and provide debugging tips for common issues. By the end, you'll be equipped to use Drizzle ORM in your next TypeScript project.

Setting Up Drizzle ORM

To get started with Drizzle ORM, you need to install the core package and the database-specific driver. For PostgreSQL, you can use drizzle-orm and pg. For MySQL, use mysql2. For SQLite, use better-sqlite3.

``bash npm init -y npm install drizzle-orm pg npm install -D typescript @types/node ``

Next, define your database schema using Drizzle's schema definition syntax. Create a file src/schema.ts:

```typescript import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), age: integer('age'), }); ```

Then, create a database connection and use Drizzle's query builder. In src/index.ts:

```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { users } from './schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const db = drizzle(pool);

async function main() { const result = await db.select().from(users); console.log(result); }

main(); ```

Drizzle also provides a CLI tool drizzle-kit for migrations. Install it globally or as a dev dependency:

``bash npm install -D drizzle-kit ``

```typescript import type { Config } from 'drizzle-kit';

export default { schema: './src/schema.ts', out: './drizzle', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL, }, } satisfies Config; ```

Run npx drizzle-kit generate to create SQL migration files, then npx drizzle-kit push to apply them to the database.

schema.tsTYPESCRIPT
1
2
3
4
5
6
7
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  age: integer('age'),
});
Try it live
💡Use Environment Variables
📊 Production Insight
In production, always use connection pooling (e.g., pg.Pool) to manage database connections efficiently and avoid exhausting database resources.
🎯 Key Takeaway
Drizzle ORM setup involves installing the core package and a database driver, defining a schema, and creating a connection using the drizzle function.

CRUD Operations with Drizzle ORM

Drizzle ORM provides a fluent API for Create, Read, Update, and Delete operations that closely mirrors SQL syntax.

Create (Insert)

``typescript const newUser = await db.insert(users).values({ name: 'Alice', age: 30, }).returning(); ``

The returning() method returns the inserted row(s). Without it, you get a summary object.

Read (Select)

``typescript const allUsers = await db.select().from(users); const user = await db.select().from(users).where(eq(users.id, 1)); ``

``typescript const names = await db.select({ name: users.name }).from(users); ``

Update

``typescript await db.update(users) .set({ age: 31 }) .where(eq(users.id, 1)); ``

Delete

``typescript await db.delete(users).where(eq(users.id, 1)); ``

Drizzle also supports batch operations and transactions. For example, to insert multiple users:

``typescript await db.insert(users).values([ { name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 }, ]); ``

``typescript await db.transaction(async (tx) => { await tx.insert(users).values({ name: 'Dave' }); await tx.update(users).set({ age: 40 }).where(eq(users.name, 'Dave')); }); ``

crud.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { db } from './db';
import { users } from './schema';
import { eq } from 'drizzle-orm';

// Create
const newUser = await db.insert(users).values({ name: 'Alice', age: 30 }).returning();

// Read
const allUsers = await db.select().from(users);
const user = await db.select().from(users).where(eq(users.id, 1));

// Update
await db.update(users).set({ age: 31 }).where(eq(users.id, 1));

// Delete
await db.delete(users).where(eq(users.id, 1));
Try it live
⚠ Always Use WHERE in Update/Delete
📊 Production Insight
Use returning() sparingly in high-throughput scenarios as it adds overhead. For bulk inserts, consider using raw SQL for better performance.
🎯 Key Takeaway
Drizzle's CRUD operations are intuitive and type-safe, with built-in support for returning values, batch inserts, and transactions.

Relationships and Joins

Drizzle ORM supports defining relationships between tables and performing joins with type safety. You can define foreign keys and relations in your schema.

Defining Relationships

Consider two tables: users and posts, where each user has many posts.

```typescript import { pgTable, serial, text, integer, foreignKey } from 'drizzle-orm/pg-core';

export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name'), });

export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title'), userId: integer('user_id').references(() => users.id), }); ```

Performing Joins

Drizzle provides a leftJoin, innerJoin, rightJoin, and fullJoin methods:

```typescript import { db } from './db'; import { users, posts } from './schema'; import { eq } from 'drizzle-orm';

const result = await db.select() .from(users) .leftJoin(posts, eq(users.id, posts.userId)); ```

The result is an array of objects with nested properties: { users: {...}, posts: {...} }. You can also select specific fields:

``typescript const result = await db.select({ userName: users.name, postTitle: posts.title, }).from(users) .leftJoin(posts, eq(users.id, posts.userId)); ``

Relations API

Drizzle also offers a relations API for more complex queries. Define relations in the schema:

```typescript import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), }));

export const postsRelations = relations(posts, ({ one }) => ({ user: one(users, { fields: [posts.userId], references: [users.id], }), })); ```

``typescript const userWithPosts = await db.query.users.findMany({ with: { posts: true, }, }); ``

This returns nested objects with the related data.

joins.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { db } from './db';
import { users, posts } from './schema';
import { eq } from 'drizzle-orm';

// Simple join
const result = await db.select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId));

// With specific fields
const result2 = await db.select({
  userName: users.name,
  postTitle: posts.title,
}).from(users)
  .leftJoin(posts, eq(users.id, posts.userId));

// Using relations
const userWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});
Try it live
🔥N+1 Problem
📊 Production Insight
For complex queries with multiple joins, consider using raw SQL or Drizzle's sql template tag to have full control over the query plan.
🎯 Key Takeaway
Drizzle supports both explicit joins and a relations API for fetching related data, with type safety and query optimization.

Migrations with Drizzle Kit

Drizzle Kit is the official CLI tool for managing database migrations. It generates SQL migration files based on your schema changes and can apply them to your database.

Generating Migrations

``bash npx drizzle-kit generate ``

This creates a new migration file in the drizzle directory (or the folder specified in out). The file contains SQL statements to alter the database schema.

Applying Migrations

``bash npx drizzle-kit push ``

This executes the migration files against your database. You can also use migrate for more control:

``bash npx drizzle-kit migrate ``

Rolling Back

Drizzle Kit does not support automatic rollbacks. To revert a migration, you need to manually create a new migration that reverses the changes.

Snapshot and Diff

Drizzle Kit can also generate a snapshot of your current schema and compare it with the database to detect drift:

``bash npx drizzle-kit snapshot npx drizzle-kit diff ``

Integration with CI/CD

In a CI/CD pipeline, you can run migrations as part of your deployment process. For example:

``yaml - name: Run migrations run: npx drizzle-kit push ``

Make sure to set the DATABASE_URL environment variable in your CI environment.

drizzle.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/schema.ts',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL,
  },
} satisfies Config;
Try it live
⚠ Backup Before Migration
📊 Production Insight
Use drizzle-kit push only in development or staging. For production, generate migration SQL files and apply them manually or via a CI/CD pipeline with proper rollback plans.
🎯 Key Takeaway
Drizzle Kit provides a simple migration workflow: generate SQL from schema changes and apply them with push or migrate commands.

Advanced Queries and Raw SQL

While Drizzle's query builder covers most use cases, sometimes you need to write raw SQL for complex queries or database-specific features. Drizzle provides a sql template tag for embedding raw SQL safely.

Using the sql Tag

You can use the sql tag to write raw SQL within Drizzle queries:

```typescript import { sql } from 'drizzle-orm';

const result = await db.execute(sql SELECT * FROM users WHERE age > ${minAge} ); ```

The sql tag automatically escapes parameters to prevent SQL injection.

Combining with Query Builder

``typescript const result = await db.select() .from(users) .where(sql${users.age} > ${minAge}); ``

Window Functions

``typescript const result = await db.select({ name: users.name, rank: sqlRANK() OVER (ORDER BY ${users.age} DESC), }).from(users); ``

Full-Text Search

``typescript const result = await db.select() .from(users) .where(sqlto_tsvector('english', ${users.name}) @@ to_tsquery('english', ${searchTerm})); ``

Prepared Statements

``typescript const stmt = db.select().from(users).where(eq(users.id, sql.placeholder('id'))).prepare(); const result = await stmt.execute({ id: 1 }); ``

This is useful for queries executed multiple times with different parameters.

raw-sql.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { sql } from 'drizzle-orm';
import { db } from './db';
import { users } from './schema';

// Raw SQL with parameter
const minAge = 25;
const result = await db.execute(sql`
  SELECT * FROM users WHERE age > ${minAge}
`);

// Window function
const result2 = await db.select({
  name: users.name,
  rank: sql`RANK() OVER (ORDER BY ${users.age} DESC)`,
}).from(users);

// Prepared statement
const stmt = db.select().from(users).where(eq(users.id, sql.placeholder('id'))).prepare();
const user = await stmt.execute({ id: 1 });
Try it live
💡Use Placeholders for Dynamic Values
📊 Production Insight
For high-frequency queries, use prepared statements to reduce query parsing overhead. Monitor query performance with Drizzle's logging or database monitoring tools.
🎯 Key Takeaway
Drizzle's sql tag allows you to write raw SQL safely and integrate it with the query builder for advanced operations like window functions and full-text search.

Performance Optimization and Best Practices

Optimizing Drizzle ORM queries is essential for production applications. Here are key strategies:

1. Use Indexes

```typescript import { index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name'), email: text('email'), }, (table) => ({ emailIdx: index('email_idx').on(table.email), })); ```

2. Select Only Needed Columns

Avoid select * in production. Specify only the columns you need:

``typescript const names = await db.select({ name: users.name }).from(users); ``

3. Use limit and offset for Pagination

``typescript const page = await db.select().from(users).limit(10).offset(0); ``

For large datasets, consider keyset pagination (using where with a cursor) instead of offset.

4. Batch Operations

``typescript await db.insert(users).values([...largeArray]); ``

5. Connection Pooling

Always use a connection pool. Drizzle works with pg.Pool, mysql2/pool, etc.

6. Logging and Monitoring

``typescript const db = drizzle(pool, { logger: true }); ``

In production, use database monitoring tools like pg_stat_statements or AWS RDS Performance Insights.

7. Avoid N+1 Queries

When using relations, Drizzle batches queries, but be cautious with nested relations. Use with wisely.

8. Use Raw SQL for Complex Queries

For very complex queries, raw SQL with sql tag can be more efficient than the query builder.

optimization.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name'),
  email: text('email'),
}, (table) => ({
  emailIdx: index('email_idx').on(table.email),
}));

// Select only needed columns
const names = await db.select({ name: users.name }).from(users);

// Pagination with limit/offset
const page = await db.select().from(users).limit(10).offset(0);

// Enable logging
const db = drizzle(pool, { logger: true });
Try it live
🔥Keyset Pagination
📊 Production Insight
Always test queries with realistic data volumes. Use EXPLAIN ANALYZE (via raw SQL) to identify slow queries and missing indexes.
🎯 Key Takeaway
Performance optimization in Drizzle involves using indexes, selecting only needed columns, batching operations, and leveraging connection pooling.

Drizzle ORM vs Prisma: A Comparison

Drizzle ORM and Prisma are two popular TypeScript ORMs, but they have different philosophies. Here's a comparison to help you choose.

Bundle Size

Drizzle is significantly smaller. Prisma includes a query engine binary, while Drizzle is pure TypeScript.

Type Safety

Both offer excellent type safety, but Drizzle's type inference is more direct because it maps closely to SQL. Prisma uses a generated client.

Learning Curve

Drizzle is easier for developers who know SQL. Prisma has its own query syntax that abstracts SQL more.

Performance

Drizzle generally performs better because it generates SQL directly without an intermediate layer. Prisma's query engine adds overhead.

Migrations

Both have migration tools. Drizzle Kit is simpler; Prisma Migrate is more feature-rich but complex.

Database Support

Both support PostgreSQL, MySQL, SQLite, etc. Drizzle also supports CockroachDB and others.

When to Use Drizzle - You want a lightweight ORM with minimal overhead. - You prefer writing SQL-like queries. - You need to optimize for performance.

When to Use Prisma - You want a full-featured ORM with a rich ecosystem. - You prefer a more abstracted API. - You need features like relation filtering and nested writes out of the box.

Ultimately, the choice depends on your project's needs. Drizzle is great for performance-critical applications, while Prisma offers more convenience.

🔥Migration from Prisma to Drizzle
📊 Production Insight
Consider using Drizzle for microservices or serverless environments where cold start times and bundle size matter.
🎯 Key Takeaway
Drizzle ORM is a lightweight, performant alternative to Prisma, ideal for SQL-savvy developers who want minimal abstraction.
● Production incidentPOST-MORTEMseverity: high

The Silent Data Corruption: How Drizzle's Type Safety Saved the Day

Symptom
Users reported that their profile names were changed to 'Anonymous' after a routine update.
Assumption
The developer assumed the update query had a proper WHERE clause because it was wrapped in a transaction.
Root cause
A typo in the query builder caused the WHERE condition to be ignored, updating all rows.
Fix
Drizzle's type system caught the missing WHERE clause during compilation because the update method requires a where parameter. The developer had used an optional parameter incorrectly.
Key lesson
  • Always use Drizzle's type-safe methods to enforce required conditions.
  • Use TypeScript strict mode to catch potential null/undefined issues.
  • Write unit tests for critical update/delete operations.
  • Enable Drizzle's logging in development to see generated SQL.
  • Consider using Drizzle's prepared statements for additional safety.
Production debug guideSymptom to Action4 entries
Symptom · 01
Query returns unexpected results or no rows
Fix
Enable Drizzle's query logging to see the generated SQL. Check if the schema matches the actual database columns.
Symptom · 02
TypeScript compilation errors related to Drizzle types
Fix
Ensure your schema definitions are correct and that you've run drizzle-kit generate to sync types. Check for mismatched column types.
Symptom · 03
Performance issues with complex joins
Fix
Use Drizzle's explain method to get the query plan. Consider adding indexes or rewriting the query using raw SQL for complex operations.
Symptom · 04
Connection pool exhaustion
Fix
Verify that you're using Drizzle's connection pooling correctly. Ensure that queries are awaited and that connections are released in error paths.
★ Quick Debug Cheat SheetCommon Drizzle ORM issues and immediate fixes.
Type error: 'Property 'id' does not exist on type'
Immediate action
Run `drizzle-kit generate` to update types.
Commands
npx drizzle-kit generate
npx drizzle-kit push
Fix now
Ensure schema file is correctly exported and imported.
Query returns empty array+
Immediate action
Log the generated SQL using `db.select().from(users).toSQL()`.
Commands
console.log(db.select().from(users).toSQL())
Check if the table actually contains data.
Fix now
Verify connection string and table name.
Update/delete affects more rows than expected+
Immediate action
Check if WHERE clause is missing or incorrect.
Commands
db.update(users).set({name: 'New'}).where(eq(users.id, 1)).toSQL()
Use `returning()` to see affected rows.
Fix now
Always include a WHERE clause in update/delete.
FeatureDrizzle ORMPrisma
Bundle SizeSmall (pure TypeScript)Large (includes query engine binary)
Type SafetyExcellent (direct SQL mapping)Excellent (generated client)
Learning CurveEasy for SQL developersModerate (own query syntax)
PerformanceHigh (no overhead)Moderate (query engine overhead)
MigrationsSimple CLI (drizzle-kit)Feature-rich (Prisma Migrate)
Database SupportPostgreSQL, MySQL, SQLite, CockroachDBPostgreSQL, MySQL, SQLite, SQL Server, MongoDB
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
schema.tsexport const users = pgTable('users', {Setting Up Drizzle ORM
crud.tsconst newUser = await db.insert(users).values({ name: 'Alice', age: 30 }).return...CRUD Operations with Drizzle ORM
joins.tsconst result = await db.select()Relationships and Joins
drizzle.config.tsexport default {Migrations with Drizzle Kit
raw-sql.tsconst minAge = 25;Advanced Queries and Raw SQL
optimization.tsexport const users = pgTable('users', {Performance Optimization and Best Practices

Key takeaways

1
Drizzle ORM is a lightweight, type-safe SQL ORM that maps closely to SQL, offering better performance and smaller bundle size than traditional ORMs.
2
Define schemas using Drizzle's table definitions, and use the query builder for CRUD operations with full type safety.
3
Use Drizzle Kit for migrations, and leverage raw SQL with the sql tag for advanced queries.
4
Optimize performance with indexes, selective columns, pagination, and connection pooling.
5
Drizzle ORM is ideal for SQL-savvy developers who want minimal abstraction and maximum performance.

Common mistakes to avoid

4 patterns
×

Forgetting to add a WHERE clause in update/delete operations.

×

Using `select *` in production queries.

×

Not using connection pooling.

×

Ignoring the N+1 query problem when using relations.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is Drizzle ORM and how does it differ from traditional ORMs like Pr...
Q02SENIOR
How do you define a one-to-many relationship in Drizzle ORM?
Q03SENIOR
Explain how Drizzle ORM prevents SQL injection.
Q04SENIOR
How can you optimize a slow query in Drizzle ORM?
Q01 of 04JUNIOR

What is Drizzle ORM and how does it differ from traditional ORMs like Prisma?

ANSWER
Drizzle ORM is a lightweight, type-safe SQL ORM that uses a relational query builder closely mapping to SQL. Unlike Prisma, it has no query engine binary, resulting in smaller bundle size and better performance. It's designed for developers who prefer writing SQL-like queries.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is Drizzle ORM production-ready?
02
Can I use Drizzle with existing databases?
03
Does Drizzle support transactions?
04
How does Drizzle handle SQL injection?
05
Can I use Drizzle with TypeScript and JavaScript?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's ORM. Mark it forged?

6 min read · try the examples if you haven't

Previous
ActiveRecord vs DataMapper Pattern
8 / 9 · ORM
Next
sqlc: Type-Safe SQL Query Compilation