<!-- title: Claude Code数据库全栈：PostgreSQL七步实战 -->

# Claude Code数据库全栈：PostgreSQL七步实战

**学习目标**：掌握用Claude Code完成PostgreSQL项目从Schema设计、Migration管理、MCP直连、查询优化到种子数据的完整工作流。适合有基本SQL基础、想用AI提升数据库开发效率的后端开发者。

**前置条件**：Node.js 18+、PostgreSQL 15+（本地或远程）、Claude Code CLI已安装认证。

---

## 第一步：项目初始化与CLAUDE.md配置

数据库项目比普通前端项目更依赖上下文——表结构关系、命名规范、Migration策略一旦偏离，修复成本极高。CLAUDE.md是Claude Code理解你数据库世界的入口。

### 1.1 创建项目骨架

```bash
mkdir pg-claude-demo && cd pg-claude-demo
git init
npm init -y
npm install drizzle-orm pg dotenv
npm install -D drizzle-kit tsx typescript @types/pg
```

### 1.2 编写数据库专属CLAUDE.md

```markdown
# Database Project

## 技术栈
- PostgreSQL 16 + Drizzle ORM
- drizzle-kit 管理migration
- TypeScript strict mode

## 数据库规范
- 表名：snake_case复数（users, order_items）
- 列名：snake_case（created_at, user_id）
- 主键：id bigint GENERATED ALWAYS AS IDENTITY
- 外键：{关联表单数}_id（user_id, order_id）
- 时间列：created_at/updated_at TIMESTAMPTZ DEFAULT now()
- 软删除：deleted_at TIMESTAMPTZ NULL
- 金额：numeric(12,2) 不用float
- 枚举：PostgreSQL ENUM类型，不用varchar

## Migration规范
- 每个migration只做一件事
- 先加列后删列，分两步migration
- 新列必须有DEFAULT或允许NULL
- 禁止在migration中SELECT/INSERT大数据
- 文件名：0001_create_users.sql 顺序编号
- 回滚脚本与正向脚本同级目录

## 查询规范
- 禁止SELECT *，明确列出列
- JOIN超过3张表必须写注释
- 分页必须用OFFSET+LIMIT或cursor
- 大批量操作用COPY代替INSERT
```

这份配置告诉Claude Code你项目的"宪法"——命名风格、主键策略、Migration规则，后续所有生成都会遵循。

### 1.3 预期结果

```
pg-claude-demo/
├── CLAUDE.md          # 项目规范
├── package.json
├── tsconfig.json
├── drizzle.config.ts  # 下一步创建
├── src/
│   ├── db/
│   │   ├── schema.ts  # Drizzle schema定义
│   │   ├── index.ts   # 连接池
│   │   └── migrations/
│   └── app.ts
└── .env               # DATABASE_URL
```

### 1.4 常见问题

| 问题 | 解决 |
|------|------|
| Claude生成camelCase列名 | 在CLAUDE.md加`All column names MUST be snake_case` |
| Migration缺少回滚脚本 | 明确要求`Every migration must have a paired rollback .sql` |
| 生成float存金额 | 规范已写`金额：numeric(12,2)`，如仍出错再强调 |

---

## 第二步：Schema设计与Drizzle建模

不要手写SQL DDL——用Drizzle的TypeScript schema让Claude Code生成，类型安全且可逆向同步。

### 2.1 配置Drizzle Kit

```typescript
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./src/db/migrations",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
```

### 2.2 用Claude Code生成Schema

向Claude Code发出精确的Prompt：

```
基于CLAUDE.md的规范，为电商系统设计Drizzle ORM schema，包含以下表：
1. users - 用户（email唯一、密码哈希、角色enum）
2. products - 商品（名称、价格numeric(12,2)、库存、状态enum）
3. orders - 订单（用户外键、总额、状态enum、地址JSON）
4. order_items - 订单项（订单外键、商品外键、数量、单价）

要求：
- 使用pgTable，每个表都要created_at/updated_at
- 外键关系用relations()定义
- 用户和商品都支持软删除
- 导出所有类型（inferSelectModel/inferInsertModel）
```

Claude Code会生成完整的schema文件：

```typescript
// src/db/schema.ts
import {
  pgTable, bigint, varchar, text, numeric, integer,
  timestamp, pgEnum, jsonb,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// 枚举定义
export const userRoleEnum = pgEnum("user_role", ["customer", "admin", "vendor"]);
export const productStatusEnum = pgEnum("product_status", ["draft", "active", "archived"]);
export const orderStatusEnum = pgEnum("order_status", ["pending", "paid", "shipped", "delivered", "cancelled"]);

// 用户表
export const users = pgTable("users", {
  id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
  email: varchar({ length: 255 }).notNull().unique(),
  passwordHash: text("password_hash").notNull(),
  name: varchar({ length: 100 }),
  role: userRoleEnum().default("customer").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
  deletedAt: timestamp("deleted_at", { withTimezone: true }),
});

// 商品表
export const products = pgTable("products", {
  id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
  name: varchar({ length: 255 }).notNull(),
  description: text(),
  price: numeric({ precision: 12, scale: 2 }).notNull(),
  stock: integer().default(0).notNull(),
  status: productStatusEnum().default("draft").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
  deletedAt: timestamp("deleted_at", { withTimezone: true }),
});

// 订单表
export const orders = pgTable("orders", {
  id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
  userId: bigint("user_id", { mode: "number" }).notNull().references(() => users.id),
  totalAmount: numeric("total_amount", { precision: 12, scale: 2 }).notNull(),
  status: orderStatusEnum().default("pending").notNull(),
  shippingAddress: jsonb("shipping_address").$type<{
    line1: string; city: string; zip: string; country: string;
  }>(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});

// 订单项表
export const orderItems = pgTable("order_items", {
  id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
  orderId: bigint("order_id", { mode: "number" }).notNull().references(() => orders.id),
  productId: bigint("product_id", { mode: "number" }).notNull().references(() => products.id),
  quantity: integer().notNull(),
  unitPrice: numeric("unit_price", { precision: 12, scale: 2 }).notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});

// 关系定义
export const usersRelations = relations(users, ({ many }) => ({
  orders: many(orders),
}));

export const productsRelations = relations(products, ({ many }) => ({
  orderItems: many(orderItems),
}));

export const ordersRelations = relations(orders, ({ one, many }) => ({
  user: one(users, { fields: [orders.userId], references: [users.id] }),
  items: many(orderItems),
}));

export const orderItemsRelations = relations(orderItems, ({ one }) => ({
  order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
  product: one(products, { fields: [orderItems.productId], references: [products.id] }),
}));

// 类型导出
export type User = typeof users.$inferSelectModel;
export type NewUser = typeof users.$inferInsertModel;
export type Product = typeof products.$inferSelectModel;
export type Order = typeof orders.$inferSelectModel;
export type OrderItem = typeof orderItems.$inferSelectModel;
```

### 2.3 连接池初始化

```typescript
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import * as schema from "./schema";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30000,
});

export const db = drizzle(pool, { schema });
export { pool };
```

---

## 第三步：Migration生成与执行策略

Migration是数据库项目中最危险的操作。Claude Code能生成migration，但你需要一套验证流程。

### 3.1 Drizzle Kit自动生成

```bash
# 从schema生成migration SQL
npx drizzle-kit generate

# 输出结构：
# src/db/migrations/
# ├── 0000_marvelous_weapon_omega.sql   # 正向
# └── meta/
#     ├── _journal.json
#     └── 0000_snapshot.json
```

### 3.2 编写幂等Migration的最佳实践

当Claude Code生成migration时，要求它遵循幂等模式：

```sql
-- src/db/migrations/0001_add_product_category.sql

-- 幂等：IF NOT EXISTS
DO $$ BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_type WHERE typname = 'product_category'
  ) THEN
    CREATE TYPE product_category AS ENUM (
      'electronics', 'clothing', 'food', 'books', 'other'
    );
  END IF;
END $$;

-- 加列：带DEFAULT，对现有行安全
ALTER TABLE products
  ADD COLUMN IF NOT EXISTS category product_category
  DEFAULT 'other';

-- 加索引：CONCURRENTLY不锁表
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_category
  ON products (category);

-- 加注释
COMMENT ON COLUMN products.category IS 'Product classification for filtering';
```

### 3.3 回滚脚本配对

每个正向migration必须配对回滚：

```sql
-- src/db/migrations/0001_add_product_category_rollback.sql

DROP INDEX CONCURRENTLY IF EXISTS idx_products_category;
ALTER TABLE products DROP COLUMN IF EXISTS category;

-- 注意：不删除ENUM类型，因为可能有其他引用
-- 如果确认无引用：
-- DROP TYPE IF EXISTS product_category;
```

### 3.4 用Claude Code审查Migration

关键操作：不要盲信AI生成的migration。用Claude Code做预检：

```
审查 src/db/migrations/0001_add_product_category.sql：

1. 这个migration在12M行的products表上执行是否安全？
2. 是否会造成锁表？预估执行时间？
3. 回滚脚本是否完整？
4. 是否遗漏了索引？
5. 对外键约束有没有影响？
```

Claude Code会分析SQL并给出具体建议，例如：

> `ALTER TABLE products ADD COLUMN` 在PostgreSQL中对大表是安全的（不重写表），但 `CREATE INDEX` 应该用 `CONCURRENTLY` 避免锁写。回滚脚本需要先删索引再删列。

### 3.5 执行Migration

```bash
# 开发环境：直接推送schema到数据库
npx drizzle-kit push

# 生产环境：执行已审查的migration文件
npx drizzle-kit migrate

# 验证：检查数据库是否与schema一致
npx drizzle-kit check
```

---

## 第四步：MCP直连PostgreSQL

让Claude Code直接读取数据库状态——表结构、索引、数据分布——而不需要你手动复制粘贴。

### 4.1 方案一：DBHub MCP Server

DBHub是目前最成熟的数据库MCP，支持PostgreSQL、MySQL、SQLite、SQL Server。

```json
// .claude/settings.json 或 ~/.claude/settings.json
{
  "mcpServers": {
    "dbhub": {
      "command": "npx",
      "args": ["-y", "@anthropic/dbhub", "--url", "postgresql://user:pass@localhost:5432/mydb"]
    }
  }
}
```

DBHub提供的核心能力：

| 工具 | 用途 |
|------|------|
| `list-tables` | 列出所有表和行数 |
| `describe-table` | 表结构、索引、约束 |
| `read-rows` | 安全读取数据（只读） |
| `execute-query` | 执行SQL（需授权） |
| `list-constraints` | 外键和唯一约束 |

### 4.2 方案二：PostgreSQL MCP Server（读写）

```json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres",
               "postgresql://user:pass@localhost:5432/mydb"]
    }
  }
}
```

### 4.3 MCP连接后的实际用法

连接MCP后，Claude Code可以直接：

```
# 查看当前数据库状态
列出所有表和它们的行数

# 检查索引
products表上有哪些索引？有没有未使用的索引？

# 分析查询性能
对以下查询做EXPLAIN ANALYZE分析：
SELECT u.name, COUNT(o.id) as order_count
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2025-01-01'
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 20;

# 对比schema差异
对比 src/db/schema.ts 和数据库实际结构，告诉我哪些地方不一致
```

这种"数据库即上下文"的模式，让Claude Code不再依赖你的描述，而是直接感知数据库的真实状态。

### 4.4 安全注意事项

```
MCP数据库连接安全铁律：
1. 永远不要在生产数据库上配置读写MCP
2. 开发环境用只读连接（read-only user）
3. .env中的DATABASE_URL不要提交到git
4. MCP的SQL执行需要用户确认（Claude Code默认行为）
5. 定期轮换数据库密码
```

---

## 第五步：查询优化实战

这是Claude Code在数据库领域ROI最高的场景——粘贴慢查询，获得优化方案。

### 5.1 慢查询诊断流程

```mermaid
flowchart TD
    A[发现慢查询] --> B[获取EXPLAIN ANALYZE]
    B --> C{粘贴给Claude Code}
    C --> D[分析扫描类型]
    D --> E{Seq Scan?}
    E -->|是| F[检查缺失索引]
    E -->|否| G{Index Scan但仍慢?}
    G -->|是| H[检查索引选择性]
    G -->|否| I{Nested Loop?}
    I -->|是| J[检查JOIN条件索引]
    I -->|否| K[检查Sort/GroupBy]
    F --> L[Claude生成优化查询+索引]
    H --> L
    J --> L
    K --> L
    L --> M[验证优化效果]
    M --> N{查询时间降低?}
    N -->|是| O[创建migration添加索引]
    N -->|否| P[调整查询逻辑]
```

### 5.2 N+1查询检测与修复

给Claude Code一段ORM代码，它能发现N+1问题：

```typescript
// 问题代码：N+1查询
// Claude Code会标记这段代码有问题
async function getUserOrders(userId: number) {
  const orders = await db.select().from(ordersTable)
    .where(eq(ordersTable.userId, userId));

  // 每个order都查一次product —— N+1！
  for (const order of orders) {
    const items = await db.select().from(orderItemsTable)
      .where(eq(orderItemsTable.orderId, order.id));
    order.items = items;
  }
  return orders;
}
```

Claude Code建议的修复：

```typescript
// 修复：一次查询获取所有数据
async function getUserOrdersOptimized(userId: number) {
  const result = await db
    .select({
      order: ordersTable,
      item: orderItemsTable,
      product: productsTable,
    })
    .from(ordersTable)
    .innerJoin(orderItemsTable, eq(ordersTable.id, orderItemsTable.orderId))
    .innerJoin(productsTable, eq(orderItemsTable.productId, productsTable.id))
    .where(eq(ordersTable.userId, userId));

  // 在内存中组装
  const orderMap = new Map<number, Order & { items: OrderItem[] }>();
  for (const row of result) {
    if (!orderMap.has(row.order.id)) {
      orderMap.set(row.order.id, { ...row.order, items: [] });
    }
    orderMap.get(row.order.id)!.items.push(row.item);
  }
  return Array.from(orderMap.values());
}
```

### 5.3 EXPLAIN ANALYZE解读

Claude Code最擅长解读PostgreSQL执行计划：

```sql
-- 粘贴慢查询的EXPLAIN ANALYZE输出给Claude Code：
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT p.name, sum(oi.quantity * oi.unit_price) as revenue
FROM products p
JOIN order_items oi ON p.id = oi.product_id
JOIN orders o ON oi.order_id = o.id
WHERE o.created_at BETWEEN '2025-01-01' AND '2025-12-31'
  AND o.status IN ('paid', 'delivered')
GROUP BY p.id
ORDER BY revenue DESC
LIMIT 10;
```

Claude Code可能给出这样的分析：

```
诊断结果：
1. orders表在created_at上做了Seq Scan → 需要复合索引 (status, created_at)
2. order_items的JOIN用Nested Loop → product_id上缺少索引
3. 排序在内存中完成但用了external merge → work_mem可能不够

建议索引：
CREATE INDEX CONCURRENTLY idx_orders_status_created
  ON orders (status, created_at)
  WHERE status IN ('paid', 'delivered');  -- 部分索引，更小更快

CREATE INDEX CONCURRENTLY idx_order_items_product
  ON order_items (product_id)
  INCLUDE (quantity, unit_price);  -- 覆盖索引，避免回表
```

### 5.4 索引设计五原则

让Claude Code遵循这五条规则设计索引：

```markdown
## 索引设计规则（加入CLAUDE.md）

1. **等值条件列在前，范围条件列在后**
   WHERE status = 'active' AND created_at > '2025-01-01'
   → INDEX (status, created_at) ✓
   → INDEX (created_at, status) ✗

2. **高选择性列在前**
   WHERE country = 'CN' AND status = 'active'
   country选择性低（大量CN），status选择性高
   → INDEX (status, country) ✓

3. **能用部分索引就不建全表索引**
   WHERE status = 'active'
   → WHERE status = 'active' 的部分索引更小

4. **覆盖索引避免回表**
   SELECT id, name FROM users WHERE email = ?
   → INDEX (email) INCLUDE (name)

5. **监控索引使用率**
   每月检查pg_stat_user_indexes的idx_scan
   idx_scan = 0 的索引考虑删除
```

---

## 第六步：种子数据与测试数据工厂

真实的测试数据比空表调试效率高10倍。Claude Code能生成符合约束的测试数据。

### 6.1 数据生成策略

```typescript
// src/db/seed.ts
import { db } from "./index";
import { users, products, orders, orderItems } from "./schema";
import { faker } from "@faker-js/faker/locale/zh_CN";

async function seed() {
  console.log("🌱 开始播种...");

  // 1. 用户
  const userList = Array.from({ length: 100 }, () => ({
    email: faker.internet.email(),
    passwordHash: "$2b$10$dummy.hash.for.seed.data",
    name: faker.person.fullName(),
    role: faker.helpers.arrayElement(["customer", "customer", "customer", "vendor"]),
  }));
  const insertedUsers = await db.insert(users).values(userList).returning();

  // 2. 商品
  const productList = Array.from({ length: 500 }, () => ({
    name: faker.commerce.productName(),
    description: faker.commerce.productDescription(),
    price: faker.commerce.price({ min: 1, max: 9999, dec: 2 }),
    stock: faker.number.int({ min: 0, max: 1000 }),
    status: faker.helpers.arrayElement(["draft", "active", "active", "active", "archived"]),
  }));
  const insertedProducts = await db.insert(products).values(productList).returning();

  // 3. 订单 + 订单项（每个用户1-5个订单，每个订单1-5个商品）
  for (const user of insertedUsers) {
    const orderCount = faker.number.int({ min: 1, max: 5 });
    for (let i = 0; i < orderCount; i++) {
      const itemCount = faker.number.int({ min: 1, max: 5 });
      const selectedProducts = faker.helpers.arrayElements(insertedProducts, itemCount);

      const orderResult = await db.insert(orders).values({
        userId: user.id,
        totalAmount: "0", // 先插入，后面更新
        status: faker.helpers.arrayElement(["pending", "paid", "shipped", "delivered"]),
        shippingAddress: {
          line1: faker.location.streetAddress(),
          city: faker.location.city(),
          zip: faker.location.zipCode(),
          country: "CN",
        },
      }).returning();

      let total = 0;
      for (const product of selectedProducts) {
        const qty = faker.number.int({ min: 1, max: 3 });
        await db.insert(orderItems).values({
          orderId: orderResult[0].id,
          productId: product.id,
          quantity: qty,
          unitPrice: product.price,
        });
        total += parseFloat(product.price) * qty;
      }

      // 更新订单总额
      await db.update(orders)
        .set({ totalAmount: total.toFixed(2) })
        .where(eq(orders.id, orderResult[0].id));
    }
  }

  console.log("✅ 播种完成！");
}

seed().catch(console.error);
```

### 6.2 让Claude Code生成种子数据的技巧

```
为电商数据库生成种子数据：

要求：
1. 50个用户，email不重复，密码统一用测试哈希
2. 200个商品，价格在10-5000之间
3. 每个用户2-8个订单，订单状态分布：
   - pending 10%, paid 30%, shipped 25%, delivered 30%, cancelled 5%
4. 订单项的unit_price必须等于下单时商品的价格
5. 订单的total_amount必须等于所有订单项的(quantity * unit_price)之和
6. 所有外键关系必须正确

输出为 drizzle-orm 的 insert 语句
```

关键点：第5、6条约束是Claude Code容易犯错的地方——明确指出外键关系和金额一致性，能大幅提升生成质量。

### 6.3 执行种子

```bash
# 运行种子脚本
npx tsx src/db/seed.ts

# 验证数据
npx tsx -e "
const { db } = require('./src/db/index');
const { users, products, orders } = require('./src/db/schema');
import { sql } from 'drizzle-orm';

async function check() {
  const counts = await Promise.all([
    db.select({ count: sql\`count(*)\` }).from(users),
    db.select({ count: sql\`count(*)\` }).from(products),
    db.select({ count: sql\`count(*)\` }).from(orders),
  ]);
  console.log('Users:', counts[0][0].count);
  console.log('Products:', counts[1][0].count);
  console.log('Orders:', counts[2][0].count);
}
check();
"
```

---

## 第七步：完整实战——商品搜索与排序优化

将前六步整合为一个真实场景：为商品列表实现高性能搜索+排序+分页。

### 7.1 需求描述

```
实现商品搜索接口：
- 支持按名称模糊搜索（LIKE '%keyword%'）
- 支持按分类、价格区间、状态筛选
- 支持按价格/销量/创建时间排序
- 分页：cursor-based，每页20条
- 目标：< 50ms响应时间

使用Drizzle ORM，不要原始SQL。
配合必要的索引和EXPLAIN验证。
```

### 7.2 Claude Code生成的实现

```typescript
// src/repositories/product.search.ts
import { db } from "../db";
import { products, orderItems, sql, and, or, ilike, gte, lte, desc, asc, eq } from "drizzle-orm";
import type { SQL } from "drizzle-orm";

interface ProductSearchParams {
  keyword?: string;
  category?: string;
  minPrice?: string;
  maxPrice?: string;
  status?: string;
  sortBy?: "price" | "sales" | "created_at";
  sortOrder?: "asc" | "desc";
  cursor?: string; // base64 encoded JSON: { field: value }
  limit?: number;
}

interface ProductSearchResult {
  items: Product[];
  nextCursor: string | null;
  total: number;
}

export async function searchProducts(
  params: ProductSearchParams
): Promise<ProductSearchResult> {
  const {
    keyword, category, minPrice, maxPrice, status,
    sortBy = "created_at", sortOrder = "desc",
    cursor, limit = 20,
  } = params;

  // 构建WHERE条件
  const conditions: SQL[] = [];

  // 软删除过滤
  conditions.push(sql`${products.deletedAt} IS NULL`);

  if (keyword) {
    conditions.push(ilike(products.name, `%${keyword}%`));
  }
  if (category) {
    conditions.push(eq(products.category, category as any));
  }
  if (minPrice) {
    conditions.push(gte(products.price, minPrice));
  }
  if (maxPrice) {
    conditions.push(lte(products.price, maxPrice));
  }
  if (status) {
    conditions.push(eq(products.status, status as any));
  }

  // Cursor分页
  if (cursor) {
    const cursorData = JSON.parse(Buffer.from(cursor, "base64").toString());
    if (sortOrder === "desc") {
      conditions.push(sql`${products[sortBy]} < ${cursorData.value}`);
    } else {
      conditions.push(sql`${products[sortBy]} > ${cursorData.value}`);
    }
  }

  const whereClause = and(...conditions);

  // 按销量排序需要子查询
  if (sortBy === "sales") {
    const salesSubquery = db
      .select({
        productId: orderItems.productId,
        totalSold: sql<number>`coalesce(sum(${orderItems.quantity}), 0)`,
      })
      .from(orderItems)
      .groupBy(orderItems.productId)
      .as("sales");

    const result = await db
      .select({
        id: products.id,
        name: products.name,
        price: products.price,
        stock: products.stock,
        status: products.status,
        createdAt: products.createdAt,
        totalSold: salesSubquery.totalSold,
      })
      .from(products)
      .leftJoin(salesSubquery, eq(products.id, salesSubquery.productId))
      .where(whereClause)
      .orderBy(
        sortOrder === "desc"
          ? desc(salesSubquery.totalSold)
          : asc(salesSubquery.totalSold)
      )
      .limit(limit + 1);

    const hasNext = result.length > limit;
    const items = hasNext ? result.slice(0, -1) : result;
    const lastItem = items[items.length - 1];

    return {
      items,
      nextCursor: hasNext
        ? Buffer.from(JSON.stringify({
            field: "sales",
            value: lastItem.totalSold,
          })).toString("base64")
        : null,
      total: -1, // cursor分页不返回总数，单独count
    };
  }

  // 其他排序：直接查products表
  const result = await db
    .select()
    .from(products)
    .where(whereClause)
    .orderBy(
      sortOrder === "desc"
        ? desc(products[sortBy])
        : asc(products[sortBy])
    )
    .limit(limit + 1);

  const hasNext = result.length > limit;
  const items = hasNext ? result.slice(0, -1) : result;
  const lastItem = items[items.length - 1];

  return {
    items,
    nextCursor: hasNext
      ? Buffer.from(JSON.stringify({
          field: sortBy,
          value: lastItem[sortBy],
        })).toString("base64")
      : null,
    total: -1,
  };
}
```

### 7.3 配套索引Migration

Claude Code根据查询模式生成的索引：

```sql
-- src/db/migrations/0002_product_search_indexes.sql

-- 名称模糊搜索：pg_trgm三元组索引
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_name_trgm
  ON products USING gin (name gin_trgm_ops);

-- 分类+状态组合筛选
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_category_status
  ON products (category, status)
  WHERE deleted_at IS NULL;

-- 价格范围查询
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_price
  ON products (price)
  WHERE deleted_at IS NULL;

-- 创建时间排序（分页cursor）
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_created_desc
  ON products (created_at DESC)
  WHERE deleted_at IS NULL;

-- 订单项按商品聚合（销量排序）
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_order_items_product_qty
  ON order_items (product_id)
  INCLUDE (quantity);
```

### 7.4 性能验证

```bash
# 执行migration
npx drizzle-kit migrate

# 用Claude Code验证查询计划
```

将以下内容发给Claude Code（配合MCP直连数据库）：

```
在已连接的PostgreSQL上执行以下验证：

1. EXPLAIN ANALYZE 搜索 "手机" 按销量排序前20条
2. EXPLAIN ANALYZE 价格100-500区间按创建时间排序
3. 检查所有新建索引是否被使用
4. 如果有Seq Scan，分析原因并给出修复建议
```

### 7.5 完整工作流总览

```mermaid
flowchart LR
    A[CLAUDE.md规范] --> B[Schema定义]
    B --> C[Drizzle生成Migration]
    C --> D[MCP直连验证]
    D --> E[种子数据填充]
    E --> F[查询开发]
    F --> G[EXPLAIN优化]
    G --> H[索引Migration]
    H --> D

    style A fill:#e1f5fe
    style D fill:#fff3e0
    style G fill:#e8f5e9
```

---

## 高级技巧：五个让效率翻倍的Prompt模式

### 模式一：Schema Diff审查

```
对比 git HEAD~1 和 HEAD 的 schema.ts 变更：
1. 哪些表被修改了？
2. 是否有破坏性变更（删列、改类型）？
3. 需要几个migration？给出SQL。
```

### 模式二：逆向工程

```
我用MCP连接了一个已有的PostgreSQL数据库。
请：
1. 列出所有表的结构
2. 生成对应的Drizzle schema.ts
3. 生成relations()关系定义
4. 导出所有TypeScript类型
```

这对接手遗留项目极其有用。

### 模式三：性能巡检

```
对当前数据库做全面性能巡检：
1. 列出所有缺失索引的查询模式（从pg_stat_statements）
2. 找出未使用的索引（idx_scan = 0）
3. 检查膨胀率超标的表（dead_tuples ratio > 10%）
4. 建议需要VACUUM的表
5. 检查外键是否都有对应索引
```

### 模式四：Migration安全检查

```
我准备在生产库执行以下migration：

[paste SQL here]

请检查：
1. 是否会锁表？锁类型和预估时间
2. 在1000万行表上的执行时间估算
3. 是否有数据丢失风险
4. 回滚方案是否可行
5. 是否需要SET statement_timeout
```

### 模式五：数据一致性校验

```
编写SQL校验以下数据一致性规则：
1. 订单总额 = SUM(订单项.数量 * 单价)
2. 用户不能有重复email
3. 订单项的商品必须存在且未删除
4. 库存不能为负数
5. 外键无孤立记录

输出为可重复执行的检查脚本，每条规则输出通过/失败+违规数量。
```

---

## 常见问题排查表

| 症状 | 可能原因 | Claude Code诊断命令 |
|------|----------|---------------------|
| Schema生成了camelCase | CLAUDE.md未加载 | 确认CLAUDE.md在项目根目录 |
| Migration锁表 | 缺少CONCURRENTLY | 审查migration SQL |
| 查询用了Seq Scan | 索引缺失或选择性低 | EXPLAIN ANALYZE分析 |
| N+1查询 | 循环中做查询 | 审查ORM代码模式 |
| 种子数据外键冲突 | 插入顺序错误 | 检查insert执行顺序 |
| MCP连不上 | URL格式或权限 | 检查连接字符串和pg_hba.conf |
| 查询返回重复行 | JOIN关系不对 | 检查关联条件和GROUP BY |
| Cursor分页跳过记录 | 排序字段有重复值 | 改用(id, sort_field)复合cursor |

---

## 延伸阅读

- [Anthropic官方文档 - Claude Code Database Work](https://docs.anthropic.com/en/docs/claude-code) — 官方数据库工作指南
- [Drizzle ORM官方文档](https://orm.drizzle.team/docs/overview) — Drizzle完整API参考
- [PostgreSQL官方文档 - Index Types](https://www.postgresql.org/docs/current/indexes-types.html) — PostgreSQL索引类型详解
- [DBHub MCP Server](https://github.com/anthropics/dbhub) — 数据库MCP连接器
- [Developer Toolkit - Database Migrations](https://developertoolkit.ai/en/claude-code/lessons/migrations/) — 零停机Migration工作流

## 参考来源

1. [How to Use Claude Code for Database Design and Migration](https://atticusli.com/blog/posts/how-to-use-claude-code-database-design-migration/) — Atticus Li的数据库设计Migration全流程指南
2. [Database and Code Migrations | Developer Toolkit](https://developertoolkit.ai/en/claude-code/lessons/migrations/) — 零停机Migration设计与执行工作流
3. [Query Optimization with Claude Code: Fix N+1, Slow Queries](https://dev.to/myougatheaxo/query-optimization-with-claude-code-fix-n1-slow-queries-and-missing-indexes-4fk) — N+1检测与索引优化实战
4. [Using Claude Code for Database Work | Airbyte](https://airbyte.com/blog/claude-code-for-databases) — 数据库查询、Schema、Migration、优化全场景
5. [How to Use Claude Code with Your Database: DBHub](https://www.deployhq.com/blog/how-to-generate-sql-queries-with-ai-step-by-step-guide-using-claude-code-and-dbhub) — DBHub MCP配置与SQL查询生成

**训练营适用性**：本教程适合"全栈开发训练营"（Day 3-4数据库模块），建议教学时长4小时（2小时讲解+2小时实操）。学员需具备基本SQL和TypeScript基础。可作为"AI辅助后端开发"专题课的核心实验。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
