在创建API、后端或数据模型时主动应用。触发条件包括PostgreSQL、Postgres、Drizzle、drizzle-orm、drizzle-kit、数据库、模式、pgTable、表、列、索引、查询、迁移、ORM、关系、关系查询、连接、事务、SQL、连接池、PgBouncer、N+1、JSONB、RLS、全文搜索、分区。在编写数据库模式、查询、迁移、连接设置或任何与数据库相关的代码时使用。PostgreSQL和Drizzle ORM最佳实践。
PostgreSQL + Drizzle ORM
使用PostgreSQL 17/18和Drizzle ORM构建类型安全的数据库应用程序。
版本检查(首先执行)
Drizzle的API在稳定的0.x系列和v1.0之间发生了重大变化。在编写代码之前检查package.json,因为两种关系API不兼容,不能混用:
drizzle-orm版本 |
关系API | 查询过滤器 |
|---|---|---|
^0.x(npm latest) |
每个表使用relations(),drizzle(client, { schema }) |
where: eq(users.id, id) |
1.0.0-beta.* / 1.0.0-rc.* |
一次性为所有表使用defineRelations(),drizzle(client, { relations }) |
where: { id: userId }(对象风格) |
官方文档站点(orm.drizzle.team)在其主要页面上记录了v1.0语法。本技能默认使用稳定的0.x语法;对于v1.0项目,请阅读references/RELATIONS.md中的“关系查询v2”部分。
表明项目使用v1.0的信号:defineRelations导入、对象风格的where、关系中的r.many.posts()、使用from/to键而不是fields/references。
基本命令
npx drizzle-kit generate # 从模式更改生成SQL迁移
npx drizzle-kit migrate # 应用待处理的迁移
npx drizzle-kit push # 直接推送模式(仅限开发/原型设计)
npx drizzle-kit pull # 将现有数据库反向工程为模式文件
npx drizzle-kit studio # 打开数据库浏览器
npx drizzle-kit check # 检测迁移冲突(竞争条件)
快速决策树
“如何建模这种关系?”
关系类型?
├─ 一对多(用户有帖子) → 在“多”侧添加外键 + relations()
├─ 多对多(帖子有标签) → 连接表,使用复合主键 + relations()
├─ 一对一(用户有个人资料) → 外键加唯一约束
└─ 自引用(评论) → 指向同一表的外键(将引用类型设为AnyPgColumn)
“为什么我的查询慢?”
查询慢?
├─ WHERE/JOIN列缺少索引 → 添加索引(Postgres不会自动为外键创建索引)
├─ 循环中逐行查询(N+1) → 使用关系查询(`with:`)或连接
├─ 全表扫描 → EXPLAIN (ANALYZE, BUFFERS),添加索引
├─ 大OFFSET分页 → 切换到游标/键集分页
└─ 每个请求的连接开销 → 连接池(pg Pool / postgres.js / PgBouncer)
“使用哪个drizzle-kit命令?”
我需要什么?
├─ 模式已更改,需要版本化SQL → drizzle-kit generate,审查SQL,然后migrate
├─ 应用迁移(CI、生产) → drizzle-kit migrate(或在代码中使用migrate())
├─ 快速本地迭代,临时数据库 → drizzle-kit push
├─ 在现有数据库上采用Drizzle → drizzle-kit pull
└─ 手写SQL(触发器、回填)→ drizzle-kit generate --custom
连接设置
// node-postgres — 传入URL,Drizzle会为你创建Pool
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema';
export const db = drizzle(process.env.DATABASE_URL!, { schema });
// postgres.js — 内置连接池;在事务模式连接池(PgBouncer/Supavisor)后面设置prepare: false,除非它支持预处理语句
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!, { max: 20 });
export const db = drizzle(client, { schema });
传入schema是启用db.query.*关系查询的关键——忘记传入是“类型上不存在属性'users'”的最常见原因。
可选:drizzle(url, { schema, casing: 'snake_case' })将camelCase的TS键映射到snake_case列,这样你可以编写pgTable('users', { createdAt: timestamp() })而无需重复列名。在drizzle.config.ts中设置相同的casing。
模式模式
带时间戳的基本表
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.defaultNow()
.notNull()
.$onUpdate(() => new Date()),
});
优先使用timestamp(..., { withTimezone: true })(timestamptz)——无时区的时间戳会导致静默时区错误。对于整数主键,优先使用integer().primaryKey().generatedAlwaysAsIdentity()而不是serial()(PostgreSQL推荐的形式;serial是遗留的)。
带索引的外键
import { index } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
title: varchar('title', { length: 255 }).notNull(),
}, (table) => [
// Postgres不会为外键列创建索引——添加一个,否则JOIN或级联操作会扫描全表
index('posts_user_id_idx').on(table.userId),
]);
第三个pgTable参数返回一个数组(旧的对象形式已弃用)。
关系(稳定的0.x API)
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.userId], references: [users.id] }),
}));
relations()是db.query.*的应用程序级元数据——它不会创建外键约束。两者都要定义(.references()用于数据库,relations()用于查询)。
查询模式
import { eq } from 'drizzle-orm';
// 关系查询——一次往返获取嵌套数据,无N+1
const usersWithPosts = await db.query.users.findMany({
with: { posts: true },
});
// SQL风格查询——过滤器、连接、聚合
const activeUsers = await db
.select()
.from(users)
.where(eq(users.status, 'active'));
// 事务——所有语句一起提交或回滚
await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({ email }).returning();
await tx.insert(profiles).values({ userId: user.id });
});
在事务内部,始终使用tx,而不是db——在db上的查询会逃出事务,不会回滚。
性能检查清单
| 优先级 | 检查项 | 影响 |
|---|---|---|
| 严重 | 索引所有外键 | 防止JOIN和级联删除的全表扫描 |
| 严重 | 对嵌套数据使用关系查询或连接 | 避免N+1 |
| 高 | 生产环境使用连接池 | 每个PG连接消耗约MB内存 |
| 高 | 对慢查询使用EXPLAIN (ANALYZE, BUFFERS) |
识别缺失的索引 |
| 中 | 对过滤子集使用部分索引 | 更小、更快的索引 |
| 中 | 使用UUIDv7(uuidv7(),PG18+)或身份列作为主键 |
比UUIDv4更好的索引局部性 |
反模式
| 反模式 | 问题 | 修复 |
|---|---|---|
| 外键无索引 | JOIN和级联操作慢 | 在每个外键列上添加索引 |
| 循环中的N+1 | 每行查询 | with:关系查询或连接 |
| 每个请求一个连接 | 连接风暴,内存耗尽 | pg Pool / postgres.js max / PgBouncer |
生产环境使用push |
无历史记录,数据丢失提示 | generate + migrate |
混用0.x relations()和v1.0 defineRelations |
类型错误,db.query损坏 |
每个项目选择一种(参见版本检查) |
将JSON存储为text |
无验证,无索引 | jsonb()列 + GIN索引 |
无时区的timestamp |
静默时区错误 | { withTimezone: true } |
| 编辑已应用的迁移文件 | 校验和不匹配,漂移 | 新迁移(generate / generate --custom) |
参考文档
| 阅读此文档 | 当你... |
|---|---|
| references/SCHEMA.md | 定义表:列类型、约束、索引、枚举、生成列 |
| references/QUERIES.md | 编写select、insert、upsert、事务、预处理语句 |
| references/RELATIONS.md | 建模关系或使用db.query.*——包括v1.0 RQB v2 API |
| references/MIGRATIONS.md | 配置drizzle-kit,生成/应用迁移,自定义SQL |
| references/POSTGRES.md | 使用PG17/18特性,RLS,分区,JSONB操作,全文搜索 |
| references/PERFORMANCE.md | 索引策略,EXPLAIN,连接池,分页,批量操作 |
| references/CHEATSHEET.md | 需要上述任何内容的紧凑语法提醒 |
资源
- Drizzle ORM文档:https://orm.drizzle.team(记录v1.0语法;参见版本检查)
- Drizzle GitHub:https://github.com/drizzle-team/drizzle-orm
- PostgreSQL文档:https://www.postgresql.org/docs/current/
- 行级安全:https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- 索引类型:https://www.postgresql.org/docs/current/indexes-types.html






