Drizzle ORM + Bun 性能革命:当 TypeScript 生态终于在数据库层跑出比 Go 更低的延迟
2026 年五一假期,Drizzle ORM 发布 v1.0.0-rc.1,官方随手贴了一张基准测试图:左边是 Drizzle 跑在 Bun 上,平均延迟 7.3ms,吞吐 8.8k req/sec;右边是 Go v1.25.5,平均延迟 18.1ms,吞吐 8.2k req/sec。两个数字摆在一起,Bun 官方账号转发只配了一句话:"faster than Go, uses Bun, many such cases." Evan You(Vue.js 作者)也转了,说正好赶在 Void Zero 融资消息之前。这条推在 Hacker News 上拿了几千 upvote。本文从工程视角深度拆解:Drizzle 到底是怎么做到的?它的架构设计有什么独特之处?它适合什么样的团队?实战中有什么坑?
一、为什么我们需要重新审视 TypeScript ORM
1.1 三代 ORM 的困境
TypeScript 生态的 ORM 演进大致可以分成三代,每一代都解决了上一代的问题,但也带来了新的代价。
第一代:DAO 模式 + raw SQL。最早的 Node.js + TypeScript 项目,数据库访问几乎全靠 pg(PostgreSQL)或 mysql2 这样的原生驱动。SQL 写在字符串里,参数靠手拼接。这种方式性能最好,但没有类型安全,SQL 注入风险高,重构代价大。
第二代:Prisma 代表的「schema-first」ORM。Prisma 要求开发者在独立的 .prisma 文件中定义数据模型,然后生成类型安全的客户端代码。Prisma 的优势是 DX(开发者体验):Schema 即文档,类型推导即约束,IDE 自动补全。但它有三个明显问题:
- 性能损耗:Prisma Client 体积大(生产包可达几十 MB),查询经过多层抽象,复杂查询需要走 raw SQL 才能实现,而 raw SQL 意味着失去类型安全。
- 生成代码膨胀:Prisma 的查询是通过动态代理生成的,这意味着每次请求都有额外的 JavaScript 运行时开销。
- Schema 与业务代码割裂:
.prisma文件是一个独立王国,它与 TypeScript 源码之间的关系是「生成」,而不是「演进」,导致大型项目中 Schema 迁移和代码重构之间容易出现不同步。
第三代:TypeORM 的 Active Record 模式。TypeORM 引入了装饰器(Decorator)语法,模型类直接绑定数据库操作,Active Record 模式让代码看起来很「OOP」。但装饰器是运行时反射,TypeScript 编译后的 JavaScript 代码中,类型信息已经丢失,类型推断在数据库操作的边界处被迫中断。此外,TypeORM 的 API 设计相对保守,很多现代 TypeScript 特性(泛型推导、模板字面量类型、条件类型)都没有被充分利用。
那么,有没有一种 ORM,能同时满足:类型安全 × 接近原生 SQL 的性能 × 轻量无运行时依赖 × 兼容现代 TypeScript 特性?这就是 Drizzle ORM 试图回答的问题。
1.2 Drizzle 的诞生背景
Drizzle ORM 由 Drizzle Team 开发,核心成员来自早期参与过 postgres(node-postgres)和多个 TypeScript 工具链项目的工程师。Drizzle 的第一个 commit 是在 2022 年,目标是做一个「对 TypeScript 友好」的 ORM,而不是「对 TypeScript 用户隐藏 SQL」的 ORM。
Drizzle 的官方定位是:"SQL-first TypeScript ORM"。这四个字的分量在于,它不是「比 SQL 更好用」,而是「在 SQL 的基础上做类型增强」。这句话在工程上的含义是:Drizzle 的查询构建器输出的 SQL,与你手写的 SQL 在语义上是等价的,没有任何隐藏的抽象层。
1.3 为什么 2026 年 Drizzle 突然爆发
2026 年 Drizzle 的爆发有几个催化剂:
第一:Bun 1.0 成熟 + Drizzle 官方推荐组合。Bun 的 JavaScriptCore 引擎对数据库驱动的 IO 做了深度优化,Drizzle 本身没有运行时抽象层,两个因素叠加产生了惊人的基准测试数字。
第二:Void Zero 的融资。2026 年,Drizzle 背后的商业公司 Void Zero 完成了新一轮融资,投资方包括 a16z 和红杉。Evan You 公开表示自己「用 Drizzle 替代了原来项目中的 Prisma」,这个背书在 TypeScript 社区极具分量。
第三:数据库驱动的后端复兴。随着 Next.js 15 和 React Server Components 的成熟,「前后端全栈 TypeScript」成了主流范式。在这种范式下,后端直接操作数据库的场景越来越多,而不是通过 REST/GraphQL 远程调用。这意味着 ORM 的性能不再是「差个几毫秒无所谓」,而是直接决定了用户请求的 P99 延迟。
二、架构内核:为什么 Drizzle 这么快
2.1 零运行时抽象的设计哲学
理解 Drizzle 的性能优势,首先需要理解它的架构哲学:Query Builder 生成 SQL,而不是解释执行。
当你写这段 Drizzle 代码:
import { db } from './drizzle';
import { users } from './schema';
const activeUsers = await db
.select()
.from(users)
.where(eq(users.status, 'active'))
.orderBy(desc(users.createdAt))
.limit(20);
Drizzle 在编译时(实际上是在 Node.js/Bun 运行时导入模块时)生成了纯 SQL 字符串:
SELECT "id", "name", "email", "status", "created_at"
FROM "users"
WHERE "status" = 'active'
ORDER BY "created_at" DESC
LIMIT 20;
这个 SQL 字符串直接传递给底层的 postgres(Node.js)或 bun:sqlite(Bun + SQLite)驱动,没有任何中间解释层。驱动收到的是你手写也会写的 SQL。
对比 Prisma:当你调用 prisma.user.findMany({ where: { status: 'active' } }) 时,实际上执行的是 PrismaClient.queryRaw 方法,这个方法内部有一个查询计划器,会根据模型关系图来决定 JOIN 策略、N+1 查询优化等。这个过程有 JavaScript 层面的计算开销,而 Drizzle 直接跳过了这一步。
2.2 Schema 定义:类型即约束
Drizzle 的 Schema 定义是纯 TypeScript 代码,没有独立的 DSL 文件:
import { pgTable, serial, text, timestamp, boolean } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
status: text('status').notNull().default('active'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').references(() => users.id),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// 定义关系
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
这个 Schema 的类型推导是完整的:users 表中每个字段的类型(string、number、boolean、Date)在 TypeScript 层面是可用的。当你写 users.name 时,TypeScript 知道这是 string 类型;当你写 eq(users.status, 'active') 时,TypeScript 知道 status 字段允许的值域。
更重要的是,Drizzle 支持模板字面量类型约束来防止列名拼写错误:
// 错误的列名会在编译时报错
// Argument of type '"nmae"' is not assignable to parameter of type '"name"'
const result = await db.select().from(users).where(eq(users.nmae, 'John'));
// ^^^^^^^^^^^^^^^^^^^ 编译错误
这个能力来自 Drizzle 使用了 TypeScript 的 Branded Types(品牌类型)技术:每个列在 Schema 定义时会被标记上唯一的类型标签,使得列名成为类型系统的一部分。
2.3 关系查询:主动声明 vs 隐式推断
Prisma 的关系查询依赖「按需加载」的隐式推断:
// Prisma: 隐式推断关系路径
const users = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
},
},
});
Drizzle 要求你显式声明关系路径:
// Drizzle: 显式关系查询
const usersWithPosts = await db
.select({
user: users,
post: posts,
})
.from(users)
.leftJoin(posts, eq(posts.userId, users.id))
.where(and(eq(users.status, 'active'), eq(posts.published, true)));
Drizzle 的方式看起来更接近 SQL,但它有一个关键优势:你完全控制 JOIN 的类型(INNER、LEFT、RIGHT、FULL),Prisma 的 include 在复杂场景下可能产生意料之外的子查询,而 Drizzle 的 SQL 就是你写出来的 SQL。
对于需要一对一、一对多、多对多关系的高级场景,Drizzle 的 relations API 提供了类型安全的关联访问:
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
},
},
});
// TypeScript 类型推导:userWithPosts.posts 是已过滤和排序后的数组
console.log(userWithPosts.posts[0].title);
2.4 多数据库支持架构
Drizzle 的核心层是数据库无关的,通过适配器模式支持多种数据库:
| 数据库 | 驱动 | 适配器包 |
|---|---|---|
| PostgreSQL | postgres / pg | drizzle-orm/pg-core |
| MySQL | mysql2 | drizzle-orm/mysql-core |
| SQLite | better-sqlite3 / bun:sqlite | drizzle-orm/sqlite-core |
| PlanetScale | @planetscale/database | drizzle-orm/planetscale-serverless |
| Turso | @libsql/client | drizzle-orm/turso |
这个适配器层非常薄,只负责两件事:
- 将 Drizzle 的 SQL AST(抽象语法树)编译为对应数据库的方言
- 将数据库返回的行数据映射到 TypeScript 对象
因为适配器层薄,Drizzle 对每种数据库的特性支持是增量式的,而不是整体框架级别的。比如你要用 PostgreSQL 的 ON CONFLICT DO UPDATE(Upsert),Drizzle 有原生支持:
await db
.insert(users)
.values({ email: 'john@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John', updatedAt: new Date() },
});
而对于 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE,Drizzle 同样有对应的 API:
await db
.insert(users)
.values({ email: 'john@example.com', name: 'John' })
.onDuplicateKeyUpdate({
set: { name: 'John' },
});
三、基准测试解析:Bun + Drizzle 为什么能打败 Go
3.1 测试环境还原
2026 年 Drizzle 官方公布的基准测试条件:
- 硬件:Apple M4 Pro, 24GB RAM
- 操作系统:macOS Sequoia 15
- Drizzle + Bun:使用 Bun 的原生 HTTP 服务器(
Bun.serve)+bun:sqlite(SQLite 内存模式)+ Drizzle ORM - Go:使用
net/http+database/sql+lib/pq(PostgreSQL 驱动),连接同一台机器上的 PostgreSQL(本地 socket) - 测试工具:
wrk或oha(Go 编写的 HTTP 压测工具) - 查询负载:简单的增删改查(单表 CRUD),非联表复杂查询
3.2 数字背后的工程解释
Drizzle + Bun 的测试数据:
- 平均延迟:7.3ms
- 吞吐量:8.8k req/sec
Go v1.25.5 的测试数据:
- 平均延迟:18.1ms
- 吞吐量:8.2k req/sec
这个结果的工程解释分三层:
第一层:运行时开销。Bun 的 JavaScriptCore 引擎对热路径(hot path)代码做了 JIT 编译优化,当同一个数据库查询被反复执行时,V8 或 JavaScriptCore 会在运行时将其编译为高度优化的本地机器码。Drizzle 的查询构建过程(生成 SQL 字符串)在第一次调用后会被 V8/JSC 缓存,后续调用的实际成本接近于一次字符串插值操作。
Go 的 database/sql 包则有一个连接池管理器和查询参数化处理层,每次查询都需要从池中获取连接、执行参数化 SQL、处理结果集映射。Go 的 GC(垃圾回收器)也会周期性地暂停 goroutine 来回收内存,虽然 Go 1.21 之后的 GC 延迟已经大幅降低,但在高并发短查询场景下,GC pause 仍然是 P99 延迟的一个不可忽视的因素。
第二层:SQLite in-memory vs PostgreSQL socket。Drizzle + Bun 的测试用的是 SQLite in-memory 数据库,数据完全存在内存中,没有任何磁盘 IO。Go 的测试连接的是本地 PostgreSQL,虽然也是 Unix socket,但 PostgreSQL 的查询处理涉及解析器、规划器、执行器多个阶段,每次查询都有固定的计算成本。
第三层:HTTP 框架的差距。Bun 的 Bun.serve 是用 Zig/C++ 实现的高性能 HTTP 服务器,直接绑定到操作系统的 epoll/kqueue 事件循环上,处理 HTTP 请求的头解析和响应序列化几乎零成本。Go 的 net/http 虽然也使用了 epoll(通过 net 包的 poller),但 HTTP 请求/响应的编解码是在 Go 层面用反射和字符串操作完成的,这里有一些额外的 GC 压力。
3.3 正确看待这个基准测试
这个基准测试揭示的是特定场景下的相对优势,而不是「TypeScript 全面超越 Go」。我们需要区分两种场景:
适合 Drizzle + Bun 的场景:
- 单体应用或微服务的数据库层,读多写少
- 数据集较小(SQLite 内存模式)或者对延迟极为敏感
- 团队更熟悉 TypeScript/JavaScript 技术栈
- 需要在 serverless 环境中冷启动(如 Cloudflare Workers)
Go 仍然是更好选择:
- 复杂业务逻辑,大量 CPU 密集型计算
- 需要连接多个异构数据库做分布式事务
- 超高并发(100k+ req/sec)的生产系统
- 严格的内存占用控制(Go 的内存分配器比 V8 轻量得多)
关键警示:SQLite in-memory 不是生产环境的常见配置。在真实生产环境中,PostgreSQL/MySQL 的查询性能瓶颈通常在磁盘 IO 和锁竞争上,而不是驱动层的语言选择。盲目用这个基准测试来决定技术栈是不明智的。
四、实战:从零搭建 Drizzle + Bun + PostgreSQL 项目
4.1 项目初始化
mkdir drizzle-bun-demo && cd drizzle-bun-demo
bun init
bun add drizzle-orm postgres dotenv
bun add -d drizzle-kit @types/bun typescript
4.2 环境配置
// .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
4.3 完整 Schema 设计
// src/schema.ts
import {
pgTable,
serial,
text,
timestamp,
boolean,
integer,
uuid,
index,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// 用户表
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
avatarUrl: text('avatar_url'),
role: text('role').notNull().default('user'), // 'admin' | 'user' | 'moderator'
status: text('status').notNull().default('active'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
emailIdx: index('users_email_idx').on(table.email),
statusIdx: index('users_status_idx').on(table.status),
}));
// 文章表
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
authorId: uuid('author_id').notNull().references(() => users.id),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
excerpt: text('excerpt'),
content: text('content').notNull(),
status: text('status').notNull().default('draft'), // 'draft' | 'published' | 'archived'
viewCount: integer('view_count').notNull().default(0),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
slugIdx: index('posts_slug_idx').on(table.slug),
authorIdx: index('posts_author_idx').on(table.authorId),
statusIdx: index('posts_status_idx').on(table.status),
}));
// 标签表
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
slug: text('slug').notNull().unique(),
});
// 文章-标签关联表(多对多)
export const postTags = pgTable('post_tags', {
postId: uuid('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
tagId: integer('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
}, (table) => ({
pk: index('post_tags_pk').on(table.postId, table.tagId),
}));
// 关系定义
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
postTags: many(postTags),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
postTags: many(postTags),
}));
4.4 数据库连接与初始化
// src/db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import { eq, desc, count, sql } from 'drizzle-orm';
const connectionString = process.env.DATABASE_URL!;
// postgres-js 客户端配置(用于生产)
const client = postgres(connectionString, {
max: 10, // 连接池最大连接数
idle_timeout: 20,
connect_timeout: 10,
});
export const db = drizzle(client, { schema });
4.5 CRUD 操作实战
4.5.1 创建用户(带事务)
// src/services/users.ts
import { db } from '../db';
import { users, posts } from '../schema';
import { eq, desc, and, count, sql } from 'drizzle-orm';
export async function createUser(data: {
email: string;
name: string;
role?: string;
}) {
const [user] = await db
.insert(users)
.values({
email: data.email,
name: data.name,
role: data.role ?? 'user',
})
.returning();
return user;
}
export async function createUserWithFirstPost(data: {
email: string;
name: string;
postTitle: string;
postContent: string;
}) {
// 使用事务确保原子性
const result = await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ email: data.email, name: data.name })
.returning();
const [post] = await tx
.insert(posts)
.values({
authorId: user.id,
title: data.postTitle,
content: data.postContent,
status: 'published',
publishedAt: new Date(),
})
.returning();
return { user, post };
});
return result;
}
4.5.2 查询:复杂条件组合
export interface PostFilters {
authorId?: string;
status?: 'draft' | 'published' | 'archived';
tagId?: number;
searchQuery?: string;
page?: number;
pageSize?: number;
}
export async function getPosts(filters: PostFilters) {
const page = filters.page ?? 1;
const pageSize = filters.pageSize ?? 20;
const offset = (page - 1) * pageSize;
// 动态构建查询条件
const conditions = [];
if (filters.authorId) {
conditions.push(eq(posts.authorId, filters.authorId));
}
if (filters.status) {
conditions.push(eq(posts.status, filters.status));
}
if (filters.searchQuery) {
conditions.push(
sql`(${posts.title} ILIKE ${'%' + filters.searchQuery + '%'} OR ${posts.content} ILIKE ${'%' + filters.searchQuery + '%'})`
);
}
// 构建查询
const baseQuery = db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
excerpt: posts.excerpt,
status: posts.status,
viewCount: posts.viewCount,
publishedAt: posts.publishedAt,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
// 分页查询
const items = await baseQuery
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(posts.publishedAt))
.limit(pageSize)
.offset(offset);
// 计数查询(用于分页)
const [countResult] = await db
.select({ total: count() })
.from(posts)
.where(conditions.length > 0 ? and(...conditions) : undefined);
return {
items,
total: countResult.total,
page,
pageSize,
totalPages: Math.ceil(countResult.total / pageSize),
};
}
4.5.3 批量更新与软删除
export async function softDeleteOldPosts(olderThan: Date) {
const result = await db
.update(posts)
.set({
status: 'archived',
updatedAt: new Date(),
})
.where(
and(
eq(posts.status, 'draft'),
sql`${posts.createdAt} < ${olderThan}`
)
);
return result.rowCount;
}
export async function incrementViewCount(postId: string) {
await db
.update(posts)
.set({ viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, postId));
}
4.5.4 Upsert:按 slug 存在则更新,不存在则创建
export async function upsertPost(data: {
slug: string;
title: string;
content: string;
authorId: string;
status?: string;
}) {
const [post] = await db
.insert(posts)
.values({
slug: data.slug,
title: data.title,
content: data.content,
authorId: data.authorId,
status: data.status ?? 'draft',
publishedAt: data.status === 'published' ? new Date() : null,
})
.onConflictDoUpdate({
target: posts.slug,
set: {
title: data.title,
content: data.content,
status: data.status ?? 'draft',
publishedAt: data.status === 'published' ? new Date() : null,
updatedAt: new Date(),
},
})
.returning();
return post;
}
4.6 迁移管理
# 生成迁移文件(根据 schema 变更自动对比)
bunx drizzle-kit generate
# 查看生成的迁移 SQL(重要:上线前必须 review)
cat drizzle/0000_cool_rogue.sql
# 应用迁移到数据库
bunx drizzle-kit migrate
# 仅生成 SQL 但不执行(用于 CI/CD review)
bunx drizzle-kit check
# 重置数据库(仅开发环境!)
bunx drizzle-kit drop
Drizzle 的迁移文件是标准 SQL,便于 DBA review 和手动调整。这是它与 Prisma migrate 的一个重要区别:Prisma 的迁移文件是 JSON 格式的内部 DSL,数据库团队无法直接理解和修改。
五、与 Bun HTTP 服务的集成
5.1 RESTful API 完整示例
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { db } from './db';
import { users, posts } from './schema';
import { eq, desc } from 'drizzle-orm';
import { createUser, getPosts, upsertPost } from './services/users';
const app = new Hono();
// 中间件
app.use('*', logger());
app.use('*', cors({
origin: process.env.ALLOWED_ORIGIN ?? '*',
credentials: true,
}));
// 健康检查
app.get('/health', (c) => c.json({
status: 'ok',
timestamp: new Date().toISOString(),
}));
// 获取用户列表
app.get('/users', async (c) => {
const allUsers = await db
.select({
id: users.id,
name: users.name,
email: users.email,
role: users.role,
status: users.status,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.status, 'active'))
.orderBy(desc(users.createdAt))
.limit(100);
return c.json({ users: allUsers });
});
// 获取用户详情(含文章列表)
app.get('/users/:id', async (c) => {
const userId = c.req.param('id');
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: {
where: eq(posts.status, 'published'),
orderBy: desc(posts.publishedAt),
limit: 10,
},
},
});
if (!userWithPosts) {
return c.json({ error: 'User not found' }, 404);
}
return c.json({ user: userWithPosts });
});
// 获取文章列表(分页)
app.get('/posts', async (c) => {
const page = parseInt(c.req.query('page') ?? '1', 10);
const pageSize = parseInt(c.req.query('pageSize') ?? '20', 10);
const status = c.req.query('status') ?? 'published';
const result = await getPosts({
status: status as 'draft' | 'published' | 'archived',
page,
pageSize,
});
return c.json(result);
});
// 创建文章
app.post('/posts', async (c) => {
const body = await c.req.json<{
slug: string;
title: string;
content: string;
authorId: string;
status?: string;
}>();
if (!body.slug || !body.title || !body.content || !body.authorId) {
return c.json({ error: 'Missing required fields' }, 400);
}
const post = await upsertPost(body);
return c.json({ post }, 201);
});
// 统计 API
app.get('/stats', async (c) => {
const [userCount] = await db.select({ count: posts.authorId }).from(users);
const [postCount] = await db.select({ count: posts.id }).from(posts);
const [publishedCount] = await db
.select({ count: posts.id })
.from(posts)
.where(eq(posts.status, 'published'));
return c.json({
totalUsers: userCount?.count ?? 0,
totalPosts: postCount?.count ?? 0,
publishedPosts: publishedCount?.count ?? 0,
});
});
export default {
port: parseInt(process.env.PORT ?? '3000', 10),
fetch: app.fetch,
};
5.2 使用 Bun 原生 SQLite(适合轻量场景)
// src/db-sqlite.ts(Bun 原生 SQLite,性能更高)
import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { sql } from 'drizzle-orm';
const sqlite = new Database(':memory:'); // 或指定文件路径 './dev.db'
export const db = drizzle(sqlite);
// 初始化表结构
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
view_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// 在 Bun 中,sqlite.exec() 的性能几乎等同于直接 C 调用
// 比任何 Node.js 数据库驱动都快 2-5 倍
六、与其他 ORM 的横向对比
6.1 功能矩阵
| 维度 | Drizzle | Prisma | TypeORM | Sequelize |
|---|---|---|---|---|
| Schema 定义方式 | TypeScript 代码 | .prisma DSL | 装饰器/模型类 | 模型定义对象 |
| 类型安全级别 | 完整列级类型 | 完整列级类型 | 部分(运行时丢失) | 部分(运行时丢失) |
| 运行时抽象 | 无 | 中等 | 高 | 高 |
| 迁移方式 | 标准 SQL 文件 | JSON DSL | TypeScript/CLI | CLI |
| 关系查询 API | 声明式 + relations | 隐式 include | 懒加载/Eager | 同上 |
| 多数据库支持 | PostgreSQL/MySQL/SQLite/Turso/PlanetScale | PostgreSQL/MySQL/SQLite | 15+ 数据库 | 6+ 数据库 |
| 活跃度(2026) | 非常活跃 | 活跃 | 一般 | 一般 |
| Bundle 体积 | ~50KB(无运行时) | ~300KB+(Prisma Client) | ~200KB | ~150KB |
| Serverless 友好度 | 极高(bun:sqlite, Cloudflare D1) | 中等 | 低 | 低 |
| 社区生态 | 快速增长 | 成熟 | 成熟 | 成熟 |
6.2 性能横向对比
在一个等价的 CRUD 场景下(单表查询 + 分页 + 过滤),各 ORM 的相对性能:
Drizzle + Bun (SQLite in-memory) ████████████████████ 基准: 1x
Drizzle + Bun (PostgreSQL) ████████████████ 约 0.7x
Drizzle + Node.js (PostgreSQL) ██████████████ 约 0.6x
Prisma + Node.js (PostgreSQL) ████████████ 约 0.5x
TypeORM + Node.js (PostgreSQL) ██████████ 约 0.45x
Sequelize + Node.js (PostgreSQL) █████████ 约 0.4x
这个对比的前提是:数据集在内存中、无网络延迟、查询简单。当查询变复杂(多表 JOIN、聚合分析)时,数据库自身的查询规划器的权重增加,ORM 层的差异会缩小。
6.3 迁移成本评估
如果你正在使用 Prisma 并考虑迁移到 Drizzle,评估维度:
迁移成本高:
- Schema 重写(
.prisma→ TypeScript schema) - 查询 API 重写(Prisma Client API → Drizzle query builder)
- 关系查询逻辑重写(include → explicit join)
迁移成本低:
- 迁移文件:Drizzle 的 SQL 迁移比 Prisma 的 JSON DSL 更易理解
- 类型系统:Drizzle 的 TypeScript-first 设计通常比 Prisma 的 DSL 更直观
实际建议:对于已有生产数据的大型项目,迁移需要谨慎评估。对于新项目,Drizzle 已经是 TypeScript + PostgreSQL 的最优选择之一。
七、生产环境最佳实践
7.1 连接池配置
// 生产环境 PostgreSQL 连接池配置
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const maxConnections = parseInt(process.env.MAX_DB_CONNECTIONS ?? '10', 10);
const connectTimeout = 10; // 秒
const idleTimeout = 20; // 秒
const client = postgres(process.env.DATABASE_URL!, {
max: maxConnections,
connect_timeout: connectTimeout,
idle_timeout: idleTimeout,
// 生产环境必须开启 SSL
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
export const db = drizzle(client, { schema });
连接池大小经验法则:对于 CPU 密集型应用,max = CPU 核心数 × 2;对于 IO 密集型应用(数据库查询),max = CPU 核心数 × 4 到 max = CPU 核心数 × 8 之间。需要通过压测找到最优值。
7.2 查询超时与重试
import { sql } from 'drizzle-orm';
async function queryWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelayMs = 100
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// 只对临时性错误重试(连接断开、超时)
const isRetryable =
error?.code === 'ECONNRESET' ||
error?.code === 'ETIMEDOUT' ||
error?.code === 'connection terminated' ||
error?.code === ' deadlock detected';
if (!isRetryable || attempt === maxRetries - 1) {
throw error;
}
// 指数退避
await new Promise((resolve) =>
setTimeout(resolve, baseDelayMs * Math.pow(2, attempt))
);
}
}
throw lastError!;
}
// 使用
const user = await queryWithRetry(() =>
db.select().from(users).where(eq(users.id, userId)).limit(1)
);
7.3 索引优化
Drizzle 的 Schema 支持直接定义索引(上面的 Schema 中已展示),但更重要的是知道哪些字段需要索引:
// 高频查询字段 → 需要索引
export const posts = pgTable('posts', {
// ...
}, (table) => ({
// WHERE status = 'published' 频繁使用
statusIdx: index('posts_status_idx').on(table.status),
// WHERE author_id = ? 频繁使用(关联查询)
authorIdx: index('posts_author_idx').on(table.authorId),
// WHERE slug = ?(文章详情页)
slugIdx: index('posts_slug_idx').on(table.slug).unique(),
}));
// 复合索引(同时查询多个条件)
export const orderItems = pgTable('order_items', {
// ...
}, (table) => ({
// WHERE order_id = ? AND product_id = ?
orderProductIdx: index('order_items_composite_idx').on(
table.orderId,
table.productId
),
}));
索引不是免费的:每个索引都会增加 INSERT/UPDATE 的写开销(约 2-3 倍写入时间增加)。对于写入密集型表(如日志表),索引数量要谨慎控制。
7.4 日志与可观测性
// 慢查询日志(生产环境必须)
const slowQueryThreshold = 100; // ms
const client = postgres(process.env.DATABASE_URL!, {
max: 10,
onnotice: (notice) => {
// PostgreSQL 的 NOTICE 消息(如锁等待)
console.warn('[DB NOTICE]', notice.message);
},
});
// 包装 db,每次查询记录执行时间
async function loggedQuery<T>(label: string, fn: () => Promise<T>): Promise<T> {
const start = performance.now();
try {
const result = await fn();
const duration = performance.now() - start;
if (duration > slowQueryThreshold) {
console.warn(`[SLOW QUERY] ${label}: ${duration.toFixed(2)}ms`);
}
return result;
} catch (error) {
const duration = performance.now() - start;
console.error(`[QUERY ERROR] ${label}: ${duration.toFixed(2)}ms`, error);
throw error;
}
}
7.5 生产环境部署检查清单
-
DATABASE_URL使用环境变量,不硬编码在代码中 - 生产数据库连接开启 SSL/TLS
- 连接池
max值根据压测结果调整 - 所有迁移 SQL 经过 DBA review
- 慢查询阈值设置并监控(建议 P99 > 200ms 报警)
- Schema 中的所有
text字段设置合理的长度限制(防止恶意超长数据) - 开启 PostgreSQL 的
pg_stat_statements扩展来追踪最慢的 SQL 语句 - 为高频查询建立覆盖索引(Covering Index),减少回表查询
八、适用场景与局限性
8.1 Drizzle 最适合的场景
场景一:全栈 TypeScript 团队。团队从数据库到前端都是 TypeScript,不想引入额外的技术栈差异。Drizzle 的 Schema 定义和业务代码在同一语言、同一仓库内,减少了上下文切换。
场景二:性能敏感的后端 API。CRUD 操作的 P99 延迟要求在 50ms 以内。Drizzle 的零抽象层设计可以消除大部分 ORM 开销。
场景三:Serverless / Edge 部署。Bun + Drizzle + SQLite(如 Cloudflare D1、Turso)的组合,可以在边缘节点上提供极快的冷启动和本地数据访问。
场景四:数据建模变动频繁的早期项目。Drizzle 的迁移系统和 Schema 即代码的设计,让数据库 Schema 的演进和 TypeScript 源码的演进完全同步,没有 Prisma .prisma 文件那种「隔离感」。
8.2 Drizzle 的局限性
局限性一:学习曲线比 Prisma 陡。Drizzle 要求开发者有较好的 SQL 基础,它的 query builder 是「SQL 的 TypeScript 表达」,而不是「隐藏 SQL 的高级抽象」。对于 SQL 不熟悉的开发者,Prisma 的 query API 更直觉。
局限性二:生态工具链还在成熟。Prisma Studio(可视化数据库浏览器)、Prisma Accelerate(边缘缓存)这些配套设施,Drizzle 目前还没有对应的产品。虽然有第三方工具,但整体生态不如 Prisma 完整。
局限性三:不支持主动迁移(Proactive Migration)。Drizzle 的迁移需要开发者主动触发 drizzle-kit generate,它不会自动检测 Schema 变更并生成迁移。这与 Prisma 的 prisma migrate dev 工作流有所不同。
局限性四:复杂聚合查询的 DX 仍有提升空间。当需要写复杂的 window function、CTE(公用表表达式)、多层级聚合查询时,Drizzle 的 query builder 会变得很冗长,此时直接写 raw SQL 反而更清晰,但使用 raw SQL 意味着失去部分类型安全。
九、总结与展望
Drizzle ORM 的出现,回答了一个被问了很久的问题:TypeScript 生态能否在数据库层达到与 Go、Java 等编译型语言相当的性能? 答案是:可以,但有条件。
Drizzle 的性能优势来自三个设计决策:
- SQL-first — 不在 SQL 之上叠加自己的抽象层
- 编译时类型安全 — 类型信息在编译时完全保留,不依赖运行时反射
- 零运行时开销 — 没有自己的运行时、连接池或查询计划器,交给底层驱动
这三个决策的代价是:开发者需要懂 SQL。但对于专业开发者来说,这不是一个代价,而是一个前提。
2026 年,随着 Bun 1.0 的成熟、TypeScript 5.x 的泛型改进、以及 Drizzle v1.0 的稳定发布,TypeScript 全栈的性能上限已经被大幅提升。对于 CRUD 密集型应用,Drizzle + Bun 的组合已经可以在延迟和吞吐量上与 Go 正面竞争。
Evan You 的那句转发说明了一个更大的趋势:工具链的性能和 DX 正在成为 TypeScript 生态最重要的竞争维度。当 Prisma 把 TypeScript 开发者从 SQL 的繁琐中解放出来,Drizzle 则在这个基础上更进了一步——它不仅解放了开发者,还确保了数据库层的性能不会成为系统的瓶颈。
下一个问题是:Drizzle 的生态工具链能否追上 Prisma 的成熟度?如果答案是肯定的,TypeScript 全栈在后端领域的天花板将再一次被抬高。
参考资源:
- Drizzle 官方文档:https://orm.drizzle.team/
- Drizzle GitHub:https://github.com/drizzle-team/drizzle-orm
- Bun 官方文档:https://bun.sh/
- postgres-js:https://github.com/porsager/postgres
- Drizzle Kit CLI:https://orm.drizzle.team/docs/kit-overview