Deleting data isn’t glamorous… but it’s one of those things you have to get right. One wrong query and suddenly half your table is gone.
Drizzle ORM steps in like a safety-first hero — giving you a type-safe, predictable, SQL-like API that makes deletes controlled, readable, and way less scary.
This guide walks you through everything you need to know about deleting data with Drizzle ORM — from simple deletes to advanced patterns using CTEs.
To follow along with these delete examples, you’ll want some sample data in your tables. If you haven’t added any yet, you can quickly insert records using the steps in Drizzle insertion tutorial.
Simple Delete
The most basic delete operation in Drizzle:
await db.delete(users);
This removes all records from the users table. Use it carefully — this is the “nuclear” option.
Use case:
You’re resetting a table in a dev environment or doing a scheduled cleanup where you know it’s safe to remove everything.
Conditional Delete ( where )
Almost always, you want to delete only specific rows. Here’s how:
await db.delete(users).where(eq(users.name, "Sharukhan"));
This deletes only those users whose name is "Sharukhan".
Use Case:
It prevents accidental data loss. You can be specific about which rows to delete.
Limit the Number of Deletes
You might not want to delete all matching rows — maybe just the first few. Drizzle supports .limit():
await db
.delete(users)
.where(eq(users.name, "Sharukhan"))
.limit(5);
This ensures that even if more than 5 users match, only 5 will be deleted.
Use Case:
Pruning old log entries or spam entries, but doing it in batches so you don’t accidentally clear everything.
Order Before Deleting
Deleting “first 2 matched” is risky unless you define which first. .orderBy() helps you control that:
await db
.delete(users)
.where(eq(users.name, "Sharukhan"))
.orderBy(asc(users.createdAt)); // delete the oldest first
await db
.delete(users)
.where(eq(users.name, "Sharukhan"))
.orderBy(desc(users.lastLogin)); // or delete the most recently active first
Use case:
- Remove oldest sessions
- Clean up the least recently used items
- Batch delete in a predictable way
Return Deleted Rows
Sometimes deleting is not enough — you want to know what got deleted.
const deleted = await db
.delete(users)
.where(eq(users.status, "inactive"))
.returning();
Or you can return only specific fields:
const deletedIds = await db
.delete(users)
.where(eq(users.status, "inactive"))
.returning({ id: users.id });
Why use this:
- Logging for audits
- Syncing deletions to another system
- Confirming what went wrong or right
- Building UI flows where you show what was removed
Advanced Delete with CTEs (with … delete)
If you need to compute something first — like an average or threshold — then delete based on that, you can use a Common Table Expression (CTE).
Imagine you’re storing user session logs.
For each user, you want to delete only those sessions that are older than their personal average session age — essentially clearing “stale” sessions but keeping normal ones.
Here’s how you can do that with a CTE:
// Step 1: Create a CTE that calculates each user's average session age
const avgSessionAge = db.$with("avg_session_age").as(
db
.select({
userId: sessions.userId,
avgAge: sql`avg(${sessions.duration})`.as("avgAge"),
})
.from(sessions)
.groupBy(sessions.userId)
);
// Step 2: Delete all sessions older than the user's average session age
const deletedSessions = await db
.with(avgSessionAge)
.delete(sessions)
.where(
sql`${sessions.duration} > (
select "avgAge"
from ${avgSessionAge}
where ${avgSessionAge}.userId = ${sessions.userId}
)`
)
.returning({
sessionId: sessions.id,
removedForUser: sessions.userId,
});
Use Case:
- Remove outlier or abnormal data (e.g. super high-value orders)
- Cleanup based on computed metrics
- Advanced business logic in cleanup jobs
Summary
In this guide, you learned how to safely and efficiently delete data using Drizzle ORM. You saw how simple deletes work, how to target specific rows with where(), and how to control deletion volume using limit() and orderBy() for predictable batch operations.
You also discovered how returning() helps you confirm exactly what was removed, and how advanced CTE-based deletes let you run powerful, computed conditions directly inside the database.
With these patterns, you can confidently handle everything from basic cleanup tasks to complex, data-driven delete workflows in your applications.