编程 Drizzle ORM + Bun 性能革命:当 TypeScript 生态终于在数据库层跑出比 Go 更低的延迟

2026-07-11 17:15:17 +0800 CST views 10

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 的核心层是数据库无关的,通过适配器模式支持多种数据库:

数据库驱动适配器包
PostgreSQLpostgres / pgdrizzle-orm/pg-core
MySQLmysql2drizzle-orm/mysql-core
SQLitebetter-sqlite3 / bun:sqlitedrizzle-orm/sqlite-core
PlanetScale@planetscale/databasedrizzle-orm/planetscale-serverless
Turso@libsql/clientdrizzle-orm/turso

这个适配器层非常薄,只负责两件事:

  1. 将 Drizzle 的 SQL AST(抽象语法树)编译为对应数据库的方言
  2. 将数据库返回的行数据映射到 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)
  • 测试工具wrkoha(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 功能矩阵

维度DrizzlePrismaTypeORMSequelize
Schema 定义方式TypeScript 代码.prisma DSL装饰器/模型类模型定义对象
类型安全级别完整列级类型完整列级类型部分(运行时丢失)部分(运行时丢失)
运行时抽象中等
迁移方式标准 SQL 文件JSON DSLTypeScript/CLICLI
关系查询 API声明式 + relations隐式 include懒加载/Eager同上
多数据库支持PostgreSQL/MySQL/SQLite/Turso/PlanetScalePostgreSQL/MySQL/SQLite15+ 数据库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 核心数 × 4max = 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 的性能优势来自三个设计决策:

  1. SQL-first — 不在 SQL 之上叠加自己的抽象层
  2. 编译时类型安全 — 类型信息在编译时完全保留,不依赖运行时反射
  3. 零运行时开销 — 没有自己的运行时、连接池或查询计划器,交给底层驱动

这三个决策的代价是:开发者需要懂 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

推荐文章

PostgreSQL日常运维命令总结分享
2024-11-18 06:58:22 +0800 CST
程序员出海搞钱工具库
2024-11-18 22:16:19 +0800 CST
Vue3 结合 Driver.js 实现新手指引
2024-11-18 19:30:14 +0800 CST
使用 sync.Pool 优化 Go 程序性能
2024-11-19 05:56:51 +0800 CST
Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
随机分数html
2025-01-25 10:56:34 +0800 CST
Dropzone.js实现文件拖放上传功能
2024-11-18 18:28:02 +0800 CST
Elasticsearch 监控和警报
2024-11-19 10:02:29 +0800 CST
程序员茄子在线接单