Heath SchweitzerHeath Schweitzer
← All posts

Prisma vs Raw SQL: What I Learned Migrating from PHP

April 13, 2026|Heath Schweitzer|4 min read|32 views|Last Updated June 22, 2026

Technology
Diagram comparing Prisma ORM workflow and raw SQL approach, showing schema, migrations, client, and direct database queries.

For most of my career, I wrote raw SQL. SELECT * FROM posts WHERE status = 'published' ORDER BY created_at DESC LIMIT 10 — that kind of thing, directly in PHP using PDO or the old mysql_* functions before that. I knew the query language, I knew how to optimize with indexes, and I understood what was happening at the database level.

When I rebuilt heathschweitzer.com on Next.js, I chose Prisma as the ORM (Object–Relational Mapper), essentially a library that lets you work with your database using objects and classes instead of writing raw SQL. It took a few rounds of updates, but now I have a clear picture of what I gained, what I gave up, and when each approach makes sense.

What Prisma Actually Is

Prisma is not just a query builder — it's a full data access layer with three components that work together.

Prisma Schema is a declarative description of your data model. You define your tables, fields, relationships, and indexes in a schema.prisma file using Prisma's own DSL (Domain‑Specific Language). This becomes the single source of truth for your database structure.

Prisma Migrate takes your schema and generates SQL migration files. When you change the schema, it diffs against the current database state and generates the appropriate ALTER TABLE statements. You get version-controlled migrations without writing them by hand, a huge time saver IMHO.

Prisma Client is the generated TypeScript client. Based on your schema, it generates a fully typed API for querying your database. Autocomplete knows your table names, column names, and relationships. If you try to query a field that doesn't exist, TypeScript catches it at compile time, not runtime.

The Developer Experience Gap

The thing Prisma gets right that raw SQL doesn't is the feedback loop. When I was writing raw SQL in PHP, errors showed up at runtime — a typo in a column name would only surface when that code path was hit during QA/UAT testing. With Prisma, the same mistake is a TypeScript compile error.

A query like this:

const posts = await prisma.post.findMany({
  where: { status: "PUBLISHED" },
  include: { author: true, categories: true },
  orderBy: { publishedAt: "desc" },
  take: 10,
});

Returns a fully typed result. posts[0].author.email is autocompleted and TypeScript knows its type. If I later rename the email field to emailAddress in the schema, every reference in the codebase breaks at compile time. I fix them all before any code runs.

That refactoring story is where ORMs tend to shine. In a large PHP codebase, renaming a database column means grep-searching for string literals and hoping you found them all. In Prisma it's a type error cascade that guides you to every affected location.

Where Raw SQL Is Still Better

Prisma is not always the right tool. Complex analytical queries are the clearest example that come to mind.

If I needed to write a query with multiple levels of aggregation, window functions, or complex CTEs, Prisma's abstraction starts fighting me. The ORM can express common query patterns elegantly, but the further you get from simple CRUD, the more you find yourself either writing Prisma's raw query escape hatch (prisma.$queryRaw) or wishing you'd just written SQL in the first place.

Prisma can add latency too. It generates queries that are correct but not always optimal. A query you'd write as a single join might become multiple round trips in Prisma depending on how you've structured the includes. For high-throughput applications where query performance is critical, the overhead matters.

For my use case — a personal site and blog with modest traffic — the performance difference is academic. The developer experience win is very concrete.

Migrations: The Part Developers Should be Talking About More

One of the most underappreciated aspects of any ORM is the migration story. In the old PHP world, I managed migrations manually — a folder of numbered SQL files, a table tracking which ones had run, and a deployment script that executed new ones wasn't all that uncommon on a project. Sure, it worked but it was fragile and easy to get out of sync between environments.

Prisma Migrate handles this much more elegantly. Every schema change generates a migration file with the SQL. prisma migrate deploy applies any pending migrations in order. The migration history is version-controlled alongside the application code. When I deploy to production, the database schema updates atomically with the code that depends on it as part of my deploy process.

The one rough edge I hit was the shadow database requirement for prisma migrate dev. Prisma needs a second database it can freely create and drop to validate migrations before applying them. Setting up the right permissions for that database user took some debugging. Once working, it's invisible, but it's a setup step that caught me off guard.

My Recommendation

Use Prisma when: you're building a TypeScript application, your data model is relatively normalized, you want refactoring safety, and developer experience matters more than raw query performance.

Stick with raw SQL or a lighter query builder when: you're doing heavy analytics, you need fine-grained query control, or you're on a team where everyone knows SQL deeply and the ORM abstraction adds cognitive overhead without a clear payoff.

For the kind of applications I build, mostly web apps, APIs, and content sites, Prisma is the right default. The type safety alone has caught bugs I wouldn't have found until later.

Tagged

prismatypescriptsqldatabasePHPDXDevExp

If this post was useful, consider buying me a coffee ☕ with ₿itcoin — no account needed, any amount welcome.

Bitcoin tip QR code
⚡ Open in Wallet