--- name: postgres-drizzle description: | PostgreSQL and Drizzle ORM best practices. Triggers on: PostgreSQL, Postgres, Drizzle, database, schema, tables, columns, indexes, queries, migrations, ORM, relations, joins, transactions, SQL, drizzle-kit, connection pooling, N+1, JSONB, RLS. Use when: writing database schemas, queries, migrations, or any database-related code. Proactively apply when creating APIs, backends, or data models. --- # PostgreSQL + Drizzle ORM Type-safe database applications with PostgreSQL 18 and Drizzle ORM. ## Essential Commands ```bash npx drizzle-kit generate # Generate migration from schema changes npx drizzle-kit migrate # Apply pending migrations npx drizzle-kit push # Push schema directly (dev only!) npx drizzle-kit studio # Open database browser ``` ## Quick Decision Trees ### "How do I model this relationship?" ``` Relationship type? ├─ One-to-many (user has posts) → FK on "many" side + relations() ├─ Many-to-many (posts have tags) → Junction table + relations() ├─ One-to-one (user has profile) → FK with unique constraint └─ Self-referential (comments) → FK to same table ``` ### "Why is my query slow?" ``` Slow query? ├─ Missing index on WHERE/JOIN columns → Add index ├─ N+1 queries in loop → Use relational queries API ├─ Full table scan → EXPLAIN ANALYZE, add index ├─ Large result set → Add pagination (limit/offset) └─ Connection overhead → Enable connection pooling ``` ### "Which drizzle-kit command?" ``` What do I need? ├─ Schema changed, need SQL migration → drizzle-kit generate ├─ Apply migrations to database → drizzle-kit migrate ├─ Quick dev iteration (no migration) → drizzle-kit push └─ Browse/edit data visually → drizzle-kit studio ``` ## Directory Structure ``` src/db/ ├── schema/ │ ├── index.ts # Re-export all tables │ ├── users.ts # Table + relations │ └── posts.ts # Table + relations ├── db.ts # Connection with pooling └── migrate.ts # Migration runner drizzle/ └── migrations/ # Generated SQL files drizzle.config.ts # drizzle-kit config ``` ## Schema Patterns ### Basic Table with Timestamps ```typescript export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), email: varchar('email', { length: 255 }).notNull().unique(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); ``` ### Foreign Key with Index ```typescript export const posts = pgTable('posts', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').notNull().references(() => users.id), title: varchar('title', { length: 255 }).notNull(), }, (table) => [ index('posts_user_id_idx').on(table.userId), // ALWAYS index FKs ]); ``` ### Relations ```typescript export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })); export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.userId], references: [users.id] }), })); ``` ## Query Patterns ### Relational Query (Avoid N+1) ```typescript // ✓ Single query with nested data const usersWithPosts = await db.query.users.findMany({ with: { posts: true }, }); ``` ### Filtered Query ```typescript const activeUsers = await db .select() .from(users) .where(eq(users.status, 'active')); ``` ### Transaction ```typescript await db.transaction(async (tx) => { const [user] = await tx.insert(users).values({ email }).returning(); await tx.insert(profiles).values({ userId: user.id }); }); ``` ## Performance Checklist | Priority | Check | Impact | |----------|-------|--------| | CRITICAL | Index all foreign keys | Prevents full table scans on JOINs | | CRITICAL | Use relational queries for nested data | Avoids N+1 | | HIGH | Connection pooling in production | Reduces connection overhead | | HIGH | `EXPLAIN ANALYZE` slow queries | Identifies missing indexes | | MEDIUM | Partial indexes for filtered subsets | Smaller, faster indexes | | MEDIUM | UUIDv7 for PKs (PG18+) | Better index locality | ## Anti-Patterns (CRITICAL) | Anti-Pattern | Problem | Fix | |--------------|---------|-----| | **No FK index** | Slow JOINs, full scans | Add index on every FK column | | **N+1 in loops** | Query per row | Use `with:` relational queries | | **No pooling** | Connection per request | Use `@neondatabase/serverless` or similar | | **`push` in prod** | Data loss risk | Always use `generate` + `migrate` | | **Storing JSON as text** | No validation, bad queries | Use `jsonb()` column type | ## Reference Documentation | File | Purpose | |------|---------| | [references/SCHEMA.md](references/SCHEMA.md) | Column types, constraints | | [references/QUERIES.md](references/QUERIES.md) | Operators, joins, aggregations | | [references/RELATIONS.md](references/RELATIONS.md) | One-to-many, many-to-many | | [references/MIGRATIONS.md](references/MIGRATIONS.md) | drizzle-kit workflows | | [references/POSTGRES.md](references/POSTGRES.md) | PG18 features, RLS, partitioning | | [references/PERFORMANCE.md](references/PERFORMANCE.md) | Indexing, optimization | | [references/CHEATSHEET.md](references/CHEATSHEET.md) | Quick reference | ## Resources - **Drizzle Docs**: https://orm.drizzle.team - **PostgreSQL 18**: https://www.postgresql.org/docs/18/