Loading content…
Loading content…
Master databases in production: Prisma vs Drizzle, serverless connection limits, zero-downtime migrations, transactions, indexing, and EXPLAIN ANALYZE query profiling
| Feature | Prisma | Drizzle ORM |
|---|---|---|
| Philosophy | Tool suite (declarative schema file) | TypeScript-first schema (SQL-like syntax) |
| Architecture | Rust-based query engine binary | Lightweight TypeScript SQL translator |
| Client Size | Heavy (due to query engine binary) | Ultra-lightweight (native JavaScript) |
| Performance | Good (with cache warmups) | High (fast start times; ideal for edge) |
| SQL Control | Abstracted away (custom API query syntax) | Direct SQL control (write raw SQL if needed) |
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
import { pgTable, serial, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).unique().notNull(),
});
// Next.js Serverless Route using Drizzle with Neon HTTP client
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
export async function GET() {
// Uses direct HTTP pipeline; no persistent TCP connection pool needed!
const allUsers = await db.select().from(users);
return Response.json(allUsers);
}
prisma db push or raw queries that alter tables live can lock your database, blocking all incoming traffic and causing downtime.new_name). Keep writing to both the old and new columns.Common Pitfall
ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT true;) will lock large tables for hours as Postgres updates every row. Instead, add the column as nullable first, deploy, backfill in chunks, and then add the NOT NULL constraint.try {
const result = await prisma.$transaction(async (tx) => {
// 1. Deduct funds from sender
const sender = await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 100 } }
});
if (sender.balance < 0) {
throw new Error("Insufficient funds"); // Auto roll-back!
}
// 2. Add funds to receiver
await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 100 } }
});
});
} catch (error) {
console.error("Transaction failed, database rolled back", error);
}
await db.transaction(async (tx) => {
await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100` }).where(eq(accounts.id, 1));
await tx.update(accounts).set({ balance: sql`${accounts.balance} + 100` }).where(eq(accounts.id, 2));
});
>, <), and sorting operations.-- Create an index to speed up email checks
CREATE INDEX idx_users_email ON users(email);
-- Create a GIN index for search terms inside JSONB metadata
CREATE INDEX idx_products_metadata ON products USING gin (metadata);
EXPLAIN ANALYZE before a SQL query to see how Postgres builds its execution plan and check how long individual stages take.EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
// ❌ BAD: Triggers 1 query for users, then N separate queries for posts
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({ where: { userId: user.id } }); // N queries!
}
// ✅ GOOD: Under the hood, Prisma uses a single query with SQL JOINs or IN (...)
const usersWithPosts = await prisma.user.findMany({
include: { posts: true } // Single SQL operation
});
Postgres ORM & Performance Checklist
Marking it complete updates your roadmap progress percentage.