开场:一个让我失眠的 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 操作能力。
真香,但也要等等。