#TypeScript#DevOps

ORM vs Query Builder vs 原生 SQL:怎么选

ORM 省心但可能生成烂 SQL,原生 SQL 灵活但样板多。本文客观对比 Prisma/Drizzle/Kysely/原生 SQL 各自的哲学与适用场景:类型安全、迁移、N+1、性能、学习曲线,给一份"什么项目该用哪个"的结论。

本文要点

  • 主题聚焦:TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月15日·更新于 2026年7月23日

数据库访问层选型是 TypeScript 后端里最容易吵起来的话题。一派说 ORM 是"给不会写 SQL 的人用的",生成的 SQL 烂、N+1 满天飞;另一派说手写 SQL 是"拒绝抽象",团队大了根本维护不住。两边都没说错,但都没说全。

事实是:Prisma、Drizzle、Kysely、原生 SQL 这四样东西,抽象层级完全不同,要解决的问题也不同。把"谁更好"换成"谁更适合我的项目和团队",答案会清晰很多。本文按抽象从高到低讲一遍各自的哲学、代码长相、和真实项目里的坑,最后给一份不站队的选型建议。

一、三种数据访问方式的哲学差异

数据访问方式本质上是抽象层级的选择,而抽象层级决定了你写得多爽、以及出问题时能往下钻多深。

  1. ORM(对象关系映射):高抽象。你定义"模型"和"关系",框架替你把对象图翻译成 SQL。心智里全是业务实体,没有 JOIN。
  2. Query Builder(查询构造器):中抽象。你用链式 API 一段一段拼 SQL,select().from().where(),看得见结构,但不用手写字符串。
  3. 原生 SQL:低抽象。你直接写 SQL 字符串,框架最多帮你做参数化、连接池、行到对象的映射。

抽象不是越高越好,也不是越低越好。高抽象省脑力、藏细节;低抽象给控制权、要你亲自管细节。 一个快速迭代的 SaaS 后台用 ORM 能省一半工时,一个高频读取的支付系统用 ORM 可能第一周就被慢查询拖垮。下面四节按这条线分别拆。

二、Prisma:schema-first 的类型安全优等生

Prisma 是典型的"重 ORM"。它的核心是一份 schema.prisma,定义模型和关系,然后生成出强类型的 client。

prisma
// schema.prisma
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  posts Post[]
}

model Post {
  id       Int  @id @default(autoincrement())
  authorId Int
  author   User @relation(fields: [authorId], references: [id])
}
ts
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true }, // 显式预加载,避免 N+1
});

Prisma 的优势集中在三点:类型安全是编译期真实推导出来的(不是手写 interface),schema.prisma 是 single source of truth,迁移工具(prisma migrate)对大多数业务项目够用。开箱即用,新手半天能上手。

但它的代价也是真实的。第一,生成的 SQL 不透明——你写的是 include: { posts: true },Prisma 内部可能发多条查询而不是一条 JOIN,你看日志才知道。第二,复杂查询很难写:多表聚合、窗口函数、CTE、ON CONFLICT 这些,要么绕路,要么退化成 prisma.$queryRaw 去手写 SQL。第三,抽象层有性能税,大表上的某些查询会比你手写的 SQL 慢一截。

一句话:Prisma 适合"模型清晰、查询以 CRUD 为主、团队想要类型安全和迁移开箱即用"的场景。不适合复杂报表、高频热点查询。

三、Drizzle:Prisma 和 Kysely 之间的甜区

Drizzle 是这几年最值得学的选项。它的 API 是 SQL-like 的——你写出来的代码,几乎能一对一映射到 SQL。

ts
// schema.ts
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  authorId: integer('author_id').notNull().references(() => users.id),
});

// 查询
const rows = await db
  .select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId))
  .where(eq(users.id, 1));

Drizzle 的哲学是薄抽象:它不藏 SQL,你写 leftJoin 就是 LEFT JOIN,你写 eq 就是 =。生成的 SQL 你能一眼看穿,复杂查询(CTE、窗口函数、原生 SQL 片段)都直接支持。schema 用 TS 写,类型从 schema 自动推出来,不需要 codegen 那么重的流程。体积也很轻,Edge Runtime、Serverless 上跑都没负担。

它的"甜"在于位置:类型安全和 Prisma 一样强,但对 SQL 的控制力接近 Kysely。代价是相比 Prisma 心智稍重——你得真的会读 SQL,链式拼错了会得到错的查询;关系嵌套不像 Prisma 的 include 那么"自动",要用 with 配合 relational-style API,或者自己写 JOIN。

一句话:Drizzle 适合"既想要类型安全、又不想失去对 SQL 控制力"的中大型项目。是当前 TS 后端的默认推荐。

四、Kysely:纯 query builder,无 ORM 负担

Kysely 是纯 query builder,它只有 query,没有 model。你手写一份类型定义(或者用工具从数据库 schema 生成),然后用链式 API 拼 SQL。

ts
interface User { id: number; email: string; }
interface Post { id: number; author_id: number; }

const rows = await db
  .selectFrom('users')
  .leftJoin('posts', 'posts.author_id', 'users.id')
  .where('users.id', '=', 1)
  .selectAll()
  .execute();

和 Drizzle 的核心区别在于:Kysely 完全没有 ORM 的概念——没有模型、没有关系图、没有"对象图"。你拿到的是"SQL 的类型化构造器",行就是行,表就是表。

这意味着几件事。好处是零 ORM 负担:没有懒加载陷阱、没有 N+1 因为"懒"导致的、生成的 SQL 你写什么样就是什么样。复杂查询、动态拼条件(if (filter) query = query.where(...)),Kysely 处理得非常自然。坏处是类型要自己维护(虽然能 codegen),关系查询要自己 JOIN、自己拼结果形状,没有 Prisma 那种"include 一下就给你嵌套对象"的便利。

一句话:Kysely 适合"要对 SQL 精确控制、不想引入任何 ORM 心智"的场景。报表系统、数据层薄的微服务、团队里 SQL 功底扎实的项目。

五、原生 SQL:最透明也最容易踩坑

最后是直接写 SQL。Node.js 里通常是 pg 库配模板字符串,或者用 @database/pg、postgres 这类带类型推导的轻量库。

ts
const rows = await sql`
  SELECT u.*, p.*
  FROM users u
  LEFT JOIN posts p ON p.author_id = u.id
  WHERE u.id = ${1}
`;

原生 SQL 的好处是绝对的透明和灵活:你写什么就执行什么,窗口函数、LATERAL、数据库特定语法、性能优化的每一招都能用上。没有抽象层的性能税。

但代价也最重。第一,类型安全要自己搞:返回类型默认是 any,要么靠库推导、要么手写。第二,样板多:映射、分页、动态 WHERE 拼接,全是自己写的循环。第三,SQL 注入风险——上面用模板字符串参数化是安全的,但一旦你忍不住用 ${variable} 拼表名/列名/排序字段(这些不能参数化),就埋了雷。第四,迁移工具要自己选(node-pg-migrate 之类),没有 Prisma 那种开箱体验。

一句话:原生 SQL 适合"查询极度复杂、性能要求极端、团队对注入和迁移都有纪律"的场景。不是默认选项,是特殊选项。

六、各自的坑:N+1、慢查询、注入、样板

选型前要先看清坑,坑决定了每个工具的天花板。

ORM 的 N+1 陷阱:ORM 默认懒加载关系。你在循环里 user.posts,每次都触发一条 SQL,100 个用户就是 101 条查询。Prisma 的解法是显式 include,Drizzle 用 with,但前提是你得起 awareness。N+1 不是 ORM 的 bug,是高抽象的固有副作用——你不去想 SQL,就不会想到合并。

ORM 生成的 SQL 慢:Prisma 的 include 有时拆成多条查询而非 JOIN,大表上会慢。这种慢往往上线才暴露,因为开发库数据少。

Query Builder 的复杂查询冗长:Kysely/Drizzle 写 5 表 JOIN + 子查询 + CTE,代码会比原生 SQL 长,可读性也下降。中等复杂度是甜区,极复杂度反而不如原生 SQL 直观。

原生 SQL 的注入风险:参数化能挡值,挡不住表名、列名、ORDER BY 字段。一旦动态拼这些(ORDER BY ${sortField}),就是注入入口。需要白名单校验。

原生 SQL 的样板税:每个查询都要写类型、写映射、写分页,团队大了容易各写各的,没有统一的 query 风格。

七、选型:什么项目该用哪个(别教条)

给一份可操作的结论,但先说前提:没有银弹,只有匹配。

  • 快速开发、MVP、CRUD 为主、团队规模小:用 Prisma。类型安全 + 迁移 + 极低心智成本,能把"从 0 到上线"压缩到最快。复杂查询那部分用 $queryRaw 兜底。
  • 中大型项目、想要类型安全又不想失去 SQL 控制力:用 Drizzle。当前 TS 生态的默认推荐,长期看投资回报最高。
  • 报表系统、数据层薄、团队 SQL 功底扎实、要精确控制 SQL:用 Kysely。没有 ORM 负担,复杂动态查询最顺手。
  • 极端性能、极复杂 SQL(大量数据库特定语法、窗口函数、LATERAL):用原生 SQL + 一个带类型推导的库。把抽象留给该抽象的地方,核心查询不抽象。

混合用也是合理的:Prisma 管业务 CRUD,复杂报表用 Kysely 或原生 SQL 走同一张库。不要为了"统一"在一个不适合 ORM 的地方硬上 ORM,也不要为了"纯粹"在一个纯 CRUD 的地方硬写 SQL。

最后两条反教条:ORM 不总是烂——它让大量中小项目用一半的时间跑完了比赛,N+1 和慢查询是可控的、有最佳实践的;原生 SQL 不总是对的——一个 5 人的 SaaS 团队全程手写 SQL,迁移、类型、风格全靠自觉,半年后就维护不动了。按项目规模、查询复杂度、团队能力三个维度选,而不是按信仰选。

小结

  • 抽象层级是核心权衡:ORM 高抽象省脑力、藏细节;Query Builder 中抽象看得见 SQL;原生 SQL 低抽象给控制权、要亲自管细节。
  • Prisma:类型安全最强、迁移开箱即用,适合 CRUD 为主的快速开发;代价是 SQL 不透明、复杂查询难。
  • Drizzle:SQL-like API + 强类型,薄抽象看得见生成 SQL,是 Prisma 和 Kysely 的甜区,TS 后端当前默认推荐。
  • Kysely:纯 query builder 无 ORM 负担,适合要精确控 SQL、团队功底扎实的场景;关系查询要自己拼。
  • 原生 SQL:最灵活最透明,但类型、迁移、注入、样板全是自己的活,是特殊场景选项而非默认。

金句:ORM 省的是脑力,原生 SQL 省的是抽象税——你到底该付哪一边,取决于你的查询有多复杂、团队有多大,而不是你信哪一派。

Related

相关推荐