Overview
Use raw SQL when the query builder cannot express the query, and keep the tagged-template form so values stay parameterized. $queryRaw returns rows and $executeRaw returns the affected row count. Reach for them deliberately: for the same table, a missing index or a materialized view often removes the need.
Use executeRaw for writes
const rows = await prisma.$queryRaw<Array<{ id: string; rank: number }>>`
SELECT id, ts_rank(search, websearch_to_tsquery('english', ${q})) AS rank
FROM articles
WHERE search @@ websearch_to_tsquery('english', ${q})
ORDER BY rank DESC
LIMIT ${limit}
`
const count = await prisma.$executeRaw`
UPDATE jobs SET status = 'running', started_at = now()
WHERE id = ${jobId} AND status = 'pending'
`- Each
${value}becomes a$1,$2, … placeholder. Parameters protect values only; identifiers (table, column, keyword) cannot be parameters. - The type argument on
$queryRaw<T>is not checked at runtime. Prisma casts the rows toT, so define it accurately. - Inside a transaction, call these on the
txclient; see prisma-transactions.
Never build SQL by string concatenation
// WRONG: injection risk
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${userInput}'`)
// RIGHT
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${userInput}`$queryRawUnsafe and $executeRawUnsafe take plain strings. Use them only when the query structure must be dynamic, and validate the dynamic part against a hardcoded allowlist first. Never pass user input as a column name, table name, or keyword.
Compose fragments with Prisma.sql
import { Prisma } from "./generated/prisma/client"
const filter = status ? Prisma.sql`WHERE status = ${status}` : Prisma.empty
const rows = await prisma.$queryRaw<Array<{ id: string }>>`
SELECT id FROM jobs ${filter} ORDER BY created_at DESC LIMIT 50
`- Fragments keep their own parameters, so composition does not reopen injection.
Prisma.join(ids)builds a parameter list forIN:WHERE id IN (${Prisma.join(ids)}).Prisma.raw(str)injects an unparameterized literal. Use it only for allowlisted identifiers.- In Prisma 7, import
Prismafrom the generator’soutputpath, not@prisma/client.
Know what needs raw SQL
- Recursive CTEs, window functions, and lateral joins.
- Full-text ranking and highlighting (
ts_rank,ts_headline); see postgres-full-text-search. - JSONB operators beyond Prisma’s Json filters (
path,equals,string_contains,array_contains):jsonb_path_query,@?,#>>; see postgres-jsonb. EXPLAIN ANALYZE; see postgres-explain.SELECT ... FOR UPDATE,COPY, andINSERT ... ON CONFLICTwith complexSETexpressions.
Convert raw results when needed
$queryRaw bypasses the model layer, so rows carry database types, not Prisma conversions.
- Postgres
count(*)and otherbigintaggregates arrive as JavaScriptBigInt. Cast withNumber(...)before JSON serialization, or cast in SQL (count(*)::int). timestampcolumns arrive asDate, matching client behavior.- When a raw query returns the same shape as a model, prefer the ORM query unless profiling shows a clear gain.