Stop 08 · ORMs & Query BuildersTopics · 10/10

Raw Queries vs ORM

Part of the Backend Developer Roadmap

In short

ORMs are great for everyday CRUD, but some queries are easier, or only possible, in plain SQL. Good backend developers use both without guilt.

Knowing when to bypass your ORM and write raw SQL directly — usually for complex reporting queries or performance-critical paths the ORM handles poorly.

The blob points at a ready dosa on display while showing the cook how to fold another.

Think of it like this

Menu card vs custom order

At a dosa counter, pointing at the menu gets you a perfect masala dosa, fast. But for a paper-thin ghee roast folded your way, you tell the cook exactly how. The ORM is the menu; raw SQL is telling the cook precisely what to make.

Watch it happen

Which one, when?

ORM

  • Typed results and autocomplete
  • Safe parameters by default
  • Easy relations and migrations

Raw SQL

  • Full SQL power: CTEs, window functions
  • You control the exact query
  • You handle types and safety yourself

Use the ORM for most queries. Drop to raw SQL for reports and tricky queries, always with parameters, never string concatenation.

The big ideas

What you need to know

ORM for everyday work

Create, read, update, delete and simple relations are cleaner and safer through the ORM.

Raw SQL for the hard bits

Reports with window functions, CTEs or database-specific features are often clearer as SQL.

Parameters, always

Raw doesn't have to mean unsafe. Use tagged templates or placeholders, never string concatenation.

Escape hatches built in

Prisma has $queryRaw, TypeORM has query() and Knex has raw(). You don't have to leave your ORM.

See it in code

Walk through the code

ts
1// Safe: Prisma's tagged template turns ${city} into a $1 parameter2const top = await prisma.$queryRaw<{ name: string; rank: bigint }[]>`3  SELECT name,4         RANK() OVER (ORDER BY total_spent DESC) AS rank5  FROM customers6  WHERE city = ${city}7  LIMIT 108`;9 10// Dangerous: string concatenation lets input become SQL11// await prisma.$queryRawUnsafe("SELECT * FROM users WHERE city = '" + city + "'");

1/4$queryRaw is a tagged template. You write the SQL; Prisma handles the parameters.

Type the result yourself, as with the generic here. Postgres RANK() returns a bigint, so Prisma hands you a BigInt.

Save yourself the pain

Common mistakes

Oops

Building raw SQL by concatenating user input.

Do this instead

Always use placeholders or tagged templates. One stray quote in the input can otherwise rewrite your query.

Oops

Forcing a complex report through the ORM's query API until it's unreadable.

Do this instead

If the ORM version is twice as long as the SQL, write the SQL. Keep it in one well-named function.

Your mission

Leaderboard query

~20 min

Quick check

Which of these raw queries is safe from SQL injection?

Go deeper

  1. 1Rewrite one ORM query as raw SQL and compare readability and performance.
  2. 2Learn your ORM's escape hatch for raw queries, e.g. Prisma's `$queryRaw`.
  3. 3Keep raw queries parameterized to avoid reintroducing SQL injection risk.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge