DevLift
Back to Blog

Prisma vs Drizzle ORM: Schema-First Abstraction vs SQL-Native Control

Prisma 7 eliminated the Rust binary and closed the performance gap. So why are teams still choosing Drizzle? The real answer is about SQL transparency, bundle size, and who owns complexity.

Admin
August 5, 202610 min read2 views

Prisma vs Drizzle ORM: Schema-First Abstraction vs SQL-Native Control

You're spinning up a new Next.js project. You open a fresh terminal, run npm create next-app, pick TypeScript, and then hit the question everyone hits about five minutes later: Prisma or Drizzle?

Three years ago this was a non-debate — Prisma was just the TypeScript ORM. But Drizzle's rise, Prisma 7's architectural overhaul, and the explosion of serverless and edge deployments have made this a genuinely hard call. The two ORMs are going in different directions philosophically, and picking the wrong one will cost you.

Versions this is written against: prisma/@prisma/client 7.9.1, drizzle-orm 0.45.2, drizzle-kit 0.31.10. Prisma 7.0.0 shipped on 2025-11-19, so anything you read about Prisma's architecture from before then is describing a different product. Where I say "I ran it," I ran it — against a live PostgreSQL 17.6 for the Prisma side (on Prisma 6.19, which is what the codebase I had in front of me pins) and a scratch project for Drizzle.

Let me save you a weekend of experimentation.

Decision Matrix

ScenarioPick
Team prefers schema-as-documentation, visual toolingPrisma
Deploying to edge functions / Cloudflare WorkersDrizzle
SQL-heavy queries, joins, window functionsDrizzle
Rapid prototyping, want batteries includedPrisma
Bundle size is critical (lambda cold starts)Drizzle
Multi-schema, complex migrationsPrisma
Raw SQL comfort level is lowPrisma
Building a SaaS with multi-tenant DB patternsDrizzle
Renaming columns without losing dataDrizzle (it asks; Prisma emits DROP + ADD)

The Core Philosophical Split

These two ORMs have different answers to the same question: how much should the ORM hide from you?

Prisma says: quite a lot. You write a .prisma schema file in a custom DSL, run prisma generate, and get a fully typed client. The client API is deliberately high-level — you think in terms of models and relations, not tables and joins. Note that "no codegen" is the one thing Prisma will never offer you: the generate step is load-bearing, and in v7 the generator block now requires an output path because the client is no longer written into node_modules by default.

Drizzle says: nothing, really. Your schema lives in TypeScript files. Your queries look like SQL. There's no generation step, no binary, no separate language to learn. If you know SQL, you already know 80% of Drizzle.

Neither answer is wrong. They're just optimizing for different things.

Schema Definition

This is where you feel the difference immediately.

Prisma uses its own schema language (Prisma Schema Language, PSL):

// schema.prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}
 
model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
}

The DSL is readable. Anyone on your team can open schema.prisma and understand your data model without knowing TypeScript. Relations are explicit. After running npx prisma generate, you get a fully typed PrismaClient.

Drizzle does all of this in TypeScript:

// src/db/schema.ts
import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
 
export const users = pgTable("users", {
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
  email: text("email").notNull().unique(),
  name: text("name"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
 
export const posts = pgTable("posts", {
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
  title: text("title").notNull(),
  content: text("content"),
  published: boolean("published").default(false).notNull(),
  authorId: text("author_id")
    .notNull()
    .references(() => users.id),
  createdAt: timestamp("created_at").defaultNow().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] }),
}));

More verbose? Yes. But it's just TypeScript — you can use variables, loops, and helpers to build your schema programmatically. And there's zero codegen: the schema file is your source of truth.

Query API

This is the starkest difference between the two.

Prisma gives you a high-level, relational API:

// Fetch user with posts
const user = await prisma.user.findUnique({
  where: { email: "alice@example.com" },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: "desc" },
      take: 5,
    },
  },
});
 
// Create with nested relation
const newPost = await prisma.post.create({
  data: {
    title: "Hello World",
    author: { connect: { id: userId } },
  },
});

Clean, intent-driven, and the return types are exactly what you'd expect. user.posts is typed as Post[]. No manual type assertions.

Drizzle mirrors SQL:

import { db } from "@/db";
import { users, posts } from "@/db/schema";
import { eq, desc, and } from "drizzle-orm";
 
// Fetch user with posts via join
const result = await db
  .select({
    user: users,
    post: posts,
  })
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id))
  .where(
    and(eq(users.email, "alice@example.com"), eq(posts.published, true))
  )
  .orderBy(desc(posts.createdAt))
  .limit(5);
 
// Or use Relational Query Builder (closer to Prisma's style)
const userWithPosts = await db.query.users.findFirst({
  where: eq(users.email, "alice@example.com"),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
});

Drizzle actually has two query modes: the SQL-builder (explicit joins) and the Relational Query Builder (db.query.*). The RQB is closer to Prisma's API. The SQL builder gives you full control — useful for complex aggregations, window functions, and CTEs that ORMs typically mangle.

Both blocks above type-check clean under strict on drizzle-orm 0.45.2, which is the point of the library. What neither the compiler nor Drizzle will tell you is that the first one is not the same query as the second. eq(posts.published, true) sits in the WHERE, so the LEFT JOIN collapses into an inner join and a user with no published posts drops out of the result entirely — and .limit(5) caps joined rows, not posts per user. Move the predicate into the join condition if you meant "the user, plus up to five published posts." This is the tax on SQL transparency: you get exactly the query you wrote.

Bundle Size and Edge Runtime

This is where Drizzle wins decisively — for now.

Prisma 7 ditched the Rust query engine binary that made Prisma infamous in serverless circles. Prisma's own upgrade guide calls the new prisma-client provider "the new Rust-free client," and it delivers: I generated a client from the schema above and there is no .node binary anywhere under @prisma/client — the output is 184 KB of plain TypeScript. What replaces the engine at runtime is a WebAssembly query compiler, shipped inside the npm package as base64 embedded in a .js file.

"Rust-free client" is exact wording, though, and worth reading closely: the CLI still ships a native binary. @prisma/engines in my install contains a 21.9 MB schema-engine-linux-arm64-openssl-3.0.x, which is what runs your migrations. It's a devDependency and it doesn't reach production, but if your mental model was "Prisma 7 deleted the binaries," it deleted the one that was in the request path.

The size claims on both sides of this comparison get repeated a lot without units or a method, so I measured them the same way for both — one esbuild --bundle --minify of a real module, with the database driver marked external, then gzip:

drizzle-orm 0.45.2, esbuild --bundle --minify --external:pg
  drizzle-orm/pg-core only (schema, no driver)      32.1 KB min    7.7 KB min+gzip
  the schema + both query modes from this article   83.4 KB min   21.6 KB min+gzip
 
@prisma/client 7.9.1, postgres, files as shipped in the package
  runtime/client.mjs                                  183 KB        58 KB gzip
  query_compiler_fast_bg.postgresql.wasm             3.68 MB      1.16 MB gzip
  query_compiler_small_bg.postgresql.wasm            1.85 MB       690 KB gzip

So the widely quoted "Drizzle is ~7KB" is real, and it is the floor: pg-core on its own, meaning schema definitions with nothing connected to them. A module that actually talks to Postgres measured 21.6 KB min+gzip for me. That is still a different order of magnitude from Prisma, but it's 3x the marketing number, and if you're picking an ORM on bundle size you should be comparing the thing you'll deploy.

On the Prisma side, the generated client I produced points at the fast compiler variant (getRuntime imports query_compiler_fast_bg.postgresql), which is the 1.16 MB gzipped one. The smaller variant ships in the same package at 690 KB gzipped. Either way it's a WASM module that has to be fetched, decoded from base64 and instantiated, so it belongs in your cold-start budget rather than your bundle-size spreadsheet. I don't have Lambda numbers for you and I'm not going to invent any — measure it on your own runtime, because the answer depends on whether that module is cached between invocations.

💡

The bigger v7 change for edge and serverless is that a driver adapter is now mandatory. Prisma's upgrade guide: "The way to create a new Prisma Client has changed to require a driver adapter for all databases." You pass new PrismaPg({ ... }) (or @prisma/adapter-better-sqlite3, etc.) to the constructor, and url in the datasource block is no longer supported at all — I tried it and got P1012: The datasource property 'url' is no longer supported in schema files. Connection URLs move to prisma.config.ts. Accelerate is now a distinct path with its own prisma+postgres:// URL and its own extension, not a prerequisite for running outside Node.

Rendering diagram...

Migrations

Prisma has the more mature workflow. It does not have the safer one, and the difference is column renames.

prisma migrate dev generates SQL migration files from schema diffs, uses a shadow database to detect drift, separates migrate dev from migrate deploy, and has years of edge cases ironed out. The workflow is:

# Edit schema.prisma, then:
npx prisma migrate dev --name add_user_avatar

Drizzle Kit's equivalent:

# Edit schema.ts, then:
npx drizzle-kit generate
npx drizzle-kit migrate

Now rename a column, because that's the operation that separates them. I renamed name to fullName on both sides.

Prisma 6.19.2, via prisma migrate diff on the two schemas:

-- AlterTable
ALTER TABLE "User" DROP COLUMN "name",
ADD COLUMN     "fullName" TEXT;

Drizzle Kit 0.31.10, on the same rename:

Is full_name column in users table created or renamed from another column?
❯ + full_name        create column
  ~ name › full_name rename column

Prisma does not detect renames. It never has, and it doesn't ask — it emits DROP COLUMN plus ADD COLUMN and the data in that column is gone. This isn't obscure: Prisma's own docs say that renaming a field produces a migration that will "CREATE a new column" and "DROP the existing column (for example, name) and the data in that column," and the documented fix is to hand-edit the generated SQL into a RENAME COLUMN before you apply it. Which works, and is fine, as long as somebody notices.

Drizzle Kit asks. Pick the second option and you get ALTER TABLE ... RENAME COLUMN. There's a real caveat, and I found it by piping /dev/null into it: with no TTY to prompt on, drizzle-kit generate throws and writes no migration at all. In CI that's a broken build rather than silent data loss — fail-closed, but it does mean the rename step is a human-in-the-loop operation you can't fully automate.

So: for teams with complex schema evolutions, multi-tenant setups, or people who shouldn't be eyeballing raw SQL, Prisma's tooling is more complete and the shadow-database drift detection has no Drizzle equivalent. Just don't confuse "more mature" with "won't drop your column." Review every generated migration on both sides.

Relations and N+1

Both ORMs handle relations, but they're solving different problems.

Prisma's include/select API is designed to avoid N+1 automatically, and it does — but not in the way most people assume. I turned on query logging against a live Postgres 17.6 with Prisma 6.19.2 and watched what a two-level include actually emits:

prisma.course.findMany({ take: 2, include: { chapters: true } })
  BEGIN
  SELECT ... FROM "public"."courses" ...
  SELECT ... FROM "public"."chapters" ...
  COMMIT
 
prisma.course.findMany({ take: 2, include: { chapters: { include: { lessons: true } } } })
  BEGIN
  SELECT ... FROM "public"."courses" ...
  SELECT ... FROM "public"."chapters" ...
  SELECT ... FROM "public"."lessons" ...
  COMMIT

One SELECT per relation level, wrapped in a transaction, stitched together in the client. That's not N+1 — it doesn't scale with row count — but it isn't one query either, and each level is a round trip. If you want a real join you have to ask: relationLoadStrategy: "join" uses LATERAL JOINs with JSON aggregation on Postgres. It's still behind the relationJoins preview flag, which is why my probe took the query path without me choosing it. Worth knowing before you conclude from a slow trace that Prisma is "doing something weird" — it's doing exactly what its default strategy says.

Drizzle's RQB makes the same class of decision, but the SQL builder makes you state it. When you drop to raw joins you own the query shape completely — including the result grouping:

// Raw join result: flat array of {user, post} rows
// You need to group them manually
const grouped = result.reduce(
  (acc, row) => {
    const userId = row.user.id;
    if (!acc[userId]) acc[userId] = { ...row.user, posts: [] };
    if (row.post) acc[userId].posts.push(row.post);
    return acc;
  },
  {} as Record<string, typeof users.$inferSelect & { posts: typeof posts.$inferSelect[] }>
);

Not hard. But it's the kind of thing Prisma handles for you and Drizzle expects you to handle yourself.

Tooling and Ecosystem

Prisma Studio is genuinely useful. npx prisma studio spins up a web GUI where you can browse, filter, edit, and delete rows directly. Not a replacement for a real DB client, but it's saved me from installing TablePlus on countless staging environments.

Drizzle has Drizzle Studio, which covers the same ground and ships updates frequently.

Documentation: both are good. Prisma's docs are more thorough with more real-world examples. Drizzle's docs assume you know SQL well enough to fill in the gaps.

Community/ecosystem: Prisma has a larger user base, more community packages, and better Stack Overflow coverage. Drizzle is growing fast — create-t3-app documents it as a first-class option alongside Prisma, so it's a default-path choice rather than a bolt-on — but the ecosystem is younger.

One thing to check before you npm install: the latest tag on npm is still drizzle-orm@0.45.2, but there's a 1.0.0-rc.4 sitting on the rc tag. Everything in this article was run against 0.45.2. If you're starting a project that will outlive this quarter, look at what 1.0 changes before you write a lot of schema.

Prisma 7: Has It Changed the Game?

Honest answer: yes, significantly. The removal of the Rust binary eliminates the single biggest reason teams were switching to Drizzle. No more platform-specific binary in your deployment artifact, no more binaryTargets in your schema because you built on a Mac and deployed to Alpine.

But Drizzle still wins on:

  • Shipped size (21.6 KB gzipped for a working module, against 58 KB plus a 690 KB-1.16 MB WASM compiler)
  • SQL transparency (you always know exactly what query runs, and its default isn't a preview flag away from something else)
  • Schema-as-TypeScript (no codegen, no .prisma file, no generate step to forget in CI)
  • Renames that ask before they drop

Prisma 7 closed the gap. It didn't eliminate it.

When to Use Prisma

  • Your team is more comfortable with data modeling than SQL
  • You want prisma studio, prisma migrate, and full documentation out of the box
  • You're deploying to traditional serverless (Lambda, Vercel Functions) and you've measured that instantiating a ~1 MB WASM module per cold start is acceptable
  • You need shadow-database drift detection and a separate migrate deploy step for production
  • You're building something fast and want the ORM to handle query optimization

When to Use Drizzle

  • You're deploying to edge runtimes where every KB matters
  • Your queries are SQL-heavy (reporting, analytics, complex aggregations)
  • You want full visibility into every query that hits your database — no magic
  • You're using Neon, Turso, or PlanetScale and want their HTTP drivers
  • Your schema needs programmatic generation (multi-tenant, dynamic tables)

When to Use Both (Sort Of)

Some teams use Prisma for migrations only — maintaining the schema and generating SQL files — while using Drizzle for runtime queries. This is niche but it works: you get Prisma's battle-tested migration workflow and Drizzle's lightweight runtime. The schema duplication is the main downside.


The comparison will keep shifting. Prisma 7 was a major recalibration. Drizzle's ecosystem is growing. But the underlying philosophy is stable: Prisma is for teams who want the ORM to manage complexity, and Drizzle is for teams who want to manage it themselves.

Pick based on where your team sits on that spectrum, not on which one has more GitHub stars this week.

Comments (0)

No comments yet. Be the first to share your thoughts!

Related Articles

One file to guard every route — plus the Next.js 16 rename that moves it off the Edge runtime, the request-vs-response header trap that leaks user IDs to the browser, and the CVE that explains why this can never be your only auth layer.
AdminAugust 3, 20268 min read
Redis does more; Memcached does one thing faster. Here's exactly when that trade-off matters — plus why Valkey is now the open-source default in 2026.
AdminAugust 5, 202614 min read
Client-side validation is UX. Server-side validation is security. And a schema is not an auth guard — here's the fix that still ships an account takeover, and how to catch it in review.
AdminAugust 5, 20269 min read