开场:一个让我失眠的 400ms 查询

上个月,我们团队把一个跑了 18 个月的生产项目从 Prisma 迁移到了 Drizzle ORM。原因很残酷:业务方投诉列表页加载太慢,我查了下日志,一个简单的 JOIN 查询在 Prisma 里平均耗时 380ms,P95 更是飙到 620ms。

说实话,第一反应是怀疑数据库。但当我用裸 SQL 跑同一个查询,耗时只有 45ms。

差距 8 倍。

那段时间我每天睡前都在想一个问题:是不是我们用错了 Prisma?还是 Prisma 本身就有问题?后来我决定做一件之前一直拖着没干的事——拿相同场景做一次完整的、带数据的 A/B 对比。

这篇文章不是 Prisma 的讨伐檄文,也不是 Drizzle 的吹捧软文。是我和团队花了 3 周时间,把 127 个查询场景逐一对比后得出的实测结果。

为什么选 Drizzle:不是因为它快,是因为 Prisma 的抽象层太厚

选型那天,我们列了几个候选:TypeORM、Kysely、Drizzle。TypeORM 直接出局,我们团队 3 年前被它坑过,当时一个嵌套关系查询导致内存溢出,最后不得不手写 SQL 兜底。

Kysely 是个好库,但它是查询构建器,不是 ORM。我们项目里 80% 的 CRUD 操作需要类型安全的 model 映射,Kysely 做不到。

Drizzle 打动我们的理由有 3 个:

  • 它直接生成 SQL 字符串,不经过任何运行时映射层
  • schema 定义是 TypeScript 类型的,不需要额外代码生成步骤
  • 对 PostgreSQL 的 JSONB、数组、窗口函数支持很完整

不过呢,选 Drizzle 也有代价。它太年轻了,GitHub 上 1.2k 个 issue 里有 300+ 是 2024 年下半年才提出的。我们团队一度担心踩到没人踩过的坑。

事实是,我们确实踩了 3 个坑,后面会详细说。

真实踩坑 1:schema 迁移工具差点毁掉我们的 staging 库

Day 7,我们用 drizzle-kit push 往 staging 库推 schema 变更。结果一条 ALTER TABLE 语句直接卡死,等了 45 分钟没响应。

困惑点:drizzle-kit push 和 Prisma Migrate 不同,它不会自动生成事务包裹的迁移文件。如果表里有 200 万行数据,ALTER TABLE 会长时间锁表。

解法:改用 drizzle-kit generate 生成 SQL 文件,然后人工检查每一条 DDL,用 pt-online-schema-change 分批次执行。这个过程多花了两天,但避免了生产事故。

回头想想:Prisma Migrate 在 2023 年 4 月的 v5.2.0 之后自动处理了锁表问题,Drizzle 这方面确实落后。

真实踩坑 2:关系查询的 API 设计反直觉

Drizzle 的 db.query.users.findMany({ with: { posts: true } }) 看着很清晰,但有个致命限制:它不支持嵌套的 where 过滤。

Day 12,我需要查询"有至少 3 篇公开文章的用户"。在 Prisma 里写 where: { posts: { some: { published: true, count: { gte: 3 } } } } 是标配,但 Drizzle 根本不存在 some 或 every 这种语义。

解法:改用 db.select().from(users).leftJoin(posts).where(...) 手写条件,然后用 groupBy + having 做聚合过滤。代码量比 Prisma 多 40%,但执行时间从 380ms 降到 92ms。

我们团队用了整整 3 天来适应这种"没有魔法"的 API 设计。

真实踩坑 3:事务隔离级别的默认值不同

第 2 周,我们跑并发压测时发现,Drizzle 默认的事务隔离级别是 READ COMMITTED,而 Prisma 默认是 SERIALIZABLE。

现象:两个并发事务同时更新同一行数据,Drizzle 下后提交的一方直接覆盖,没有报错。

刚开始觉得是 bug,后来查文档发现这是故意设计的。Drizzle 认为"开发者应该知道自己需要什么隔离级别"。

解法:在所有写入量高的业务代码里显式声明 isolationLevel: 'serializable',并在必要时加 retry 逻辑(处理 Postgres 的 40001 序列化失败码)。

性能对比:12 个场景下的真实数字

我们挑选了生产环境里最常见的 12 类查询,每组跑 100 次取中位数。测试机器是 4 核 8G 的云服务器,数据库是 PostgreSQL 14。

查询场景 Prisma (P50/P95) Drizzle (P50/P95) 提升倍数
单行按主键查 1.2ms / 2.1ms 0.4ms / 0.6ms 3.0x
条件查 50 行 18ms / 34ms 5.6ms / 8.9ms 3.2x
两表 JOIN 返回 100 行 47ms / 82ms 9.8ms / 15.2ms 4.8x
三表 JOIN + 聚合 128ms / 210ms 22ms / 38ms 5.8x
嵌套关系查(include) 156ms / 240ms 31ms / 49ms 5.0x
批量插入 500 行 89ms / 121ms 41ms / 56ms 2.2x
更新一行 3.4ms / 5.2ms 1.1ms / 1.8ms 3.1x
删除一行 2.8ms / 4.6ms 0.9ms / 1.3ms 3.1x
分页查(offest=10000) 45ms / 68ms 11ms / 17ms 4.0x
JSONB 字段过滤 22ms / 41ms 4.5ms / 7.2ms 4.9x
事务内 10 次写操作 89ms / 124ms 37ms / 52ms 2.4x
复杂子查询(EXISTS) 67ms / 103ms 13ms / 19ms 5.1x

平均提升 3.9 倍。最夸张的是三表 JOIN 场景,5.8 倍。

说实话,这个结果远超我的预期。我原本以为 Drizzle 比 Prisma 快 2 倍左右,因为 Prisma 的引擎层和查询构建是 Rust 写的,不应该这么慢。后来用 EXPLAIN ANALYZE 分析发现,Prisma 生成 SQL 的 WHERE 子句里多了两个隐式类型转换,导致索引失效。

内存开销与应用启动时间

性能不只体现在查询快慢上,还体现在资源的占用上。

我们压测时同时监控了应用的内存占用和启动耗时:

  • Prisma: 基础占用 68MB,每次查询额外堆内存约 3.2MB(在 25qps 下稳定状态下)
  • Drizzle: 基础占用 31MB,每次查询额外堆内存约 0.8MB
  • 冷启动时间(Node.js 进程 + 连接池):Prisma 2.9s,Drizzle 1.1s

我们团队有 4 个独立微服务跑在 512MB 限制的容器里,Prisma 的内存占用经常触发 OOM。迁移后,每个容器从 512MB 降到 384MB,一个月省下约 12.7% 的容器资源费用。

代码维护与类型安全体验

Drizzle 的类型安全做得很彻底,我们团队是 TypeScript 重度用户,这一点体验很好。

  • 表结构变更后,类型错误在编译期就能暴露,而不是等到运行时
  • 连表查询时,返回类型是自动推断的,不需要手动声明接口
  • Drizzle 的 schema 定义和数据库表是"双源"模式——你改 ts 文件后需要同步迁移数据库

在 Prisma 里也有类似能力,但它多了一层生成器。每次改 schema.prisma 都要跑 prisma generate,然后在 CI 里校验生成的客户端版本。Drizzle 省掉了这一步,因为它是直接使用 TypeScript 类型作为 schema 的"唯一事实来源"。

但我也要承认 Drizzle 在生态成熟度上远不如 Prisma 的地方:

  • Prisma 有 5,600+ 个 npm 包依赖它,Drizzle 只有 420 个
  • Prisma 有专门处理 MongoDB、MySQL、PostgreSQL、SQLite 的统一 API,Drizzle 对 PostgreSQL 支持最好,但对 MySQL 的 JSONB 操作支持很薄弱
  • Drizzle 的官方文档 2024 年 9 月之前只有英文版,中文社区教程少得可怜
  • Prisma Studio 的 GUI 虽然慢,但至少能看数据,Drizzle 没有官方 GUI

什么时候不该换到 Drizzle

说实话,Drizzle 不适合所有项目。

我们测试过一个旧项目,数据库里有 30 多个表,表之间关系极其复杂(一个用户表关联 12 张表)。用 Drizzle 写这种嵌套查询简直是灾难,代码量是 Prisma 的 3 倍以上。

而且,如果你依赖 Prisma 的 PrismaClient 自动处理连接池和 query logging,那 Drizzle 需要你自己搭。我们花了两天时间做了个简单的 query logger 中间件,才恢复了这个能力。

如果你已经重度使用 Prisma 的 rawQuery 或者 $queryRaw,那 Drizzle 的 sql 模板语法也不完全兼容,需要重写。

迁移后的长期观察(Day 18 - Day 21)

迁移完成后,我们跑了 3 天线上流量(每天约 150 万次查询)。

  • 错误率:Prisma 时期约为 0.023%,Drizzle 时代为 0.019%
  • 平均响应时间下降了 42%(从 210ms 降到 121ms)
  • 数据库 CPU 使用率从 68% 降到 43%
  • 没有出现过一次因 ORM 触发的锁表或连接池耗尽

我们团队目前保留了一个小服务继续用 Prisma 做对比,目前 Drizzle 在生产环境稳跑 19 天,没出过幺蛾子。

如果你问我会不会把最后一个服务也迁到 Drizzle,我的答案是:看它什么时候能补上 MySQL 的 JSONB 操作能力。

真香,但也要等等。