About the Benchmark

See how this live Prisma vs Drizzle benchmark is set up, what each test covers, and how to read the numbers

Suggest new tests

Think we missed a scenario? Open a GitHub issue and propose a Prisma vs Drizzle workload.

Benchmark Overview

This is a live benchmark of Prisma 7.1.0 and Drizzle ORM 0.45.0 hitting the same Neon PostgreSQL database via Next.js server actions. Prisma uses the PrismaNeon adapter; Drizzle uses neon-serverless with the same DATABASE_URL.

Each run executes the Prisma flow and then the Drizzle flow with identical inputs. Timings capture total execution time (network + Postgres + ORM work) using performance.now() on the server and are rounded to two decimals.

Infrastructure

  • Database - Neon PostgreSQL (serverless) in AWS ap-southeast-1
  • Website - Hosted on Railway in the same region to keep latency stable

Seeded Dataset

  • Volume - 100 users, 400 posts (mix of published/unpublished), 1,000 comments
  • Taxonomy - 20 categories, 40 tags, many-to-many links via post_categories/post_tags
  • Shape - Realistic titles/content and view counts up to 50k from the seed script

Database Schema

  • users - User accounts with email, name, timestamps
  • posts - Blog posts with title, content, view count
  • comments - Comments on posts
  • categories - Post categories
  • tags - Post tags
  • post_categories - Many-to-many relationship
  • post_tags - Many-to-many relationship

Test Hygiene

  • Write-heavy tests use timestamped IDs and delete what they create so the base dataset stays consistent.
  • Bulk runs insert 100 users and ~50 posts per run, then remove them before returning results.
  • Transaction tests cover create/update/delete inside transactions and clean up in the final step.

Test Types

CRUD Operations
Tests basic Create, Read, Update, and Delete operations on single records.
PrismaExample
// Prisma CRUD Example
const user = await prisma.user.create({
  data: { email: "user@example.com", name: "John" }
});

const found = await prisma.user.findUnique({
  where: { id: user.id }
});

await prisma.user.update({
  where: { id: user.id },
  data: { name: "Jane" }
});

await prisma.user.delete({
  where: { id: user.id }
});
DrizzleExample
// Drizzle CRUD Example
const [user] = await db.insert(users)
  .values({ email: "user@example.com", name: "John" })
  .returning();

const found = await db.query.users.findFirst({
  where: eq(users.id, user.id)
});

await db.update(users)
  .set({ name: "Jane" })
  .where(eq(users.id, user.id));

await db.delete(users)
  .where(eq(users.id, user.id));
Complex Joins
Tests multi-table joins across 3-4 tables with nested relations.
PrismaExample
// Prisma Join Example
const usersWithPosts = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        comments: {
          include: { author: true }
        }
      }
    }
  }
});
DrizzleExample
// Drizzle Join Example
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: {
      with: {
        comments: {
          with: { author: true }
        }
      }
    }
  }
});
Aggregations
Tests COUNT, SUM, AVG, and GROUP BY operations for data analysis.
PrismaExample
// Prisma Aggregation Example
const totalViews = await prisma.post.aggregate({
  _sum: { viewCount: true }
});

const avgViews = await prisma.post.aggregate({
  _avg: { viewCount: true }
});

const postsPerUser = await prisma.post.groupBy({
  by: ["authorId"],
  _count: { id: true }
});
DrizzleExample
// Drizzle Aggregation Example
const totalViews = await db
  .select({ sum: sum(posts.viewCount) })
  .from(posts);

const avgViews = await db
  .select({ avg: avg(posts.viewCount) })
  .from(posts);

const postsPerUser = await db
  .select({
    authorId: posts.authorId,
    count: count(posts.id)
  })
  .from(posts)
  .groupBy(posts.authorId);
Subqueries
Tests nested SELECT statements and correlated subqueries for complex filtering.
PrismaExample
// Prisma Subquery Example
const avgPosts = await prisma.post.groupBy({
  by: ["authorId"],
  _avg: { viewCount: true }
});

const usersAboveAvg = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        viewCount: { gt: avgPosts[0]._avg.viewCount }
      }
    }
  }
});
DrizzleExample
// Drizzle Subquery Example
const avgPosts = db
  .select({ avg: avg(posts.viewCount) })
  .from(posts)
  .as("avg_posts");

const usersAboveAvg = await db
  .select()
  .from(users)
  .where(
    gt(
      sql`(SELECT AVG(view_count) FROM posts)`,
      sql`(SELECT view_count FROM posts WHERE author_id = users.id)`
    )
  );
Transactions
Tests atomic multi-operation transactions ensuring data consistency.
PrismaExample
// Prisma Transaction Example
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { email: "user@example.com", name: "John" }
  });
  
  const post = await tx.post.create({
    data: { title: "Post", authorId: user.id }
  });
  
  await tx.comment.create({
    data: { content: "Comment", postId: post.id, authorId: user.id }
  });
});
DrizzleExample
// Drizzle Transaction Example
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users)
    .values({ email: "user@example.com", name: "John" })
    .returning();
  
  const [post] = await tx.insert(posts)
    .values({ title: "Post", authorId: user.id })
    .returning();
  
  await tx.insert(comments)
    .values({ content: "Comment", postId: post.id, authorId: user.id });
});
Bulk Operations
Tests inserting, updating, and deleting 100+ records in a single operation.
PrismaExample
// Prisma Bulk Example
await prisma.user.createMany({
  data: Array.from({ length: 100 }, (_, i) => ({
    email: `user${i}@example.com`,
    name: `User ${i}`
  }))
});

await prisma.user.updateMany({
  where: { email: { startsWith: "user" } },
  data: { name: "Updated" }
});

await prisma.user.deleteMany({
  where: { email: { startsWith: "user" } }
});
DrizzleExample
// Drizzle Bulk Example
await db.insert(users).values(
  Array.from({ length: 100 }, (_, i) => ({
    email: `user${i}@example.com`,
    name: `User ${i}`
  }))
);

await db.update(users)
  .set({ name: "Updated" })
  .where(like(users.email, "user%"));

await db.delete(users)
  .where(like(users.email, "user%"));
How We Measure

Execution Time

Each click runs one end-to-end pass per ORM (no batching). Timers wrap the whole server action, so numbers include network, Postgres, and ORM overhead and may vary slightly run to run.

Fair Comparison

Both ORMs hit the same schema, dataset, region, and DATABASE_URL. Query shapes, filters, joins, groupings, and limits are mirrored as closely as the APIs allow.

Real-World Scenarios

Workloads mirror production-style patterns: relational joins across users/posts/comments/categories, aggregates with HAVING clauses, nested subqueries, multi-step transactions, and 100-row bulk writes.