Skip to content
Blog

ORMs in 2026: Prisma, Drizzle, and Raw SQL

Type-safe queries, migrations, and the cases where raw SQL still wins. A practical comparison of Prisma, Drizzle, and writing SQL by hand.

Published on August 10, 2026

AI Assistant

There’s a lie at the heart of the “ORM vs. raw SQL” debate: that they’re opposite approaches. They’re not. An ORM is a layer that turns your database into typed, code-first shapes; raw SQL is a layer that keeps the database’s own language verbatim. The real question in 2026 is which abstraction pays for itself — and the honest answer depends on the shape of your queries and your tolerance for ceremony.

In this post, you will learn how Prisma and Drizzle model data differently, where each shines, and the cases where raw SQL (or a thin query builder) is still the right call.

Prisma: schema-first, declarative, battle-tested

Prisma’s model is schema-first: you declare your data model in schema.prisma, and Prisma generates a fully typed client plus migrations. Your schema is the single source of truth.

// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Queries read like English and come back fully typed:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const author = await prisma.user.findUnique({
  where: { email: "ada@example.com" },
  include: { posts: { where: { published: true } } },
});

const published = await prisma.post.findMany({
  where: { published: true, author: { name: { not: null } } },
  orderBy: { createdAt: "desc" },
  take: 10,
});

Prisma’s strengths: an extremely readable query API, automatic relation includes, first-class migrations, and ecosystem maturity. Its costs: the generated-client layer, an additional abstraction between you and SQL, and occasionally a query shape that’s awkward to express without falling back to raw.

// When Prisma's API fights you, it hands you escape hatches
const rows = await prisma.$queryRaw`
  SELECT date_trunc('month', "createdAt") AS month, count(*)
  FROM "Post"
  WHERE published = true
  GROUP BY 1
  ORDER BY 1 DESC
`;

Prisma also evolved a full stack around the ORM — Prisma Postgres (a managed DB) and Prisma Compute (hosting) — so in 2026 “Prisma” increasingly means “managed Postgres + typed client,” not just the mapper.

Drizzle: SQL-typed, composable, thin

Drizzle takes the opposite philosophy: you write SQL-shaped code, typed in TypeScript, with no heavy client layer. It’s a lightweight, composable query builder that stays close to the database’s own semantics.

import { pgTable, serial, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name"),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  published: boolean("published").default(false),
  authorId: integer("author_id").references(() => users.id),
});

Queries mirror SQL structure directly — a select is a select, a where is a where:

import { eq, and, desc, count } from "drizzle-orm";
import { db } from "./db";
import { users, posts } from "./schema";

const publishedPosts = await db
  .select({ id: posts.id, title: posts.title, author: users.name })
  .from(posts)
  .leftJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(10);

Drizzle’s strengths: SQL-fidelity (advanced queries map 1:1), tiny footprint, no generated code to commit, and it composes well with the driver ecosystem (node-postgres, libSQL, D1 on Cloudflare, Neon, Turso). Its cost: you must know SQL — Drizzle won’t translate intent for you.

Raw SQL: when the abstraction costs more than it saves

There are queries where an ORM’s type-safety is a distraction. Aggregations, window functions, recursive CTEs, full-text search, and complex reporting queries are often clearer — and faster to write — in the database’s own language:

// Raw SQL for a reporting query that ORMs render awkwardly
const byCategory = await db.query(`
  SELECT
    c.name,
    COUNT(o.id) AS orders,
    SUM(o.total) AS revenue,
    RANK() OVER (ORDER BY SUM(o.total) DESC) AS rank
  FROM categories c
  LEFT JOIN orders o ON o.category_id = c.id
  GROUP BY c.id, c.name
`);

If you go this route, a thin query builder (like Kysely) gives you parameterized, typed SQL strings without the ORM’s model layer — a middle ground where the query stays literal but the results come back typed.

Choosing in 2026

PrismaDrizzleRaw SQL / Kysely
PhilosophySchema-first, declarativeSQL-typed, thinLiteral SQL
Learning curveGentleRequires SQLRequires SQL
MigrationsBuilt-in, maturedrizzle-kitManual (or tools like Flyway)
Advanced queriesEscape hatchesNativeNative
FootprintClient + generatedMinimalMinimal
Best forRapid product dev, teamsTeams who know SQL, edge/serverlessReporting, fine-tuned queries

A pragmatic split used by many teams: Prisma or Drizzle for application CRUD (typed, migration-managed, safe), raw SQL for analytics and reporting (complex shapes, aggregates), and a query builder for everything middle. What you should almost never do is sprinkle raw SQL through hand-written string concatenation — every fragment should be parameterized to avoid injection.

// ❌ Never — SQL injection and untyped results
const q = `SELECT * FROM users WHERE email = '${email}'`;

// ✅ Parameterized
const q = `SELECT * FROM users WHERE email = $1`;

Putting It All Together

A complete comparison project — the same schema (users + posts) implemented in Prisma and in Drizzle, with the same three queries (typed CRUD, a join, and a reporting aggregate), plus the raw-SQL version for the aggregate — is in this repository. Clone it, run both against the same Postgres, and compare the developer experience side by side.

Conclusion & Next Steps

You now know the three layers: Prisma for schema-first, declarative CRUD with first-class migrations; Drizzle for thin, SQL-faithful typing close to the database; raw SQL (with a query builder) for the queries where abstractions cost more than they save. Next steps: prototype one feature with Drizzle’s relational queries, add drizzle-kit push to your edge deploy (it’s great for D1), and set up an audit trail so raw SQL is only ever parameterized.

References / Sources