数据库评审清单

数据库评审关注数据模型、存储选型、查询性能、数据一致性、迁移和运维。数据库改动成本高、风险大,评审阶段把好关能避免大量后期返工。

要点

  • 数据库评审关注数据模型、存储选型、查询性能、一致性、迁移
  • 重点检查:表结构设计、索引覆盖、查询性能、并发控制、备份恢复
  • 数据库改动成本高,评审阶段发现问题比上线后优化划算
  • Drizzle ORM 提供类型安全的 schema 定义和迁移工具

1. 适用时机

  • 新增数据表或集合
  • 修改现有表结构(加字段、改类型、改索引)
  • 引入新的数据存储(从 MySQL 迁到 PostgreSQL、引入 MongoDB、Elasticsearch 等)
  • 数据迁移(ETL、数据清洗、历史数据归档)
  • 查询性能优化(加索引、重构查询)

2. 评审前准备

  • 数据模型图:表/集合关系、字段、约束
  • 数据量估算:当前数据量、增长速率、未来 6-12 个月预期
  • 查询模式:主要查询有哪些?QPS 多少?是否有复杂查询?
  • 一致性要求:哪些场景需要强一致?哪些可以最终一致?
  • 迁移方案:如何从现有结构迁移到新结构?
  • 性能基线:当前查询延迟、慢查询列表

3. 核心检查项

3.1 数据模型

  • 表名、字段名命名清晰,符合团队约定(snake_case 等)
  • 字段类型正确(字符串长度、数字精度、时间精度)
  • 主键设计合理(自增、UUID、业务主键?)
  • 外键和约束关系清楚
  • 索引设计合理(覆盖主要查询,避免过多索引)
  • 没有冗余字段(或冗余有明确理由 + 维护策略)

Drizzle ORM 示例:Schema 定义

import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'
 
// 用户表
export const users = sqliteTable('users', {
  id: text('id').primaryKey(),  // UUID
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  age: integer('age'),
  role: text('role', { enum: ['user', 'admin'] }).default('user').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
})
 
// 订单表
export const orders = sqliteTable('orders', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id),
  status: text('status', { enum: ['pending', 'paid', 'shipped', 'completed'] }).notNull(),
  totalAmount: real('total_amount').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull()
})
 
// 订单商品表
export const orderItems = sqliteTable('order_items', {
  id: text('id').primaryKey(),
  orderId: text('order_id').notNull().references(() => orders.id),
  productId: text('product_id').notNull(),
  quantity: integer('quantity').notNull(),
  price: real('price').notNull()
})
 
// 关系定义
export const usersRelations = relations(users, ({ many }) => ({
  orders: many(orders)
}))
 
export const ordersRelations = relations(orders, ({ one, many }) => ({
  user: one(users, {
    fields: [orders.userId],
    references: [users.id]
  }),
  items: many(orderItems)
}))

索引设计

import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core'
 
export const orders = sqliteTable('orders', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull(),
  status: text('status').notNull(),
  createdAt: integer('created_at').notNull()
}, (table) => ({
  // 单字段索引
  userIdIdx: index('user_id_idx').on(table.userId),
 
  // 复合索引:查询用户的订单,按时间倒序
  userCreatedIdx: index('user_created_idx').on(table.userId, table.createdAt),
 
  // 状态索引:查询待处理订单
  statusIdx: index('status_idx').on(table.status)
}))

3.2 存储选型

  • 关系型 vs 文档型 vs KV vs 时序 vs 向量 —— 选型理由充分
  • 数据特性与存储引擎匹配(事务、查询模式、扩展性)
  • 团队有能力运维这个存储(不是「因为流行」)
  • 成本和容量规划清晰

存储选型决策表

场景推荐存储理由
订单、用户、支付关系型(D1/PostgreSQL)事务、一致性、复杂查询
配置、缓存KV(Cloudflare KV/Redis)快速读写、简单结构
日志、事件时序数据库(InfluxDB)时间序列查询、聚合
文档、内容文档型(MongoDB)灵活 schema、嵌套结构
向量搜索向量数据库(Pinecone)相似度搜索

3.3 查询性能

  • 主要查询都有 EXPLAIN 分析
  • 没有全表扫描(除非表很小或确实需要)
  • 索引覆盖主要查询路径
  • 没有 N+1 查询(批量加载或 JOIN)
  • 大数据量查询有分页(不能一次返回 10 万条)
  • 复杂查询考虑物化视图或缓存

Drizzle ORM 示例:查询优化

import { drizzle } from 'drizzle-orm/d1'
import { eq, and, desc, inArray } from 'drizzle-orm'
 
const db = drizzle(c.env.DB)
 
// 好的查询:使用索引
const userOrders = await db
  .select()
  .from(orders)
  .where(eq(orders.userId, userId))
  .orderBy(desc(orders.createdAt))
  .limit(20)
  .offset((page - 1) * 20)
 
// 坏的查询:全表扫描(没有索引)
const allOrders = await db.select().from(orders)  // 不要这样!
 
// 避免 N+1:使用 IN 查询
const userIds = orders.map(o => o.userId)
const users = await db
  .select()
  .from(users)
  .where(inArray(users.id, userIds))
 
// 而不是循环查询
for (const order of orders) {
  const user = await db.select().from(users).where(eq(users.id, order.userId))  // N+1!
}
 
// 批量插入
const items = [
  { orderId: '1', productId: 'p1', quantity: 2, price: 100 },
  { orderId: '1', productId: 'p2', quantity: 1, price: 200 }
]
 
await db.insert(orderItems).values(items)  // 批量插入,而不是循环单条插入

EXPLAIN 分析

-- D1 支持 EXPLAIN
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE user_id = 'xxx' ORDER BY created_at DESC LIMIT 20;
 
-- 输出示例:
-- 0|0|0|SEARCH orders USING INDEX user_created_idx (user_id=?)
-- 说明:使用了索引,没有全表扫描

3.4 数据一致性

  • 事务边界清楚(哪些操作必须在同一事务)
  • 隔离级别选择合理(读已提交 vs 可序列化)
  • 并发写入处理(乐观锁、悲观锁、CAS)
  • 跨服务数据一致性方案(Saga、最终一致、事件溯源)
  • 数据校验在应用层和数据库层都有

Drizzle ORM 示例:事务与并发控制

import { db } from './db'
import { orders, inventory } from './schema'
import { eq, and, sql } from 'drizzle-orm'
 
// 事务:扣减库存 + 创建订单
async function createOrder(userId: string, items: OrderItem[]) {
  return await db.transaction(async (tx) => {
    // 检查库存
    for (const item of items) {
      const stock = await tx
        .select()
        .from(inventory)
        .where(eq(inventory.productId, item.productId))
        .get()
 
      if (!stock || stock.quantity < item.quantity) {
        throw new Error('库存不足')
      }
 
      // 扣减库存
      await tx
        .update(inventory)
        .set({ quantity: sql`${inventory.quantity} - ${item.quantity}` })
        .where(eq(inventory.productId, item.productId))
    }
 
    // 创建订单
    const orderId = crypto.randomUUID()
    await tx.insert(orders).values({
      id: orderId,
      userId,
      status: 'pending',
      totalAmount: items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    })
 
    return orderId
  })
}
 
// 乐观锁:防止并发更新冲突
async function updateOrderStatus(orderId: string, newStatus: string, expectedVersion: number) {
  const result = await db
    .update(orders)
    .set({
      status: newStatus,
      version: expectedVersion + 1,
      updatedAt: new Date()
    })
    .where(
      and(
        eq(orders.id, orderId),
        eq(orders.version, expectedVersion)
      )
    )
    .returning()
 
  if (result.length === 0) {
    throw new Error('订单已被修改,请重试')
  }
 
  return result[0]
}

3.5 数据迁移

  • 迁移脚本可重复执行(幂等)
  • 大表迁移分批执行(避免锁表)
  • 有回滚方案(迁移失败能退回)
  • 迁移期间服务可用性考虑(在线迁移 vs 停机迁移)
  • 数据校验(迁移后对比源和目标的记录数、关键指标)

Drizzle ORM 示例:迁移脚本

// drizzle/migrations/0001_add_status_to_orders.ts
import { sql } from 'drizzle-orm'
import { migrate } from 'drizzle-orm/d1/migrator'
 
export async function up(db: D1Database) {
  // 添加字段,带默认值
  await db.exec(`
    ALTER TABLE orders
    ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'
  `)
 
  // 添加索引
  await db.exec(`
    CREATE INDEX idx_orders_status ON orders(status)
  `)
}
 
export async function down(db: D1Database) {
  // 回滚:删除索引和字段
  await db.exec(`DROP INDEX IF EXISTS idx_orders_status`)
  await db.exec(`ALTER TABLE orders DROP COLUMN status`)
}
 
// 大表迁移:分批执行
async function migrateLargeTable(db: D1Database) {
  const batchSize = 1000
  let offset = 0
 
  while (true) {
    const batch = await db
      .select()
      .from(oldTable)
      .limit(batchSize)
      .offset(offset)
      .all()
 
    if (batch.results.length === 0) break
 
    await db.insert(newTable).values(batch.results)
 
    offset += batchSize
 
    // 避免长时间占用
    await new Promise(resolve => setTimeout(resolve, 100))
  }
}

3.6 备份与恢复

  • 备份策略明确(全量 + 增量、频率、保留时长)
  • 恢复演练定期执行(不是「备份了但从没试过恢复」)
  • RPO(恢复点目标)和 RTO(恢复时间目标)明确
  • 跨地域备份(灾难恢复)

3.7 安全与权限

  • 敏感字段加密存储(密码、身份证号、银行卡号)
  • 数据库账号权限最小化(应用账号、只读账号、管理账号分开)
  • 审计日志记录关键操作(谁在什么时候删了什么)
  • 数据脱敏(开发/测试环境不用真实数据)
  • 合规要求满足(GDPR、等保、行业规范)

敏感数据加密示例

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
 
// 加密敏感字段
function encrypt(value: string, key: string): string {
  const iv = randomBytes(16)
  const cipher = createCipheriv('aes-256-cbc', Buffer.from(key), iv)
  let encrypted = cipher.update(value, 'utf8', 'hex')
  encrypted += cipher.final('hex')
  return `${iv.toString('hex')}:${encrypted}`
}
 
// 解密
function decrypt(encrypted: string, key: string): string {
  const [ivHex, data] = encrypted.split(':')
  const iv = Buffer.from(ivHex, 'hex')
  const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key), iv)
  let decrypted = decipher.update(data, 'hex', 'utf8')
  decrypted += decipher.final('utf8')
  return decrypted
}
 
// 使用
const encryptedEmail = encrypt('[email protected]', process.env.ENCRYPTION_KEY!)
await db.insert(users).values({
  id: crypto.randomUUID(),
  email: encryptedEmail  // 存储加密后的值
})

3.8 连接与资源

  • 连接池配置合理(最大连接数、超时、空闲回收)
  • 慢查询监控和告警
  • 连接泄漏防护(用 try-finally 或 ORM 自动管理)
  • 读写分离(读多写少场景)
  • 分库分表策略(数据量超过单机容量时)

3.9 归档与清理

  • 历史数据归档策略(多久前的数据移到冷存储)
  • 软删除 vs 硬删除(是否需要保留历史)
  • 数据清理任务(定时删除过期数据)
  • 存储空间监控和预警

软删除示例

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { isNull } from 'drizzle-orm'
 
export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  deletedAt: integer('deleted_at', { mode: 'timestamp' })  // 软删除标记
})
 
// 查询未删除的用户
const activeUsers = await db
  .select()
  .from(users)
  .where(isNull(users.deletedAt))
 
// 软删除
await db
  .update(users)
  .set({ deletedAt: new Date() })
  .where(eq(users.id, userId))
 
// 定时清理:删除 30 天前的软删除数据
async function cleanupDeletedUsers() {
  const thirtyDaysAgo = new Date()
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
 
  await db
    .delete(users)
    .where(
      and(
        isNull(users.deletedAt),
        lt(users.deletedAt, thirtyDaysAgo)
      )
    )
}

4. 评审会上容易漏的点

  1. 数据量估算:问「一年后这张表有多少行?查询还能这么快吗?」
  2. 索引代价:问「加了这个索引,写入性能会降多少?」
  3. 并发场景:问「两个人同时修改同一条记录会怎样?」
  4. 迁移回滚:问「迁移失败了,怎么回到原来的状态?」
  5. 敏感数据:问「哪些字段是敏感的?怎么加密?谁能看?」

5. 通过标准

  • 数据模型清晰、规范、无明显冗余
  • 查询性能满足预期(有 EXPLAIN 证据)
  • 一致性方案明确,并发场景已考虑
  • 迁移方案完整,有回滚路径
  • 备份与恢复策略明确
  • 安全措施到位
  • 监控与告警覆盖

6. 反模式

  • 大宽表:一张表 50+ 字段,什么都有
  • 万能字段:用 JSON 存所有可变数据,失去数据库的约束能力
  • 无索引查询:大数据量表没有合适索引,全靠全表扫描
  • 过度索引:一张表 20 个索引,写入性能崩塌
  • 跨表事务:一个事务横跨 5 张表,锁竞争严重
  • 无归档策略:表越来越大,查询越来越慢

7. 维护建议

数据库评审清单需要随技术栈和业务规模更新:

  • 引入新的存储引擎(如向量数据库、时序数据库)时,补充对应检查项
  • 出现数据相关事故后,把根因对应的检查项加进来
  • 数据规模增长后,调整查询性能和索引相关的要求
  • 每半年复审一次,删除过时的检查项