name: 数据库设计 description: 设计可扩展、规范化的数据库模式,包括正确的关系、索引、约束和迁移策略,适用于关系型和NoSQL数据库。使用场景:设计数据库模式、规划表关系和外来键、创建索引以优化查询、定义约束和验证、设计可扩展的数据模型、规划数据库迁移、选择SQL和NoSQL之间、实施分片策略、优化查询性能或建立数据完整性规则。
数据库设计 - 模式、索引和查询优化
何时使用此技能
- 设计新的数据库模式和模型
- 规划表关系和外来键约束
- 创建索引以优化查询性能
- 定义数据库约束和验证规则
- 设计可扩展性和未来增长
- 规划数据库迁移策略
- 选择SQL和NoSQL数据库之间
- 实施数据库分片或分区
- 规范化数据以减少冗余
- 优化慢速数据库查询
- 设计审计跟踪和软删除
- 规划备份和灾难恢复策略
何时使用此技能
- 设计数据库模式、选择SQL/NoSQL、优化查询、规划迁移或解决数据建模问题。
- 当处理相关任务或功能时
- 在需要此专业知识的开发过程中
使用场景:设计数据库模式、选择SQL/NoSQL、优化查询、规划迁移或解决数据建模问题。
核心原则
- 先规范化,需要时再反规范化 - 从正确规范化开始
- 战略性地索引 - 平衡查询速度与写入开销
- 为扩展而设计 - 从一开始就考虑增长模式
- 选择正确的工具 - SQL与NoSQL取决于使用案例
- 迁移是永远的 - 仔细规划模式变更
模式设计基础
1. 规范化(减少冗余)
-- ❌ 反规范化 - 数据重复
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_name VARCHAR(100),
customer_email VARCHAR(100),
customer_address TEXT, -- 每个订单都重复!
product_name VARCHAR(200),
product_price DECIMAL(10,2), -- 重复的产品数据
quantity INT
);
-- ✅ 规范化 - 分离关注点
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
address TEXT
);
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT DEFAULT 0
);
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id),
created_at TIMESTAMP DEFAULT NOW(),
status VARCHAR(20) DEFAULT 'pending'
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0),
price_at_purchase DECIMAL(10,2) NOT NULL -- 历史快照
);
2. 主键和外键
-- ✅ 自增整数(传统)
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- PostgreSQL
-- id INT AUTO_INCREMENT PRIMARY KEY, -- MySQL
email VARCHAR(255) UNIQUE NOT NULL
);
-- ✅ UUID(分布式系统,无冲突)
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL
);
-- ✅ 复合主键(连接表)
CREATE TABLE user_roles (
user_id INT NOT NULL REFERENCES users(id),
role_id INT NOT NULL REFERENCES roles(id),
granted_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, role_id)
);
-- ✅ 外键与级联选项
CREATE TABLE comments (
id INT PRIMARY KEY,
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, -- 删除帖子时删除评论
user_id INT NOT NULL REFERENCES users(id) ON DELETE SET NULL, -- 保留评论,将用户设为空
content TEXT NOT NULL
);
3. 索引优化性能
-- ✅ 单列索引(频繁查询)
CREATE INDEX idx_users_email ON users(email);
-- ✅ 复合索引(多列查询)
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, created_at DESC);
-- ✅ 部分索引(条件性)
CREATE INDEX idx_active_users
ON users(email) WHERE status = 'active';
-- ✅ 全文搜索索引
CREATE INDEX idx_posts_search
ON posts USING GIN(to_tsvector('english', title || ' ' || content));
-- ✅ 唯一索引(强制执行约束 + 加速查找)
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- ❌ 过度索引(减慢写入)
-- 不要盲目索引每一列
-- 索引占用磁盘空间并减慢 INSERT/UPDATE/DELETE
索引策略:
索引时机:
✓ 频繁在 WHERE 子句中使用
✓ 在 JOIN 条件中使用
✓ 在 ORDER BY / GROUP BY 中使用
✓ 高基数(许多唯一值)
不索引时机:
✗ 小表(< 1000 行)
✗ 低基数(如布尔、状态只有2-3个值)
✗ 频繁更新的列(除非读取 >> 写入)
✗ 已由复合索引覆盖
4. 关系
-- ✅ 一对多:用户有多个帖子
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE posts (
id INT PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
title VARCHAR(200)
);
-- ✅ 多对多:用户 ↔ 角色(连接表)
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE roles (id INT PRIMARY KEY, name VARCHAR(50));
CREATE TABLE user_roles (
user_id INT REFERENCES users(id),
role_id INT REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
-- ✅ 一对一:用户有一个个人资料
CREATE TABLE users (id INT PRIMARY KEY, email VARCHAR(255));
CREATE TABLE profiles (
id INT PRIMARY KEY,
user_id INT UNIQUE NOT NULL REFERENCES users(id), -- UNIQUE 强制执行1:1
bio TEXT,
avatar_url VARCHAR(500)
);
-- ✅ 自引用:树形结构
CREATE TABLE categories (
id INT PRIMARY KEY,
name VARCHAR(100),
parent_id INT REFERENCES categories(id) -- 指向自身
);
5. 数据类型
-- ✅ 选择合适类型
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(200) NOT NULL, -- 变长字符串
description TEXT, -- 长文本
price DECIMAL(10,2) NOT NULL, -- 精确货币精度
stock INT DEFAULT 0, -- 整数
weight FLOAT, -- 近似数
is_active BOOLEAN DEFAULT true, -- 真/假
created_at TIMESTAMP DEFAULT NOW(), -- 日期 + 时间
metadata JSONB, -- JSON 数据(PostgreSQL)
tags TEXT[] -- 数组(PostgreSQL)
);
-- ❌ 错误类型
-- price FLOAT - ❌ 浮点误差对货币不利
-- created_at VARCHAR(50) - ❌ 使用适当日期时间类型
-- status INT - ❌ 使用 ENUM 或 VARCHAR 提高可读性
SQL 与 NoSQL 选择
何时使用 SQL(PostgreSQL, MySQL)
✓ 复杂关系和连接
✓ 需要 ACID 事务
✓ 结构化、可预测数据
✓ 复杂查询和聚合
✓ 需要强一致性
✓ 示例:金融系统、电子商务、CRM
何时使用 NoSQL
文档存储(MongoDB, Firestore):
✓ 灵活、演进模式
✓ 嵌套/分层数据
✓ 快速原型设计
✓ 文档中心访问模式
✓ 示例:CMS、目录、用户资料
键值存储(Redis, DynamoDB):
✓ 简单键查找
✓ 缓存层
✓ 会话存储
✓ 速率限制计数器
✓ 示例:缓存、会话、实时排行榜
时间序列(InfluxDB, TimescaleDB):
✓ 时间戳数据
✓ 高写入吞吐量
✓ 时间聚合
✓ 示例:指标、日志、IoT 传感器
查询优化
1. 识别慢查询
-- PostgreSQL - 解释查询计划
EXPLAIN ANALYZE
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id, u.name;
/*
输出显示:
- Seq Scan(差) vs Index Scan(好)
- 执行时间
- 扫描行数 vs 返回行数
*/
2. 避免 N+1 查询
// ❌ N+1 问题 - 1 查询 + N 查询每个用户
const users = await db.query('SELECT * FROM users');
for (const user of users) {
const posts = await db.query(
'SELECT * FROM posts WHERE user_id = ?',
[user.id]
); // N 查询!
}
// ✅ 单查询带 JOIN
const results = await db.query(`
SELECT
u.id, u.name, u.email,
p.id as post_id, p.title as post_title
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
`);
// ✅ 使用 Prisma ORM 急切加载
const users = await prisma.user.findMany({
include: {
posts: true // 高效加载相关帖子
}
});
3. 使用高效连接
-- ✅ INNER JOIN - 只匹配行
SELECT u.name, p.title
FROM users u
INNER JOIN posts p ON p.user_id = u.id;
-- ✅ LEFT JOIN - 所有用户,即使无帖子
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id, u.name;
-- ❌ 避免 SELECT 中子查询可能时
SELECT
u.name,
(SELECT COUNT(*) FROM posts WHERE user_id = u.id) as post_count -- 慢!
FROM users u;
-- ✅ 更好:使用 JOIN
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id, u.name;
4. 分页
-- ✅ 偏移分页(简单但深页慢)
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 40; -- 第 3 页
-- ✅ 游标分页(深页快)
SELECT * FROM posts
WHERE created_at < '2024-01-01 12:00:00'
ORDER BY created_at DESC
LIMIT 20;
5. 聚合
-- ✅ 高效计数
SELECT COUNT(*) FROM users WHERE status = 'active';
-- ✅ 分组聚合
SELECT
category,
COUNT(*) as total,
AVG(price) as avg_price,
MAX(price) as max_price
FROM products
GROUP BY category
HAVING COUNT(*) > 10; -- HAVING 过滤组,WHERE 过滤行
-- ✅ 窗口函数(PostgreSQL)
SELECT
name,
salary,
AVG(salary) OVER (PARTITION BY department) as dept_avg_salary
FROM employees;
迁移策略
1. 使用 Prisma 的模式迁移
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
@@index([authorId])
}
# 生成迁移
npx prisma migrate dev --name add_post_table
# 应用到生产
npx prisma migrate deploy
2. 安全迁移模式
-- ✅ 添加列(安全 - 不锁表)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- ✅ 并发添加索引(PostgreSQL)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- ❌ 风险 - 锁整个表
ALTER TABLE users ALTER COLUMN email TYPE TEXT;
-- ✅ 更好:多步骤迁移
-- 步骤 1: 添加新列
ALTER TABLE users ADD COLUMN email_new TEXT;
-- 步骤 2: 回填数据
UPDATE users SET email_new = email;
-- 步骤 3: 交换列(单独部署)
ALTER TABLE users DROP COLUMN email;
ALTER TABLE users RENAME COLUMN email_new TO email;
3. 数据迁移
// migration-001-backfill-slugs.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function up() {
const posts = await prisma.post.findMany({
where: { slug: null }
});
for (const post of posts) {
await prisma.post.update({
where: { id: post.id },
data: { slug: generateSlug(post.title) }
});
}
}
function generateSlug(title: string) {
return title.toLowerCase().replace(/\s+/g, '-');
}
高级模式
1. 软删除
-- ✅ 保留删除记录
CREATE TABLE posts (
id INT PRIMARY KEY,
title VARCHAR(200),
deleted_at TIMESTAMP NULL -- NULL = 未删除
);
-- 查询仅活动记录
SELECT * FROM posts WHERE deleted_at IS NULL;
-- “删除”记录(软删除)
UPDATE posts SET deleted_at = NOW() WHERE id = 123;
-- 创建视图方便
CREATE VIEW active_posts AS
SELECT * FROM posts WHERE deleted_at IS NULL;
2. 审计跟踪
-- ✅ 跟踪所有变更
CREATE TABLE users_audit (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
field_changed VARCHAR(50),
old_value TEXT,
new_value TEXT,
changed_by INT REFERENCES users(id),
changed_at TIMESTAMP DEFAULT NOW()
);
-- 触发器自动填充
CREATE OR REPLACE FUNCTION audit_user_changes()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO users_audit (user_id, field_changed, old_value, new_value)
VALUES (OLD.id, 'email', OLD.email, NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_email_audit
AFTER UPDATE OF email ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_changes();
3. 多态关联
-- ✅ 多种资源类型评论
CREATE TABLE comments (
id INT PRIMARY KEY,
commentable_type VARCHAR(50) NOT NULL, -- 'Post', 'Photo', 'Video'
commentable_id INT NOT NULL,
content TEXT NOT NULL,
-- 多态查找复合索引
INDEX idx_commentable (commentable_type, commentable_id)
);
-- 查询帖子评论
SELECT * FROM comments
WHERE commentable_type = 'Post' AND commentable_id = 123;
数据库检查清单
模式设计:
□ 适当规范化(至少 3NF)
□ 所有表有主键
□ 外键带适当级联/设为空
□ 检查约束数据验证
□ 适当地方有 NOT NULL 约束
□ 唯一约束业务规则
索引:
□ 外键已索引
□ WHERE 子句列已索引
□ JOIN 列已索引
□ 多列查询复合索引
□ 过滤查询部分索引
□ 无冗余索引
性能:
□ 慢查询 EXPLAIN ANALYZE
□ 消除 N+1 查询
□ 适当使用 JOIN vs 子查询
□ 实现分页
□ 配置连接池
迁移:
□ 所有变更在迁移文件中
□ 回滚能力已测试
□ 安全列添加/移除
□ 约束前数据回填
□ 生产零停机迁移
监控:
□ 启用慢查询日志
□ 跟踪连接池指标
□ 监控磁盘使用
□ 检查复制延迟(如果适用)
□ 定期 VACUUM(PostgreSQL)
资源
记住:良好数据库设计是可扩展应用的基础。前期投资时间于正确模式设计和索引。