原始 SQL 查询:$queryRaw 和 $executeRaw 回退方案
什么时候需要原始 SQL
Prisma Client 的查询 API 覆盖了绝大多数 CRUD 和关系查询场景。但以下情况你可能需要回退到原生 SQL:
- 窗口函数 / CTE / 递归查询 — Prisma Client 不支持
- 复杂报表聚合 — 比如按多个维度 GROUP BY + HAVING + 子查询
- 数据库特定功能 — 比如 PostgreSQL 的
jsonb_path_query、全文搜索tsvector、MySQL 的MATCH AGAINST - 性能敏感的手写优化 SQL — 你比 Prisma 更清楚怎么写最快的查询
- 批量 DDL / DML — 比如一次性清理历史数据、重建索引
Prisma Client 提供 $queryRaw 和 $executeRaw 两个兜底。
$queryRaw — 执行查询并返回结果
返回行数据,适合 SELECT:
// 简单查询
const result = await prisma.$queryRaw`SELECT * FROM users WHERE active = true`
// 带参数(用模板字符串占位符,Prisma 自动防 SQL 注入)
const email = 'alice@prisma.io'
const user = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`
模板字符串 vs 字符串拼接
正确:用 ES6 模板字符串(Prisma 内部处理参数绑定)
const result = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`
危险:不要用 .unsafe() 或手动拼接字符串
// ❌ 绝对不要这样写!这是 SQL 注入
const result = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${email}'`
)
$queryRawUnsafe 存在但只有在你能 100% 确定输入安全时才用。
带命名参数(PostgreSQL)
const result = await prisma.$queryRaw`
SELECT * FROM users
WHERE email = ${Prisma.sql`${email}`}
AND created_at > ${Prisma.sql`${sinceDate}`}
`
使用 Prisma.sql 构造片段
const tableName = Prisma.raw('users') // 注意:raw 用于标识符(表名、列名),仍然需要确保安全
const result = await prisma.$queryRaw`SELECT * FROM ${tableName}`
$executeRaw — 执行语句不返回结果
适合 INSERT / UPDATE / DELETE / DDL:
await prisma.$executeRaw`UPDATE users SET active = false WHERE inactive_days > 30`
// 批量清理历史数据
await prisma.$executeRaw`DELETE FROM logs WHERE created_at < ${cutoffDate}`
实战案例
案例一:窗口函数(Prisma Client 不支持)
// 计算每个用户在每个月的排名
const rankings = await prisma.$queryRaw`
SELECT
user_id,
DATE_TRUNC('month', created_at) as month,
ROW_NUMBER() OVER (PARTITION BY DATE_TRUNC('month', created_at) ORDER BY total DESC) as rank
FROM user_stats
`
案例二:PostgreSQL JSONB 查询
// 查询 payload JSON 里包含某个标签的记录
const tag = 'production'
const logs = await prisma.$queryRaw`
SELECT * FROM logs
WHERE payload @> ${`{"tags": ["${tag}"]}`}::jsonb
`
案例三:全文搜索
// PostgreSQL 全文搜索
const keyword = 'prisma'
const results = await prisma.$queryRaw`
SELECT * FROM posts
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', ${keyword})
ORDER BY ts_rank(to_tsvector('english', title || ' ' || content), plainto_tsquery('english', ${keyword})) DESC
`
与事务配合
$queryRaw 和 $executeRaw 可以在 Prisma 事务里使用:
await prisma.$transaction(async (tx) => {
await tx.$executeRaw`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
await tx.$executeRaw`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
})
性能考虑
$queryRaw的结果返回后,你拿到的是原始数据库行的 JavaScript 对象,不经过 Prisma 的序列化层,所以比 Prisma Client 查询更快(少了一层转换)- 但你失去了类型安全和关系处理能力,需要自己手动定义返回类型
- 能用 Prisma Client 就用,复杂查询再回退到 raw