29e - 数据库Prompt模板与反模式
本文是《AI Agent 实战手册》第 29 章第 5 节。 上一节:29d-ORM与查询优化 | 下一节:30a-AI辅助架构设计概览
概述
数据库是应用系统的基石,而 AI 编码助手在数据库设计中既是强大的加速器,也是潜在的”地雷制造者”。2025-2026 年的实践表明,AI 生成的数据库代码中约 40-60% 存在不同程度的设计缺陷——从过度范式化导致的性能退化,到缺少约束引发的数据完整性问题,再到不安全迁移造成的生产事故。本节提供一套完整的数据库 Prompt 模板库(覆盖 Schema 设计、迁移审查、查询优化、索引推荐、数据建模、文档生成、安全审计七大场景),深入剖析 AI 在数据库设计中最常犯的六大反模式(过度范式化、缺少索引、类型错误、不安全迁移、缺少约束、命名不规范),并为每种反模式提供问题描述、AI 犯错原因、检测方法、Steering 规则和修复 Prompt 模板。最后提供完整的数据库 Steering 规则模板和设计检查清单,帮助开发者在 AI 辅助数据库设计中建立系统化的质量防线。
1. 数据库 Prompt 模板库
1.1 工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| Claude Code | Agentic 数据库设计,CLAUDE.md 规则驱动 | 按 token 计费(Max $100/月起) | 复杂 Schema 设计、迁移生成、全流程数据库工程 |
| Kiro | Spec-Driven 数据库设计,分层 Steering | 免费(预览期) | 规范化团队数据库开发流程 |
| Cursor | AI IDE,.cursorrules 数据库规则 | 免费 / Pro $20/月 | 日常数据库开发和查询编写 |
| AI2sql | 自然语言转 SQL 查询 | 免费(基础)/ Pro $9.99/月 | 快速 SQL 查询生成和优化 |
| dbdiagram.io | AI 辅助 ER 图设计 | 免费(基础)/ Pro $9/月 | 可视化 Schema 设计和文档 |
| ChartDB | AI 辅助数据库可视化与重构 | 免费(开源)/ Cloud 版按用量 | 现有数据库分析和重构建议 |
| Prisma Optimize | AI 驱动查询性能分析 | 免费(基础)/ Pro $29/月 | Prisma 项目查询优化 |
| pganalyze | PostgreSQL 性能监控与索引建议 | $149/月起 | PostgreSQL 生产环境优化 |
| PlanetScale Insights | AI 驱动索引建议 | 按用量计费(Scaler $29/月起) | MySQL/PostgreSQL 索引优化 |
| Bytebase | 数据库 Schema 变更管理与审查 | 免费(社区版)/ Team $79/月 | 团队数据库迁移审查流程 |
1.2 Schema 设计 Prompt 模板(从需求到 Schema)
模板 1:业务需求 → 初始 Schema 设计
你是一位资深数据库架构师。请根据以下业务需求设计数据库 Schema。
## 业务需求
[详细描述业务场景、核心实体、用户角色和主要功能]
## 技术栈
- 数据库:[PostgreSQL 17 / MySQL 8.4 / SQLite]
- ORM:[Prisma / Drizzle / SQLAlchemy / TypeORM / Diesel]
- 应用类型:[Web 应用 / 移动端 / API 服务]
## 设计约束
- 预估数据量:[每张核心表的预估行数]
- 读写比例:[读多写少 / 读写均衡 / 写多读少]
- 一致性要求:[强一致 / 最终一致]
- 多租户:[是 / 否]
## 输出要求
1. ER 图(Mermaid 格式)
2. 完整的 DDL 语句(含注释)
3. 每张表的设计决策说明(为什么选择这个结构)
4. 索引策略(含创建理由)
5. 约束定义(UNIQUE、CHECK、NOT NULL、DEFAULT)
6. 潜在的性能风险和优化建议
7. 未来扩展考虑(哪些地方预留了扩展空间)
## 设计原则
- 优先考虑查询性能,适度反范式化
- 所有表必须有 created_at 和 updated_at 时间戳
- 主键使用 UUID v7(时间有序)或自增 BIGINT
- 金额字段使用 DECIMAL/NUMERIC,禁止 FLOAT
- 状态字段使用 ENUM 或 CHECK 约束
- 表名和列名使用 snake_case
- 外键必须有对应索引模板 2:现有 Schema 审查与优化
你是一位数据库性能专家。请审查以下数据库 Schema 并提供优化建议。
## 当前 Schema
[粘贴完整的 DDL 或 ORM Schema 定义]
## 当前问题
[描述遇到的性能问题、数据完整性问题或维护困难]
## 数据规模
- [表名]:约 [行数] 行,日增 [行数] 行
- 高峰期 QPS:[数值]
- 最慢查询响应时间:[毫秒数]
## 审查清单
1. **范式化程度**:是否过度范式化(JOIN 过多)或不足(数据冗余)?
2. **索引策略**:是否缺少关键索引?是否有冗余索引?
3. **数据类型**:是否有不当的数据类型选择?
4. **约束完整性**:是否缺少 UNIQUE、CHECK、NOT NULL 约束?
5. **命名规范**:表名和列名是否一致?
6. **扩展性**:Schema 是否支持未来业务扩展?
7. **安全性**:是否有敏感数据未加密?
## 输出格式
- 🔴 严重问题(必须立即修复)
- 🟡 性能隐患(建议尽快修复)
- 🟢 优化建议(可选改进)
- 每个问题提供:问题描述 → 影响分析 → 修复 SQL模板 3:NoSQL Schema 设计(MongoDB / DynamoDB)
你是一位 NoSQL 数据库架构师。请根据以下需求设计 [MongoDB / DynamoDB] 的数据模型。
## 业务需求
[描述业务场景和核心实体]
## 访问模式(Access Patterns)
请根据以下查询模式设计数据模型:
1. [查询模式 1:例如"按用户 ID 查询所有订单"]
2. [查询模式 2:例如"按日期范围查询订单"]
3. [查询模式 3:例如"按商品类别统计销量"]
4. [查询模式 4:例如"查询用户最近 10 条操作记录"]
## 设计约束
- 读写比例:[读多写少 / 写多读少]
- 单文档最大大小:[16MB for MongoDB]
- 是否需要事务:[是 / 否]
- 数据一致性要求:[强一致 / 最终一致]
## 输出要求
1. 集合/表设计(含字段定义和嵌套结构)
2. 索引设计(含复合索引和 TTL 索引)
3. 嵌入 vs 引用的决策说明
4. 分片/分区键选择(如适用)
5. 数据增长预估和容量规划
6. 反模式检查(避免无界数组、过深嵌套等)1.3 迁移审查 Prompt 模板
模板 4:迁移文件安全审查
你是一位数据库迁移安全专家。请审查以下迁移文件的安全性和正确性。
## 迁移文件
[粘贴迁移文件内容]
## 当前环境
- 数据库:[PostgreSQL / MySQL]
- ORM:[Prisma / Drizzle / SQLAlchemy / TypeORM]
- 受影响表的数据量:[行数]
- 部署方式:[零停机 / 维护窗口]
## 安全审查清单
1. **数据丢失风险**:是否有 DROP TABLE、DROP COLUMN、TRUNCATE?
2. **锁表风险**:是否有 ALTER TABLE 在大表上添加列或索引?
3. **回滚计划**:是否有对应的 down 迁移?回滚是否安全?
4. **默认值**:新增 NOT NULL 列是否提供了默认值?
5. **索引创建**:大表索引是否使用 CONCURRENTLY(PostgreSQL)?
6. **数据迁移**:是否需要数据回填?回填是否分批执行?
7. **外键约束**:新增外键是否会导致长时间锁?
8. **兼容性**:迁移是否与当前运行的应用代码兼容?
## 输出格式
- ✅ 安全:可以直接执行
- ⚠️ 需要注意:需要在维护窗口执行或分步执行
- 🚫 危险:需要重写迁移策略
- 提供安全的替代迁移方案(如需要)模板 5:零停机迁移策略生成
你是一位数据库迁移专家。请为以下 Schema 变更设计零停机迁移策略。
## 变更需求
[描述需要进行的 Schema 变更,例如:
- 重命名列 user_name → display_name
- 将 VARCHAR(100) 改为 VARCHAR(255)
- 添加新的 NOT NULL 列
- 删除不再使用的列
- 拆分大表]
## 当前环境
- 数据库:[PostgreSQL 17 / MySQL 8.4]
- 表数据量:[行数]
- 应用部署方式:[滚动更新 / 蓝绿部署]
- ORM:[Prisma / Drizzle / SQLAlchemy]
## 输出要求
1. 分步迁移计划(每步一个迁移文件)
2. 每步的应用代码变更要求
3. 每步的回滚方案
4. 预估执行时间和锁影响
5. 监控指标(迁移期间需要关注什么)
6. 完整的迁移 SQL 和对应的 ORM 迁移代码1.4 查询优化 Prompt 模板
模板 6:慢查询分析与优化
你是一位 SQL 性能调优专家。请分析以下慢查询并提供优化方案。
## 慢查询 SQL
[粘贴慢查询 SQL]
## EXPLAIN ANALYZE 输出
[粘贴 EXPLAIN ANALYZE 结果]
## 表结构
[粘贴相关表的 DDL,包含索引定义]
## 数据规模
- [表名]:[行数] 行
- 查询频率:[次/分钟]
- 当前响应时间:[毫秒]
- 目标响应时间:[毫秒]
## 分析要求
1. 识别性能瓶颈(全表扫描、嵌套循环、排序溢出等)
2. 推荐索引优化(新建索引、复合索引、覆盖索引)
3. SQL 重写建议(子查询→JOIN、EXISTS→IN 等)
4. 是否需要反范式化(添加冗余字段、物化视图)
5. 缓存策略建议(哪些查询适合缓存)
6. 提供优化后的 SQL 和预期性能提升
## 输出格式
- 问题诊断(根因分析)
- 优化方案(按优先级排序)
- 优化后的 SQL
- 需要创建的索引 DDL
- 预期性能提升(估算)模板 7:N+1 查询检测与修复
你是一位 ORM 性能专家。请检测以下代码中的 N+1 查询问题并提供修复方案。
## 代码
[粘贴包含数据库查询的应用代码]
## ORM
[Prisma / Drizzle / SQLAlchemy / TypeORM / Django ORM / Diesel]
## Schema
[粘贴相关的 Schema 定义]
## 检测要求
1. 标记所有 N+1 查询位置
2. 计算每个 N+1 的查询次数(1 + N)
3. 提供修复方案:
- 使用 eager loading(include/with/joinedload)
- 使用批量查询(IN 子句)
- 使用 JOIN 查询
- 使用 DataLoader 模式(GraphQL 场景)
4. 修复后的完整代码
5. 修复前后的查询次数对比1.5 索引推荐 Prompt 模板
模板 8:索引策略设计
你是一位数据库索引专家。请为以下表和查询模式设计最优索引策略。
## 表结构
[粘贴表的 DDL]
## 常见查询模式(按频率排序)
1. [查询 1:SQL 或 ORM 代码] — 频率:[次/分钟]
2. [查询 2:SQL 或 ORM 代码] — 频率:[次/分钟]
3. [查询 3:SQL 或 ORM 代码] — 频率:[次/分钟]
4. [查询 4:SQL 或 ORM 代码] — 频率:[次/分钟]
## 写入模式
- INSERT 频率:[次/分钟]
- UPDATE 频率:[次/分钟]
- DELETE 频率:[次/分钟]
## 约束条件
- 最大索引数量:[数量](避免过多索引影响写入)
- 存储预算:[GB]
## 输出要求
1. 推荐的索引列表(含 CREATE INDEX DDL)
2. 每个索引的作用说明(服务哪些查询)
3. 索引类型选择理由(B-Tree / Hash / GIN / GiST / BRIN)
4. 复合索引的列顺序说明(为什么这个顺序)
5. 部分索引建议(WHERE 条件过滤)
6. 覆盖索引建议(INCLUDE 列)
7. 索引对写入性能的影响评估
8. 不推荐创建的索引及原因1.6 数据库文档生成 Prompt 模板
模板 9:数据字典自动生成
你是一位数据库文档专家。请根据以下 Schema 生成完整的数据字典文档。
## Schema
[粘贴完整的 DDL 或 ORM Schema 定义]
## 输出格式
为每张表生成以下文档:
### [表名]
- **用途**:[一句话描述表的业务用途]
- **数据量级**:[预估行数和增长速度]
| 列名 | 类型 | 约束 | 默认值 | 说明 |
|------|------|------|--------|------|
| ... | ... | ... | ... | [业务含义说明] |
**索引**:
| 索引名 | 列 | 类型 | 用途 |
|--------|-----|------|------|
| ... | ... | ... | [服务哪些查询] |
**关系**:
- [关系描述,如:一个用户有多篇文章(1:N)]
**注意事项**:
- [特殊的业务规则或技术约束]模板 10:API-数据库映射文档
你是一位全栈架构师。请根据以下 API 端点和数据库 Schema 生成 API-数据库映射文档。
## API 端点列表
[粘贴 API 路由定义或 OpenAPI spec]
## 数据库 Schema
[粘贴 DDL 或 ORM Schema]
## 输出要求
为每个 API 端点生成:
### [HTTP 方法] [路径]
- **功能**:[一句话描述]
- **涉及的表**:[表名列表]
- **查询类型**:[SELECT / INSERT / UPDATE / DELETE / 事务]
- **关键查询**:[核心 SQL 或 ORM 查询]
- **索引依赖**:[该端点依赖哪些索引]
- **性能预估**:[O(1) / O(log n) / O(n)]
- **缓存建议**:[是否适合缓存、缓存策略、TTL]1.7 安全审计 Prompt 模板
模板 11:数据库安全审计
你是一位数据库安全专家。请对以下数据库设计进行安全审计。
## Schema
[粘贴完整的 DDL 或 ORM Schema]
## 应用代码(数据库访问层)
[粘贴关键的数据库访问代码]
## 审计范围
1. **SQL 注入风险**:是否有字符串拼接 SQL?参数化查询是否正确?
2. **敏感数据保护**:密码是否哈希存储?PII 是否加密?
3. **权限控制**:数据库用户权限是否最小化?是否使用了 superuser?
4. **数据完整性**:是否有足够的约束防止脏数据?
5. **审计追踪**:是否记录了关键数据变更?
6. **备份与恢复**:是否有备份策略?恢复是否经过测试?
7. **连接安全**:是否使用 SSL/TLS 连接?连接字符串是否安全存储?
8. **行级安全**:是否需要 RLS(Row Level Security)?
## 输出格式
- 🔴 高危(必须立即修复)
- 🟡 中危(建议尽快修复)
- 🟢 低危(建议改进)
- 每个问题提供:风险描述 → 攻击场景 → 修复方案模板 12:数据库权限最小化审查
你是一位数据库安全专家。请审查以下数据库权限配置并提供最小化建议。
## 当前权限配置
[粘贴数据库用户和权限列表,或 GRANT 语句]
## 应用架构
- 应用数量:[几个应用连接此数据库]
- 每个应用的数据库操作范围:[描述每个应用需要访问哪些表和操作]
## 输出要求
1. 当前权限的风险评估
2. 推荐的用户角色设计(应用用户、只读用户、迁移用户、管理员)
3. 每个角色的最小权限 GRANT 语句
4. 行级安全(RLS)策略建议(如适用)
5. 连接池配置建议
6. 密码策略和轮换建议2. 数据库六大反模式深度剖析
AI 编码助手在数据库设计中最常犯的六大反模式,每种反模式都包含:问题描述、AI 犯错原因、检测方法、Steering 规则和修复 Prompt 模板。
┌─────────────────────────────────────────────────────────────────────┐
│ AI 数据库设计六大反模式 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 🔄 过度范式化 2. 🔍 缺少索引 │
│ JOIN 过多 全表扫描 │
│ 查询性能退化 慢查询 │
│ │
│ 3. 📏 类型错误 4. 💥 不安全迁移 │
│ TEXT vs VARCHAR DROP 无备份 │
│ FLOAT vs DECIMAL 无回滚计划 │
│ │
│ 5. 🔓 缺少约束 6. 📝 命名不规范 │
│ 无 UNIQUE/CHECK 混合命名风格 │
│ 数据完整性差 可读性差 │
│ │
└─────────────────────────────────────────────────────────────────────┘2.1 反模式 1:过度范式化(Over-Normalization)
问题描述
过度范式化是 AI 在数据库设计中最常见的反模式之一。AI 倾向于严格遵循第三范式(3NF)甚至更高范式,将数据拆分到过多的表中,导致查询时需要大量 JOIN 操作,严重影响读取性能。
典型症状:
- 一个简单的列表查询需要 JOIN 5-8 张表
- 查询响应时间随数据量线性增长
- 频繁查询的数据分散在多张表中
- 存在大量只有 2-3 列的”查找表”(lookup tables)
AI 犯错原因
- 训练数据偏差:AI 的训练数据中包含大量数据库教科书内容,这些教材强调范式化的理论正确性,但较少讨论实际生产中的性能权衡
- 缺乏业务上下文:AI 不了解实际的查询模式和数据访问频率,无法判断哪些地方需要反范式化
- 安全倾向:AI 倾向于选择”理论上正确”的方案,而非”实践中最优”的方案
- 忽略读写比例:AI 不会主动询问读写比例,默认按照写优化(范式化)设计
❌ 反模式示例
-- AI 生成的过度范式化 Schema
-- 一个简单的用户地址信息被拆分成 5 张表
CREATE TABLE countries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
code CHAR(2) NOT NULL UNIQUE
);
CREATE TABLE provinces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
country_id UUID NOT NULL REFERENCES countries(id),
UNIQUE(name, country_id)
);
CREATE TABLE cities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
province_id UUID NOT NULL REFERENCES provinces(id),
UNIQUE(name, province_id)
);
CREATE TABLE streets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
city_id UUID NOT NULL REFERENCES cities(id)
);
CREATE TABLE user_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
street_id UUID NOT NULL REFERENCES streets(id),
building_number VARCHAR(20),
apartment VARCHAR(20),
postal_code VARCHAR(20),
is_primary BOOLEAN DEFAULT false
);
-- 查询用户完整地址需要 JOIN 5 张表!
SELECT
u.name,
ua.building_number,
ua.apartment,
ua.postal_code,
s.name AS street,
c.name AS city,
p.name AS province,
co.name AS country
FROM users u
JOIN user_addresses ua ON ua.user_id = u.id
JOIN streets s ON s.id = ua.street_id
JOIN cities c ON c.id = s.city_id
JOIN provinces p ON p.id = c.province_id
JOIN countries co ON co.id = p.country_id
WHERE u.id = $1 AND ua.is_primary = true;
-- 5 个 JOIN,性能随数据量增长急剧下降✅ 正确做法
-- 适度反范式化:将地址信息合并到一张表
CREATE TABLE user_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label VARCHAR(50) DEFAULT 'home', -- 地址标签
street VARCHAR(200) NOT NULL,
building_number VARCHAR(20),
apartment VARCHAR(20),
city VARCHAR(100) NOT NULL,
province VARCHAR(100) NOT NULL,
country VARCHAR(100) NOT NULL DEFAULT '中国',
country_code CHAR(2) NOT NULL DEFAULT 'CN',
postal_code VARCHAR(20),
is_primary BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 如果确实需要标准化的地区数据,使用单独的参考表
-- 但地址表本身存储冗余的文本值,查询时无需 JOIN
CREATE TABLE regions (
code VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_code VARCHAR(20) REFERENCES regions(code),
level SMALLINT NOT NULL CHECK (level IN (1, 2, 3)) -- 省/市/区
);
-- 查询用户地址:0 个 JOIN,O(1) 查询
SELECT * FROM user_addresses
WHERE user_id = $1 AND is_primary = true;检测方法
-- 检测需要过多 JOIN 的查询(PostgreSQL)
-- 在 pg_stat_statements 中查找 JOIN 数量多的查询
SELECT
query,
calls,
mean_exec_time,
(LENGTH(query) - LENGTH(REPLACE(UPPER(query), 'JOIN', ''))) / 4 AS join_count
FROM pg_stat_statements
WHERE (LENGTH(query) - LENGTH(REPLACE(UPPER(query), 'JOIN', ''))) / 4 >= 4
ORDER BY mean_exec_time DESC
LIMIT 20;# 检测 Prompt
请分析以下 Schema,识别过度范式化的问题:
1. 哪些表可以合并?
2. 哪些 JOIN 可以通过冗余字段消除?
3. 合并后对写入一致性的影响是什么?
4. 建议的反范式化方案和数据同步策略?Steering 规则
## 数据库范式化规则
- 默认使用第三范式(3NF),但必须根据查询模式评估是否需要反范式化
- 如果一个常见查询需要 JOIN 超过 3 张表,必须考虑反范式化
- 地址、标签、分类等低变更频率的数据,优先使用冗余字段而非关联表
- 只有 2-3 列的"查找表"(如 status_types、priority_levels),应改用 ENUM 或 CHECK 约束
- 反范式化时必须说明数据同步策略(触发器、应用层、事件驱动)
- 读多写少的场景(读写比 > 10:1),优先考虑查询性能而非写入范式修复 Prompt 模板
请审查以下 Schema 的范式化程度,识别过度范式化的问题。
## Schema
[粘贴 Schema]
## 核心查询模式
[列出最常用的 5-10 个查询]
## 读写比例
[读多写少 / 读写均衡 / 写多读少]
## 要求
1. 识别所有需要 JOIN 超过 3 张表的查询
2. 对每个过度范式化的区域,提供反范式化方案
3. 评估反范式化后的数据一致性风险
4. 提供数据同步策略(如何保持冗余数据一致)
5. 给出修改后的 Schema DDL2.2 反模式 2:缺少索引(Missing Indexes)
问题描述
缺少索引是 AI 生成数据库代码中最普遍的性能问题。AI 在生成 Schema 时往往只关注数据结构的正确性,而忽略了查询性能所需的索引。这导致生产环境中出现全表扫描(Sequential Scan),查询响应时间随数据量线性增长,最终引发系统性能瓶颈。
典型症状:
- 列表查询在数据量超过 10 万行后明显变慢
- EXPLAIN ANALYZE 显示 Seq Scan(全表扫描)
- WHERE 子句中的筛选字段没有索引
- ORDER BY 字段没有索引,导致排序溢出到磁盘
- 外键字段没有索引,JOIN 操作缓慢
- 复合查询条件没有复合索引,每个条件单独使用索引效率低
AI 犯错原因
- 关注结构而非性能:AI 的首要目标是生成”正确”的 Schema,索引被视为次要的优化步骤
- 缺乏查询模式信息:AI 不知道应用的实际查询模式,无法判断哪些字段需要索引
- ORM 抽象层遮蔽:使用 ORM 时,AI 生成的是模型定义而非 SQL,索引容易被遗忘
- 过度依赖主键索引:AI 知道主键自动创建索引,但忽略了其他查询条件的索引需求
- 不了解数据分布:AI 无法判断字段的基数(cardinality),可能为低基数字段建议索引(无效)或忽略高基数字段的索引需求
❌ 反模式示例
-- AI 生成的 Schema:只有主键索引,缺少关键查询索引
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id),
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL
);
-- 常见查询全部触发全表扫描:
-- 查询用户订单列表 → Seq Scan on orders
SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC;
-- 查询待处理订单 → Seq Scan on orders
SELECT * FROM orders WHERE status = 'pending';
-- 查询订单详情(含商品)→ Seq Scan on order_items
SELECT * FROM order_items WHERE order_id = $1;
-- 按日期范围查询 → Seq Scan on orders
SELECT * FROM orders
WHERE created_at BETWEEN $1 AND $2
ORDER BY created_at DESC;
-- 统计用户消费总额 → Seq Scan on orders
SELECT SUM(total_amount) FROM orders WHERE user_id = $1;✅ 正确做法
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 外键索引:加速 JOIN 和按用户查询
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 复合索引:用户订单列表(按时间倒序)
CREATE INDEX idx_orders_user_id_created_at ON orders(user_id, created_at DESC);
-- 状态索引:部分索引只索引活跃状态,减少索引大小
CREATE INDEX idx_orders_status_pending ON orders(status)
WHERE status IN ('pending', 'processing', 'shipped');
-- 时间范围查询索引
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
-- 覆盖索引:用户订单列表常用字段(避免回表)
CREATE INDEX idx_orders_user_list ON orders(user_id, created_at DESC)
INCLUDE (status, total_amount);
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10, 2) NOT NULL CHECK (unit_price >= 0)
);
-- 外键索引
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);检测方法
-- PostgreSQL:查找缺少索引的外键
SELECT
tc.table_name,
kcu.column_name,
tc.constraint_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = tc.table_name
AND indexdef LIKE '%' || kcu.column_name || '%'
)
ORDER BY tc.table_name;
-- PostgreSQL:查找未使用索引的全表扫描查询
SELECT
schemaname,
relname AS table_name,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
CASE WHEN seq_scan > 0
THEN round(seq_tup_read::numeric / seq_scan, 2)
ELSE 0
END AS avg_rows_per_scan
FROM pg_stat_user_tables
WHERE seq_scan > 100
AND (idx_scan IS NULL OR idx_scan < seq_scan * 0.1)
ORDER BY seq_tup_read DESC
LIMIT 20;
-- PostgreSQL:查找未使用的索引(可以安全删除)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE '%_pkey'
AND indexrelname NOT LIKE '%_unique%'
ORDER BY pg_relation_size(indexrelid) DESC;Steering 规则
## 索引策略规则
- 所有外键字段必须创建索引
- WHERE 子句中频繁使用的字段必须有索引
- ORDER BY 字段应包含在索引中(注意排序方向 ASC/DESC)
- 复合查询条件使用复合索引,列顺序遵循"等值条件在前,范围条件在后"原则
- 大表(>100 万行)的索引创建必须使用 CONCURRENTLY(PostgreSQL)
- 低基数字段(如 status、type)考虑使用部分索引(WHERE 条件过滤)
- 禁止为每个列都创建单独索引(索引过多影响写入性能)
- 每张表的索引数量建议不超过 8 个
- 新增索引时必须评估对写入性能的影响
- 定期检查未使用的索引并清理修复 Prompt 模板
请分析以下 Schema 和查询模式,推荐缺失的索引。
## Schema
[粘贴 DDL]
## 常见查询(按频率排序)
[列出 TOP 10 查询]
## 数据规模
[每张表的行数]
## 要求
1. 识别所有缺少索引的外键
2. 为每个常见查询推荐索引(含 CREATE INDEX DDL)
3. 推荐复合索引和覆盖索引
4. 评估部分索引的适用场景
5. 估算索引对写入性能的影响
6. 标记可以安全删除的冗余索引
7. 大表索引使用 CREATE INDEX CONCURRENTLY2.3 反模式 3:类型错误(Wrong Data Types)
问题描述
数据类型选择错误是 AI 生成数据库代码中最隐蔽的问题之一。错误的数据类型在开发和测试阶段通常不会暴露,但在生产环境中会导致精度丢失、存储浪费、查询性能下降,甚至数据损坏。最典型的案例是使用 FLOAT/DOUBLE 存储金额,导致精度丢失引发财务差异。
典型症状:
- 金额计算出现 0.01 的误差(FLOAT 精度问题)
- 存储空间异常膨胀(TEXT 代替 VARCHAR)
- ID 溢出(INT 代替 BIGINT,超过 21 亿后溢出)
- 时区混乱(TIMESTAMP 代替 TIMESTAMPTZ)
- 枚举值无法扩展(硬编码字符串代替 ENUM)
- IP 地址存储为 VARCHAR 而非 INET 类型
AI 犯错原因
- 默认选择”安全”类型:AI 倾向于选择更大、更通用的类型(TEXT 代替 VARCHAR,BIGINT 代替 INT),认为”大一点总没错”
- 忽略数据库特有类型:AI 可能不了解 PostgreSQL 的 INET、CIDR、JSONB、TSTZRANGE 等专用类型
- 编程语言类型映射错误:AI 可能将 JavaScript 的
number(浮点数)直接映射为 SQL 的 FLOAT,而非 DECIMAL - 缺乏业务语义理解:AI 不理解”价格”应该用 DECIMAL 而非 FLOAT,“年龄”应该用 SMALLINT 而非 BIGINT
- ORM 类型抽象:某些 ORM 的类型映射不够精确,AI 依赖 ORM 默认类型而非显式指定
❌ 反模式示例
-- AI 生成的类型选择错误示例
CREATE TABLE products (
id INTEGER PRIMARY KEY, -- ❌ INT 可能溢出(最大 ~21 亿)
name TEXT NOT NULL, -- ❌ TEXT 无长度限制,浪费存储
description TEXT, -- ✅ 长文本用 TEXT 是正确的
price FLOAT NOT NULL, -- ❌ FLOAT 存储金额会丢失精度!
discount_rate DOUBLE PRECISION, -- ❌ 折扣率用 FLOAT 也会丢失精度
weight FLOAT, -- ⚠️ 重量用 FLOAT 可接受,但 NUMERIC 更精确
sku TEXT NOT NULL UNIQUE, -- ❌ SKU 有固定格式,应用 VARCHAR
status TEXT NOT NULL DEFAULT 'active', -- ❌ 状态应用 ENUM 或 CHECK 约束
ip_address TEXT, -- ❌ PostgreSQL 有专用 INET 类型
metadata TEXT, -- ❌ JSON 数据应用 JSONB 类型
tags TEXT, -- ❌ 数组数据应用 TEXT[] 或 JSONB
created_at TIMESTAMP DEFAULT now() -- ❌ 缺少时区信息,应用 TIMESTAMPTZ
);# Python 中 FLOAT 精度问题演示
>>> 0.1 + 0.2
0.30000000000000004
>>> 19.99 * 100
1998.9999999999998
# 如果用 FLOAT 存储价格 $19.99,乘以数量 100:
# 期望结果:$1999.00
# 实际结果:$1998.9999999999998
# 差异:$0.0000000000002(看似微小,但大量交易累积后差异显著)✅ 正确做法
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- ✅ BIGINT 防溢出,IDENTITY 替代 SERIAL
name VARCHAR(200) NOT NULL, -- ✅ 有长度限制
description TEXT, -- ✅ 长文本用 TEXT
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0), -- ✅ NUMERIC/DECIMAL 精确存储金额
discount_rate NUMERIC(5, 4) CHECK (discount_rate BETWEEN 0 AND 1), -- ✅ 精确小数
weight NUMERIC(8, 3), -- ✅ 精确重量
sku VARCHAR(50) NOT NULL UNIQUE, -- ✅ 有长度限制的唯一标识
status VARCHAR(20) NOT NULL DEFAULT 'active' -- ✅ 使用 CHECK 约束
CHECK (status IN ('active', 'inactive', 'discontinued', 'draft')),
ip_address INET, -- ✅ PostgreSQL 专用 IP 类型
metadata JSONB DEFAULT '{}', -- ✅ JSONB 支持索引和查询
tags TEXT[] DEFAULT '{}', -- ✅ PostgreSQL 数组类型
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- ✅ 带时区的时间戳
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- JSONB 字段的 GIN 索引
CREATE INDEX idx_products_metadata ON products USING GIN (metadata);
-- 数组字段的 GIN 索引
CREATE INDEX idx_products_tags ON products USING GIN (tags);常见数据类型选择指南
| 数据场景 | ❌ 错误类型 | ✅ 正确类型 | 原因 |
|---|---|---|---|
| 金额/价格 | FLOAT / DOUBLE | NUMERIC(10,2) / DECIMAL(10,2) | FLOAT 有精度丢失,金融计算必须精确 |
| 百分比/费率 | FLOAT | NUMERIC(5,4) | 0.0001 精度的费率需要精确存储 |
| 主键 ID | INT / SERIAL | BIGINT / UUID v7 | INT 最大 ~21 亿,高增长系统会溢出 |
| 短文本(名称) | TEXT | VARCHAR(N) | VARCHAR 有长度约束,防止异常数据 |
| 长文本(描述) | VARCHAR(10000) | TEXT | 超长 VARCHAR 不如 TEXT 高效 |
| 状态/类型 | TEXT / VARCHAR | ENUM 或 VARCHAR + CHECK | 约束合法值,防止脏数据 |
| 时间戳 | TIMESTAMP | TIMESTAMPTZ | 带时区避免时区转换问题 |
| JSON 数据 | TEXT | JSONB(PostgreSQL) | JSONB 支持索引、查询、部分更新 |
| IP 地址 | VARCHAR(45) | INET(PostgreSQL) | 专用类型支持网络运算和索引 |
| 布尔值 | INT(1) / TINYINT | BOOLEAN | 语义清晰,存储高效 |
| 邮箱 | TEXT | VARCHAR(320) + CHECK | RFC 5321 规定邮箱最长 320 字符 |
| URL | TEXT | VARCHAR(2048) | 浏览器 URL 长度限制约 2048 字符 |
| 电话号码 | INT / BIGINT | VARCHAR(20) | 电话号码含前导零和国际区号 |
| 邮政编码 | INT | VARCHAR(10) | 邮编可能含前导零(如 01234) |
检测方法
-- 检测使用 FLOAT/DOUBLE 存储的可能金额字段
SELECT
table_name,
column_name,
data_type,
udt_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND (
(data_type IN ('real', 'double precision') AND (
column_name LIKE '%price%'
OR column_name LIKE '%amount%'
OR column_name LIKE '%cost%'
OR column_name LIKE '%fee%'
OR column_name LIKE '%rate%'
OR column_name LIKE '%balance%'
OR column_name LIKE '%total%'
OR column_name LIKE '%salary%'
OR column_name LIKE '%tax%'
))
OR (data_type = 'text' AND column_name LIKE '%status%')
OR (data_type = 'text' AND column_name LIKE '%type%')
OR (data_type = 'integer' AND column_name LIKE '%phone%')
OR (data_type = 'timestamp without time zone')
)
ORDER BY table_name, column_name;Steering 规则
## 数据类型选择规则
- 金额、价格、费率等财务数据必须使用 NUMERIC/DECIMAL,禁止 FLOAT/DOUBLE
- 主键使用 BIGINT GENERATED ALWAYS AS IDENTITY 或 UUID v7,禁止 INT/SERIAL
- 短文本(名称、标题、SKU)使用 VARCHAR(N) 并指定合理长度
- 长文本(描述、内容、评论)使用 TEXT
- 时间戳必须使用 TIMESTAMPTZ(带时区),禁止 TIMESTAMP
- 状态/类型字段使用 VARCHAR + CHECK 约束或 ENUM
- JSON 数据使用 JSONB(PostgreSQL),不要用 TEXT 存储 JSON
- 电话号码、邮政编码使用 VARCHAR,不要用数字类型(含前导零)
- PostgreSQL 项目优先使用专用类型:INET、CIDR、JSONB、TEXT[]、TSTZRANGE
- 布尔值使用 BOOLEAN,不要用 INT(0/1)修复 Prompt 模板
请审查以下 Schema 中的数据类型选择,识别不当的类型并提供修正。
## Schema
[粘贴 DDL]
## 业务上下文
[描述每张表的业务用途,特别是涉及金额、时间、状态的字段]
## 数据库
[PostgreSQL / MySQL / SQLite]
## 审查重点
1. 金额/价格字段是否使用了 FLOAT?→ 改为 NUMERIC/DECIMAL
2. 主键是否使用了 INT?→ 评估是否需要改为 BIGINT
3. 时间戳是否缺少时区?→ 改为 TIMESTAMPTZ
4. 状态字段是否缺少约束?→ 添加 CHECK 或 ENUM
5. 是否有可以使用数据库专用类型的场景?
6. VARCHAR 长度是否合理?
7. 提供修正后的完整 DDL2.4 反模式 4:不安全迁移(Unsafe Migrations)
问题描述
不安全迁移是数据库反模式中风险最高的一类,可能直接导致生产数据丢失、服务中断或不可逆的 Schema 损坏。AI 编码助手在生成迁移文件时,往往只关注”让 Schema 变成目标状态”,而忽略了迁移过程中的安全性——包括数据备份、回滚计划、锁表影响和零停机兼容性。
典型症状:
- 迁移文件包含 DROP TABLE 或 DROP COLUMN 且无数据备份
- 大表上的 ALTER TABLE 导致长时间锁表,服务不可用
- 新增 NOT NULL 列没有默认值,导致迁移失败
- 没有 down 迁移(回滚脚本),出问题无法恢复
- 迁移与当前运行的应用代码不兼容,导致部署期间错误
- 数据回填在单个事务中执行,大数据量时 OOM 或超时
AI 犯错原因
- 目标导向思维:AI 只关注”从状态 A 到状态 B”,不考虑迁移过程的安全性
- 缺乏生产环境意识:AI 在开发环境中生成迁移,不了解生产环境的数据量和并发压力
- ORM 迁移工具的局限:Prisma、TypeORM 等工具的自动迁移可能生成破坏性操作
- 忽略部署流程:AI 不了解应用的部署方式(滚动更新、蓝绿部署),无法判断迁移是否兼容
- 缺乏回滚意识:AI 很少主动生成 down 迁移或回滚脚本
❌ 反模式示例
-- 危险迁移 1:直接删除列,无备份
ALTER TABLE users DROP COLUMN phone_number;
-- 🚫 数据永久丢失,无法恢复!
-- 危险迁移 2:重命名列(旧代码会立即报错)
ALTER TABLE users RENAME COLUMN user_name TO display_name;
-- 🚫 滚动更新期间,旧版本应用会因找不到 user_name 列而崩溃!
-- 危险迁移 3:大表添加带默认值的列(PostgreSQL < 11 会锁表)
ALTER TABLE orders ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
-- ⚠️ PostgreSQL 11+ 不会锁表,但 MySQL 仍会锁表
-- 危险迁移 4:大表创建索引(锁表)
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- 🚫 大表上 CREATE INDEX 会锁表,应使用 CONCURRENTLY
-- 危险迁移 5:单事务数据回填(大数据量 OOM)
UPDATE orders SET priority =
CASE WHEN total_amount > 1000 THEN 1 ELSE 0 END;
-- 🚫 1000 万行的 UPDATE 在单事务中执行,可能 OOM 或超时
-- 危险迁移 6:添加外键约束到大表
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_product
FOREIGN KEY (product_id) REFERENCES products(id);
-- 🚫 需要扫描整张表验证约束,大表会长时间锁表✅ 正确做法
-- 安全迁移 1:删除列的三步法
-- 步骤 1(迁移 1):应用代码停止读写该列
-- 步骤 2(迁移 2):将列标记为可空(安全网)
ALTER TABLE users ALTER COLUMN phone_number DROP NOT NULL;
-- 步骤 3(迁移 3):确认无代码引用后,删除列
-- 建议:先备份数据到归档表
CREATE TABLE _archive_users_phone AS
SELECT id, phone_number FROM users WHERE phone_number IS NOT NULL;
ALTER TABLE users DROP COLUMN phone_number;
-- 安全迁移 2:重命名列的四步法
-- 步骤 1:添加新列
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
-- 步骤 2:数据回填(分批)
UPDATE users SET display_name = user_name WHERE display_name IS NULL;
-- 步骤 3:应用代码同时读写两列(兼容期)
-- 步骤 4:确认所有服务已更新后,删除旧列
ALTER TABLE users DROP COLUMN user_name;
-- 安全迁移 3:大表创建索引(PostgreSQL)
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);
-- ✅ CONCURRENTLY 不锁表,但耗时更长
-- 安全迁移 4:分批数据回填
DO $$
DECLARE
batch_size INTEGER := 10000;
affected INTEGER;
BEGIN
LOOP
UPDATE orders
SET priority = CASE WHEN total_amount > 1000 THEN 1 ELSE 0 END
WHERE id IN (
SELECT id FROM orders
WHERE priority IS NULL
LIMIT batch_size
FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS affected = ROW_COUNT;
EXIT WHEN affected = 0;
RAISE NOTICE 'Updated % rows', affected;
PERFORM pg_sleep(0.1); -- 短暂暂停,减少锁竞争
COMMIT;
END LOOP;
END $$;
-- 安全迁移 5:添加外键约束(PostgreSQL)
-- 先添加约束但不验证(不锁表)
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_product
FOREIGN KEY (product_id) REFERENCES products(id)
NOT VALID;
-- 然后异步验证(不锁表,但需要扫描)
ALTER TABLE order_items
VALIDATE CONSTRAINT fk_order_items_product;迁移安全检查清单
┌─────────────────────────────────────────────────────────────────┐
│ 迁移安全检查清单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ □ 是否有 DROP TABLE / DROP COLUMN? │
│ → 必须先备份数据,确认无代码引用 │
│ │
│ □ 是否有 RENAME COLUMN / RENAME TABLE? │
│ → 使用"添加新列→回填→切换→删除旧列"四步法 │
│ │
│ □ 是否在大表上 ALTER TABLE? │
│ → 评估锁表时间,考虑维护窗口或在线 DDL 工具 │
│ │
│ □ 是否在大表上 CREATE INDEX? │
│ → PostgreSQL 使用 CONCURRENTLY │
│ → MySQL 使用 pt-online-schema-change 或 gh-ost │
│ │
│ □ 新增 NOT NULL 列是否有默认值? │
│ → 必须提供 DEFAULT 值,否则迁移失败 │
│ │
│ □ 是否有数据回填操作? │
│ → 必须分批执行,避免单事务 OOM │
│ │
│ □ 是否有回滚脚本(down 迁移)? │
│ → 每个 up 迁移必须有对应的 down 迁移 │
│ │
│ □ 迁移是否与当前运行代码兼容? │
│ → 滚动更新期间,新旧代码必须都能正常工作 │
│ │
│ □ 是否在测试环境用生产级数据量验证过? │
│ → 迁移时间和锁影响必须在类生产环境中测试 │
│ │
└─────────────────────────────────────────────────────────────────┘Steering 规则
## 迁移安全规则
- 禁止在迁移中直接 DROP TABLE 或 DROP COLUMN,必须先备份数据
- 重命名列/表必须使用"添加→回填→切换→删除"四步法
- 大表(>100 万行)的 CREATE INDEX 必须使用 CONCURRENTLY(PostgreSQL)
- 新增 NOT NULL 列必须提供 DEFAULT 值
- 数据回填必须分批执行(每批 ≤ 10000 行),禁止单事务全表 UPDATE
- 每个 up 迁移必须有对应的 down 迁移(回滚脚本)
- 添加外键约束到大表使用 NOT VALID + VALIDATE 两步法
- 迁移必须与当前运行的应用代码兼容(支持滚动更新)
- 生产迁移前必须在类生产环境(数据量、并发)中测试
- 迁移文件一旦部署到生产环境,禁止修改,只能添加新迁移
- 使用 --create-only 生成迁移文件后人工审查,禁止自动应用修复 Prompt 模板
请审查以下迁移文件的安全性,并提供安全的替代方案。
## 迁移文件
[粘贴迁移文件内容]
## 环境信息
- 数据库:[PostgreSQL / MySQL]
- 受影响表的数据量:[行数]
- 部署方式:[滚动更新 / 蓝绿部署 / 维护窗口]
- ORM:[Prisma / Drizzle / SQLAlchemy / TypeORM]
## 审查要求
1. 识别所有破坏性操作(DROP、RENAME、ALTER NOT NULL)
2. 评估锁表风险和预估锁表时间
3. 检查是否有回滚脚本
4. 检查是否与当前运行代码兼容
5. 提供安全的替代迁移方案(分步执行)
6. 生成完整的 up 和 down 迁移代码
7. 提供迁移执行前的检查命令和迁移后的验证命令2.5 反模式 5:缺少约束(Missing Constraints)
问题描述
数据库约束是数据完整性的最后一道防线。AI 编码助手在生成 Schema 时,往往只定义了基本的表结构和外键关系,而忽略了 UNIQUE、CHECK、NOT NULL 等关键约束。这导致应用层的验证逻辑成为唯一的数据保护机制——一旦应用层验证被绕过(API 直接调用、数据导入、手动 SQL 操作),脏数据就会进入数据库,引发难以排查的业务逻辑错误。
典型症状:
- 同一用户注册了多个相同邮箱的账号(缺少 UNIQUE)
- 订单金额出现负数(缺少 CHECK)
- 关键字段存在 NULL 值导致查询结果异常(缺少 NOT NULL)
- 状态字段出现非法值(缺少 CHECK 或 ENUM)
- 日期范围不合理(结束日期早于开始日期,缺少 CHECK)
- 百分比字段超过 100%(缺少 CHECK)
AI 犯错原因
- 依赖应用层验证:AI 认为”应用层已经做了验证”,数据库层不需要重复约束
- ORM 抽象遮蔽:某些 ORM 不方便定义 CHECK 约束,AI 就跳过了
- 简化倾向:AI 为了生成”简洁”的 Schema,省略了约束定义
- 缺乏防御性思维:AI 不会考虑”如果有人直接操作数据库怎么办”
- NOT NULL 的过度谨慎:AI 担心 NOT NULL 导致插入失败,倾向于让字段可空
❌ 反模式示例
-- AI 生成的缺少约束的 Schema
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255), -- ❌ 缺少 NOT NULL 和 UNIQUE
username VARCHAR(50), -- ❌ 缺少 NOT NULL 和 UNIQUE
age INTEGER, -- ❌ 缺少 CHECK (age > 0 AND age < 150)
role VARCHAR(20) DEFAULT 'user', -- ❌ 缺少 CHECK 约束合法值
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id), -- ❌ 缺少 NOT NULL 和 ON DELETE 策略
total_amount DECIMAL(10, 2), -- ❌ 缺少 NOT NULL 和 CHECK (>= 0)
discount_percent DECIMAL(5, 2), -- ❌ 缺少 CHECK (0-100)
status VARCHAR(20), -- ❌ 缺少 NOT NULL、DEFAULT 和 CHECK
start_date DATE,
end_date DATE, -- ❌ 缺少 CHECK (end_date >= start_date)
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
plan VARCHAR(20), -- ❌ 缺少约束
-- ❌ 缺少 UNIQUE(user_id, plan) 防止重复订阅
created_at TIMESTAMP DEFAULT now()
);-- 缺少约束导致的脏数据示例
INSERT INTO users (email, age, role) VALUES (NULL, -5, 'superadmin');
-- ✅ 插入成功!但数据完全无效:
-- email 为 NULL、age 为负数、role 不是合法值
INSERT INTO orders (total_amount, discount_percent, status)
VALUES (-100.00, 150.00, 'invalid_status');
-- ✅ 插入成功!但:
-- 金额为负数、折扣超过 100%、状态不合法✅ 正确做法
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(320) NOT NULL UNIQUE, -- ✅ NOT NULL + UNIQUE
username VARCHAR(50) NOT NULL UNIQUE, -- ✅ NOT NULL + UNIQUE
age SMALLINT CHECK (age > 0 AND age < 150), -- ✅ CHECK 约束合理范围
role VARCHAR(20) NOT NULL DEFAULT 'user' -- ✅ NOT NULL + DEFAULT + CHECK
CHECK (role IN ('user', 'admin', 'moderator')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- ✅ NOT NULL + ON DELETE
total_amount NUMERIC(10, 2) NOT NULL CHECK (total_amount >= 0), -- ✅ NOT NULL + CHECK
discount_percent NUMERIC(5, 2) DEFAULT 0 -- ✅ DEFAULT + CHECK
CHECK (discount_percent >= 0 AND discount_percent <= 100),
status VARCHAR(20) NOT NULL DEFAULT 'pending' -- ✅ NOT NULL + DEFAULT + CHECK
CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
start_date DATE,
end_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- ✅ 表级 CHECK 约束:结束日期必须晚于开始日期
CONSTRAINT chk_date_range CHECK (end_date IS NULL OR end_date >= start_date)
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan VARCHAR(20) NOT NULL
CHECK (plan IN ('free', 'basic', 'pro', 'enterprise')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- ✅ 复合 UNIQUE:每个用户每种计划只能有一个活跃订阅
UNIQUE(user_id, plan)
);约束类型速查表
| 约束类型 | 用途 | 示例 | AI 遗漏频率 |
|---|---|---|---|
| NOT NULL | 确保字段必须有值 | email VARCHAR(320) NOT NULL | ⭐⭐⭐⭐ 高 |
| UNIQUE | 确保字段值唯一 | email VARCHAR(320) UNIQUE | ⭐⭐⭐⭐ 高 |
| CHECK | 确保字段值满足条件 | CHECK (age > 0 AND age < 150) | ⭐⭐⭐⭐⭐ 极高 |
| DEFAULT | 提供默认值 | DEFAULT 'pending' | ⭐⭐⭐ 中 |
| FOREIGN KEY | 确保引用完整性 | REFERENCES users(id) | ⭐⭐ 低 |
| ON DELETE | 定义级联删除策略 | ON DELETE CASCADE | ⭐⭐⭐⭐ 高 |
| EXCLUDE | 排除约束(PostgreSQL) | 防止时间范围重叠 | ⭐⭐⭐⭐⭐ 极高 |
| 复合 UNIQUE | 多列组合唯一 | UNIQUE(user_id, plan) | ⭐⭐⭐⭐ 高 |
检测方法
-- 检测缺少 NOT NULL 约束的关键字段
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND is_nullable = 'YES'
AND (
column_name LIKE '%_id' -- 外键通常应该 NOT NULL
OR column_name = 'email'
OR column_name = 'status'
OR column_name = 'name'
OR column_name = 'type'
OR column_name = 'created_at'
)
ORDER BY table_name, column_name;
-- 检测缺少 UNIQUE 约束的候选字段
SELECT
table_name,
column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND (
column_name = 'email'
OR column_name = 'username'
OR column_name = 'slug'
OR column_name = 'sku'
OR column_name = 'code'
)
AND column_name NOT IN (
SELECT column_name
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type IN ('UNIQUE', 'PRIMARY KEY')
)
ORDER BY table_name;
-- 检测没有 CHECK 约束的表
SELECT
t.table_name,
COUNT(tc.constraint_name) AS check_count
FROM information_schema.tables t
LEFT JOIN information_schema.table_constraints tc
ON t.table_name = tc.table_name
AND tc.constraint_type = 'CHECK'
WHERE t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
GROUP BY t.table_name
HAVING COUNT(tc.constraint_name) = 0
ORDER BY t.table_name;Steering 规则
## 约束完整性规则
- 所有外键字段必须 NOT NULL(除非业务明确允许为空)
- email、username、slug、sku 等自然唯一标识必须有 UNIQUE 约束
- 金额字段必须有 CHECK (amount >= 0)
- 百分比字段必须有 CHECK (percent >= 0 AND percent <= 100)
- 状态/类型字段必须有 CHECK 约束限制合法值
- 日期范围必须有 CHECK (end_date >= start_date)
- 年龄、数量等数值字段必须有合理范围的 CHECK 约束
- 外键必须指定 ON DELETE 策略(CASCADE / SET NULL / RESTRICT)
- 业务上不允许重复的组合必须有复合 UNIQUE 约束
- created_at 和 updated_at 必须 NOT NULL 且有 DEFAULT
- 约束命名使用描述性名称:chk_[表名]_[描述]、uq_[表名]_[列名]修复 Prompt 模板
请审查以下 Schema 的约束完整性,识别缺失的约束并提供修复。
## Schema
[粘贴 DDL]
## 业务规则
[描述关键的业务规则,如:
- 邮箱必须唯一
- 订单金额不能为负
- 每个用户只能有一个活跃订阅
- 结束日期必须晚于开始日期]
## 审查要求
1. 识别所有缺少 NOT NULL 的关键字段
2. 识别所有缺少 UNIQUE 的自然唯一标识
3. 识别所有缺少 CHECK 的数值和状态字段
4. 识别所有缺少 ON DELETE 策略的外键
5. 识别需要复合 UNIQUE 的场景
6. 提供完整的 ALTER TABLE ADD CONSTRAINT 语句
7. 评估添加约束对现有数据的影响(是否有违反约束的脏数据)2.6 反模式 6:命名不规范(Inconsistent Naming)
问题描述
命名不规范是 AI 生成数据库代码中最常见的”软性”问题。虽然不会直接导致功能错误,但混乱的命名会严重影响代码可读性、团队协作效率和长期维护成本。AI 编码助手在不同的对话轮次中可能使用不同的命名风格,或者混合使用 ORM 的 camelCase 和数据库的 snake_case,导致 Schema 中出现风格不一致的命名。
典型症状:
- 同一 Schema 中混合使用 camelCase 和 snake_case
- 表名有的用单数(user)有的用复数(orders)
- 外键命名不一致(有的用 userId,有的用 author_id)
- 布尔字段命名不统一(有的用 is_active,有的用 active,有的用 enabled)
- 时间字段命名不统一(有的用 created_at,有的用 createdAt,有的用 creation_date)
- 缩写使用不一致(有的用 desc,有的用 description)
- 表名前缀不一致(有的用 tbl_,有的不用)
AI 犯错原因
- 多轮对话上下文丢失:AI 在不同对话轮次中可能”忘记”之前使用的命名风格
- 训练数据风格混杂:AI 的训练数据包含各种命名风格的代码,生成时可能随机选择
- ORM 与数据库风格冲突:ORM 通常使用 camelCase(JavaScript/TypeScript),而数据库惯例是 snake_case
- 缺乏项目级命名规范:如果没有在 Steering 规则中明确命名规范,AI 会自行决定
- 复制粘贴残留:AI 可能从不同来源复制代码片段,带入不同的命名风格
❌ 反模式示例
-- 命名混乱的 Schema
CREATE TABLE Users ( -- ❌ PascalCase 表名
UserID UUID PRIMARY KEY, -- ❌ PascalCase 列名
email_address VARCHAR(255), -- ✅ snake_case(但与其他列不一致)
firstName VARCHAR(100), -- ❌ camelCase
last_name VARCHAR(100), -- ✅ snake_case(但与 firstName 不一致)
isActive BOOLEAN, -- ❌ camelCase
created TIMESTAMP, -- ❌ 缺少 _at 后缀
updatedAt TIMESTAMP -- ❌ camelCase
);
CREATE TABLE order ( -- ❌ 单数(与 Users 复数不一致)
id UUID PRIMARY KEY,
userId UUID REFERENCES Users(UserID), -- ❌ camelCase 外键
orderTotal DECIMAL(10, 2), -- ❌ camelCase
order_status VARCHAR(20), -- ✅ snake_case(但与其他列不一致)
desc TEXT, -- ❌ 使用缩写,且 desc 是 SQL 保留字!
creationDate TIMESTAMP -- ❌ camelCase,且与 Users.created 风格不同
);
CREATE TABLE tbl_products ( -- ❌ 使用 tbl_ 前缀(匈牙利命名法)
prod_id UUID PRIMARY KEY, -- ❌ 使用 prod_ 前缀
prod_name VARCHAR(200), -- ❌ 使用 prod_ 前缀
ProductCategory VARCHAR(50), -- ❌ PascalCase
is_available BOOLEAN, -- ✅ snake_case(但与其他表不一致)
SKU VARCHAR(50) -- ❌ 全大写缩写
);✅ 正确做法
-- 统一的 snake_case 命名规范
CREATE TABLE users ( -- ✅ 复数、snake_case
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(320) NOT NULL UNIQUE,
first_name VARCHAR(100) NOT NULL, -- ✅ snake_case
last_name VARCHAR(100) NOT NULL, -- ✅ snake_case
is_active BOOLEAN NOT NULL DEFAULT true, -- ✅ is_ 前缀表示布尔
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- ✅ _at 后缀表示时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- ✅ _at 后缀表示时间
);
CREATE TABLE orders ( -- ✅ 复数、snake_case
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- ✅ _id 后缀
total_amount NUMERIC(10, 2) NOT NULL CHECK (total_amount >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
description TEXT, -- ✅ 完整单词,不用缩写
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE products ( -- ✅ 无前缀
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL, -- ✅ 无前缀
category VARCHAR(50) NOT NULL,
sku VARCHAR(50) NOT NULL UNIQUE, -- ✅ 缩写小写
is_available BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 索引命名规范
CREATE INDEX idx_orders_user_id ON orders(user_id); -- ✅ idx_表名_列名
CREATE INDEX idx_orders_status ON orders(status);
CREATE UNIQUE INDEX uq_users_email ON users(email); -- ✅ uq_表名_列名数据库命名规范速查表
| 元素 | 规范 | 示例 | 说明 |
|---|---|---|---|
| 表名 | 复数 + snake_case | users, order_items | 表示集合 |
| 列名 | snake_case | first_name, created_at | 全小写下划线分隔 |
| 主键 | id | id UUID PRIMARY KEY | 简洁统一 |
| 外键 | {关联表单数}_id | user_id, order_id | 明确关联关系 |
| 布尔字段 | is_ / has_ / can_ 前缀 | is_active, has_avatar | 语义清晰 |
| 时间字段 | _at 后缀 | created_at, deleted_at | 表示时间点 |
| 日期字段 | _date 后缀 | birth_date, due_date | 表示日期 |
| 金额字段 | _amount / _price / _cost | total_amount, unit_price | 语义明确 |
| 计数字段 | _count 后缀 | view_count, like_count | 表示数量 |
| 索引 | idx_{表名}_{列名} | idx_orders_user_id | 前缀标识类型 |
| 唯一索引 | uq_{表名}_{列名} | uq_users_email | 前缀标识类型 |
| CHECK 约束 | chk_{表名}_{描述} | chk_orders_amount_positive | 前缀标识类型 |
| 外键约束 | fk_{表名}_{关联表} | fk_orders_users | 前缀标识类型 |
| 关联表 | {表1}_{表2} | posts_tags, users_roles | 按字母序排列 |
| 枚举类型 | 单数 + snake_case | order_status, user_role | 表示类型 |
ORM 与数据库命名映射
// Prisma:Schema 使用 camelCase,数据库使用 snake_case
model User {
id String @id @default(uuid())
firstName String @map("first_name") // ✅ ORM: camelCase → DB: snake_case
lastName String @map("last_name")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
@@map("users") // ✅ 表名映射
}
model Order {
id String @id @default(uuid())
userId String @map("user_id") // ✅ 外键映射
totalAmount Decimal @map("total_amount")
status String @default("pending")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id])
@@map("orders")
@@index([userId])
}// Drizzle:直接在 Schema 定义中指定数据库列名
import { pgTable, uuid, varchar, boolean, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
firstName: varchar('first_name', { length: 100 }).notNull(), // ✅ 列名 snake_case
lastName: varchar('last_name', { length: 100 }).notNull(),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
});Steering 规则
## 数据库命名规范
- 表名:复数形式 + snake_case(users, order_items)
- 列名:snake_case(first_name, created_at)
- 主键:统一使用 id
- 外键:{关联表单数}_id(user_id, order_id)
- 布尔字段:is_/has_/can_ 前缀(is_active, has_avatar)
- 时间字段:_at 后缀(created_at, updated_at, deleted_at)
- 金额字段:_amount/_price/_cost 后缀(total_amount, unit_price)
- 索引:idx_{表名}_{列名}(idx_orders_user_id)
- 唯一索引:uq_{表名}_{列名}(uq_users_email)
- CHECK 约束:chk_{表名}_{描述}(chk_orders_amount_positive)
- 外键约束:fk_{源表}_{目标表}(fk_orders_users)
- 关联表:{表1}_{表2} 按字母序(posts_tags)
- 禁止使用 SQL 保留字作为列名(desc, order, group, user, table)
- 禁止使用匈牙利命名法前缀(tbl_, col_, fn_)
- 禁止使用缩写(除非是广泛认可的缩写如 id, url, sku)
- ORM 模型使用 camelCase,通过 @map 映射到 snake_case 数据库列名修复 Prompt 模板
请审查以下 Schema 的命名规范,识别不一致的命名并提供统一的修正。
## Schema
[粘贴 DDL 或 ORM Schema]
## 项目命名规范
- 数据库层:snake_case
- ORM/应用层:camelCase
- 表名:复数形式
## 审查要求
1. 识别所有不符合 snake_case 的表名和列名
2. 识别单复数不一致的表名
3. 识别外键命名不规范的字段
4. 识别布尔字段缺少 is_/has_ 前缀的情况
5. 识别时间字段缺少 _at 后缀的情况
6. 识别使用 SQL 保留字的列名
7. 提供完整的重命名迁移 SQL(使用安全的四步法)
8. 提供 ORM 的 @map 映射配置3. 数据库 Steering 规则完整模板
以下是适用于 CLAUDE.md、Kiro Steering 和 Cursor Rules 的完整数据库 Steering 规则模板。
3.1 CLAUDE.md 数据库规则模板
# CLAUDE.md — 数据库设计规则
## 项目数据库信息
- 数据库:PostgreSQL 17
- ORM:Prisma 6 / Drizzle ORM
- 迁移工具:Prisma Migrate / Drizzle Kit
- 连接池:PgBouncer / Prisma Accelerate
## 数据库命名规范(强制)
- 表名:复数 + snake_case(users, order_items)
- 列名:snake_case(first_name, created_at)
- 主键:id(UUID v7 或 BIGINT IDENTITY)
- 外键:{关联表单数}_id(user_id, order_id)
- 布尔字段:is_/has_/can_ 前缀
- 时间字段:_at 后缀(TIMESTAMPTZ)
- 索引:idx_{表名}_{列名}
- 唯一索引:uq_{表名}_{列名}
- CHECK 约束:chk_{表名}_{描述}
- ORM 模型 camelCase,通过 @map 映射到 snake_case
## 数据类型规则(强制)
- 金额/价格:NUMERIC(10,2),禁止 FLOAT/DOUBLE
- 主键:UUID v7 或 BIGINT GENERATED ALWAYS AS IDENTITY
- 时间戳:TIMESTAMPTZ,禁止 TIMESTAMP WITHOUT TIME ZONE
- 状态/类型:VARCHAR + CHECK 约束
- JSON 数据:JSONB,禁止 TEXT 存储 JSON
- 短文本:VARCHAR(N),长文本:TEXT
- 电话/邮编:VARCHAR,禁止数字类型
## 约束规则(强制)
- 所有外键必须 NOT NULL(除非业务明确允许为空)
- 自然唯一标识(email, username, slug)必须 UNIQUE
- 金额字段必须 CHECK (>= 0)
- 状态字段必须 CHECK IN (合法值列表)
- 日期范围必须 CHECK (end >= start)
- 外键必须指定 ON DELETE 策略
- created_at/updated_at 必须 NOT NULL + DEFAULT
## 索引规则(强制)
- 所有外键字段必须有索引
- WHERE 子句常用字段必须有索引
- 复合索引列顺序:等值在前,范围在后
- 大表索引使用 CREATE INDEX CONCURRENTLY
- 每张表索引不超过 8 个
- 低基数字段使用部分索引
## 迁移安全规则(强制)
- 禁止直接 DROP TABLE/COLUMN,必须先备份
- 重命名使用"添加→回填→切换→删除"四步法
- 新增 NOT NULL 列必须有 DEFAULT
- 数据回填必须分批(每批 ≤ 10000 行)
- 每个 up 迁移必须有 down 迁移
- 迁移文件使用 --create-only 生成后人工审查
- 迁移必须与当前运行代码兼容
## 查询性能规则(强制)
- 禁止循环中执行数据库查询(N+1)
- 使用 include/with 预加载关联数据
- 列表查询必须分页(默认 20,最大 100)
- 禁止 SELECT *,使用 select 精确选择字段
- 复杂聚合考虑使用原生 SQL 或物化视图
- 事务中禁止外部 API 调用
## 安全规则(强制)
- 所有查询使用参数化查询或 ORM
- 禁止字符串拼接 SQL
- 密码使用 bcrypt(cost≥12) 或 argon2id 哈希
- PII 字段考虑加密存储
- 数据库用户使用最小权限
- 连接使用 SSL/TLS3.2 Kiro Steering 数据库规则模板
---
description: 数据库设计和迁移的 Steering 规则
globs:
- "prisma/**"
- "drizzle/**"
- "src/db/**"
- "migrations/**"
- "**/*.sql"
- "**/schema.*"
---
# 数据库 Steering 规则
## Schema 设计
- 表名复数 snake_case,列名 snake_case
- 主键 UUID v7 或 BIGINT IDENTITY
- 金额用 NUMERIC,禁止 FLOAT
- 时间用 TIMESTAMPTZ,禁止 TIMESTAMP
- 所有表必须有 created_at 和 updated_at
- 外键必须 NOT NULL + ON DELETE + 索引
- 状态字段必须有 CHECK 约束
## 迁移安全
- 禁止直接 DROP,必须先备份
- 重命名用四步法
- 大表索引用 CONCURRENTLY
- 数据回填分批执行
- 必须有回滚脚本
## 查询性能
- 禁止 N+1 查询
- 必须分页
- 禁止 SELECT *
- 外键必须有索引3.3 Cursor Rules 数据库规则模板
# .cursorrules — 数据库规则
## Database Design Rules
- Use snake_case for all table and column names
- Table names must be plural (users, orders, order_items)
- Primary keys: UUID v7 or BIGINT GENERATED ALWAYS AS IDENTITY
- Foreign keys: {singular_table}_id pattern (user_id, order_id)
- Boolean columns: is_/has_/can_ prefix (is_active, has_avatar)
- Timestamps: TIMESTAMPTZ with _at suffix (created_at, updated_at)
- Money/amounts: NUMERIC(10,2), NEVER use FLOAT or DOUBLE
- All tables must have created_at and updated_at columns
## Constraints
- Foreign keys must be NOT NULL with ON DELETE strategy
- Natural unique identifiers must have UNIQUE constraint
- Amount fields must have CHECK (>= 0)
- Status fields must have CHECK IN (valid_values)
- Date ranges must have CHECK (end >= start)
## Migrations
- Never DROP TABLE/COLUMN without backup
- Use add→backfill→switch→drop for renames
- Large table indexes: CREATE INDEX CONCURRENTLY
- Batch data backfills (≤ 10000 rows per batch)
- Every up migration needs a down migration
## Query Performance
- No N+1 queries (use eager loading)
- All list queries must be paginated
- No SELECT * (use explicit field selection)
- All foreign keys must be indexed4. 数据库设计检查清单
4.1 Schema 设计检查清单
┌─────────────────────────────────────────────────────────────────────┐
│ Schema 设计检查清单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 📐 结构设计 │
│ □ 所有实体和关系是否已识别? │
│ □ 范式化程度是否合理?(不过度也不不足) │
│ □ 常见查询是否需要 JOIN 超过 3 张表? │
│ □ 是否考虑了未来扩展(预留字段、可扩展枚举)? │
│ □ 多对多关系是否使用了关联表? │
│ □ 自引用关系(树形结构)是否设计合理? │
│ │
│ 📝 命名规范 │
│ □ 表名是否统一使用复数 + snake_case? │
│ □ 列名是否统一使用 snake_case? │
│ □ 外键是否使用 {关联表单数}_id 格式? │
│ □ 布尔字段是否使用 is_/has_/can_ 前缀? │
│ □ 时间字段是否使用 _at 后缀? │
│ □ 是否避免了 SQL 保留字? │
│ │
│ 📏 数据类型 │
│ □ 金额字段是否使用 NUMERIC/DECIMAL? │
│ □ 主键类型是否合适(UUID v7 / BIGINT)? │
│ □ 时间戳是否使用 TIMESTAMPTZ? │
│ □ 短文本是否使用 VARCHAR(N)? │
│ □ 状态字段是否有类型约束? │
│ □ 是否利用了数据库专用类型(JSONB, INET, TEXT[])? │
│ │
│ 🔒 约束完整性 │
│ □ 关键字段是否 NOT NULL? │
│ □ 自然唯一标识是否 UNIQUE? │
│ □ 数值字段是否有 CHECK 范围约束? │
│ □ 状态字段是否有 CHECK IN 约束? │
│ □ 日期范围是否有 CHECK 约束? │
│ □ 外键是否指定了 ON DELETE 策略? │
│ □ 是否有需要的复合 UNIQUE 约束? │
│ │
│ 🔍 索引策略 │
│ □ 所有外键是否有索引? │
│ □ WHERE 常用字段是否有索引? │
│ □ ORDER BY 字段是否包含在索引中? │
│ □ 是否有合适的复合索引? │
│ □ 是否考虑了部分索引和覆盖索引? │
│ □ 索引数量是否合理(每表 ≤ 8 个)? │
│ │
│ 🔐 安全性 │
│ □ 密码是否哈希存储? │
│ □ PII 字段是否考虑加密? │
│ □ 是否需要行级安全(RLS)? │
│ □ 数据库用户权限是否最小化? │
│ □ 是否有审计追踪(操作日志)? │
│ │
│ 📊 性能考虑 │
│ □ 所有表是否有 created_at 和 updated_at? │
│ □ 大表是否考虑了分区策略? │
│ □ 是否有适当的软删除策略(deleted_at)? │
│ □ 是否考虑了连接池配置? │
│ □ 是否有缓存策略(哪些数据适合缓存)? │
│ │
└─────────────────────────────────────────────────────────────────────┘4.2 迁移安全检查清单
┌─────────────────────────────────────────────────────────────────────┐
│ 迁移安全检查清单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 🔍 迁移前检查 │
│ □ 迁移文件是否经过人工审查? │
│ □ 是否在类生产环境中测试过? │
│ □ 受影响表的数据量是否已确认? │
│ □ 预估的迁移执行时间是否可接受? │
│ □ 是否有回滚脚本(down 迁移)? │
│ □ 回滚脚本是否经过测试? │
│ □ 是否已备份受影响的数据? │
│ │
│ ⚠️ 破坏性操作检查 │
│ □ 是否有 DROP TABLE / DROP COLUMN?→ 必须先备份 │
│ □ 是否有 RENAME?→ 使用四步法 │
│ □ 是否有 ALTER TYPE?→ 评估数据转换风险 │
│ □ 是否有 TRUNCATE?→ 确认数据可丢弃 │
│ │
│ 🔒 锁表风险检查 │
│ □ 大表 ALTER TABLE 是否会锁表? │
│ □ CREATE INDEX 是否使用 CONCURRENTLY? │
│ □ 添加外键是否使用 NOT VALID + VALIDATE? │
│ □ 是否需要在维护窗口执行? │
│ │
│ 🔄 兼容性检查 │
│ □ 迁移是否与当前运行的应用代码兼容? │
│ □ 滚动更新期间新旧代码是否都能正常工作? │
│ □ 新增 NOT NULL 列是否有 DEFAULT 值? │
│ □ 数据回填是否分批执行? │
│ │
│ ✅ 迁移后验证 │
│ □ Schema 是否符合预期? │
│ □ 数据完整性是否正常? │
│ □ 应用功能是否正常? │
│ □ 查询性能是否正常? │
│ □ 监控指标是否正常? │
│ │
└─────────────────────────────────────────────────────────────────────┘4.3 一键审查 Prompt
以下 Prompt 可以一次性审查整个数据库 Schema 的所有常见问题:
你是一位资深数据库架构师。请对以下数据库 Schema 进行全面审查,
检查以下六大反模式:
## Schema
[粘贴完整的 DDL 或 ORM Schema]
## 审查维度
### 1. 过度范式化
- 是否有查询需要 JOIN 超过 3 张表?
- 是否有只有 2-3 列的查找表可以用 ENUM 替代?
- 读多写少的场景是否考虑了反范式化?
### 2. 缺少索引
- 所有外键是否有索引?
- WHERE 常用字段是否有索引?
- 是否有合适的复合索引?
### 3. 类型错误
- 金额字段是否使用了 FLOAT?
- 时间戳是否缺少时区?
- 主键类型是否合适?
### 4. 不安全迁移
- 是否有破坏性操作?
- 是否有回滚计划?
- 大表操作是否安全?
### 5. 缺少约束
- 关键字段是否 NOT NULL?
- 唯一标识是否 UNIQUE?
- 数值和状态字段是否有 CHECK?
### 6. 命名不规范
- 命名风格是否一致?
- 是否遵循 snake_case 规范?
- 是否使用了 SQL 保留字?
## 输出格式
按严重程度排序:
- 🔴 严重(必须修复):[问题] → [修复 SQL]
- 🟡 警告(建议修复):[问题] → [修复 SQL]
- 🟢 建议(可选改进):[问题] → [修复 SQL]
- 📊 总结:[问题数量统计和优先修复建议]实战案例:电商平台数据库设计审查与修复
案例背景
一个使用 AI 编码助手(Claude Code + Prisma)开发的电商平台,在上线 3 个月后遇到严重的性能和数据完整性问题。以下是 AI 最初生成的 Schema 和逐步修复的过程。
AI 生成的原始 Schema(问题版本)
// schema.prisma — AI 生成的原始版本(包含多个反模式)
model User {
id String @id @default(uuid())
email String // ❌ 缺少 @unique
userName String // ❌ camelCase,缺少 @unique
password String // ⚠️ 需确认是否哈希存储
phone Int? // ❌ 电话号码用 Int 会丢失前导零
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
addresses Address[]
reviews Review[]
}
model Product {
id String @id @default(uuid())
name String // ❌ 缺少长度限制
description String?
price Float // ❌ 金额用 Float!
comparePrice Float? // ❌ 金额用 Float!
sku String // ❌ 缺少 @unique
stock Int // ❌ 缺少 CHECK >= 0
category String // ❌ 应该关联 Category 表或用 ENUM
isAvailable Boolean @default(true)
orderItems OrderItem[]
reviews Review[]
}
model Order {
id String @id @default(uuid())
userId String // ❌ 缺少索引
status String @default("pending") // ❌ 缺少约束
totalAmount Float // ❌ 金额用 Float!
discount Float @default(0) // ❌ 金额用 Float,缺少范围约束
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
items OrderItem[]
// ❌ 缺少 updatedAt
}
model OrderItem {
id String @id @default(uuid())
orderId String // ❌ 缺少索引
productId String // ❌ 缺少索引
quantity Int // ❌ 缺少 CHECK > 0
unitPrice Float // ❌ 金额用 Float!
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
}
// ❌ 过度范式化的地址结构
model Country {
id String @id @default(uuid())
name String @unique
code String @unique
provinces Province[]
}
model Province {
id String @id @default(uuid())
name String
countryId String
country Country @relation(fields: [countryId], references: [id])
cities City[]
}
model City {
id String @id @default(uuid())
name String
provinceId String
province Province @relation(fields: [provinceId], references: [id])
addresses Address[]
}
model Address {
id String @id @default(uuid())
userId String
cityId String
street String
zipCode Int? // ❌ 邮编用 Int 会丢失前导零
isDefault Boolean @default(false)
user User @relation(fields: [userId], references: [id])
city City @relation(fields: [cityId], references: [id])
}
model Review {
id String @id @default(uuid())
userId String
productId String
rating Int // ❌ 缺少 CHECK (1-5)
comment String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
product Product @relation(fields: [productId], references: [id])
// ❌ 缺少 @@unique([userId, productId]) 防止重复评价
}问题诊断
使用一键审查 Prompt 分析后,发现以下问题:
| # | 反模式 | 严重程度 | 具体问题 |
|---|---|---|---|
| 1 | 类型错误 | 🔴 严重 | price, totalAmount, unitPrice 等 5 个金额字段使用 Float |
| 2 | 类型错误 | 🔴 严重 | phone 和 zipCode 使用 Int,丢失前导零 |
| 3 | 缺少约束 | 🔴 严重 | email 缺少 @unique,可能注册重复账号 |
| 4 | 缺少约束 | 🔴 严重 | sku 缺少 @unique,可能出现重复 SKU |
| 5 | 缺少约束 | 🟡 警告 | rating 缺少范围约束(1-5) |
| 6 | 缺少约束 | 🟡 警告 | quantity 和 stock 缺少 >= 0 约束 |
| 7 | 缺少约束 | 🟡 警告 | status 和 discount 缺少合法值约束 |
| 8 | 缺少索引 | 🟡 警告 | userId, orderId, productId 等外键缺少索引 |
| 9 | 过度范式化 | 🟡 警告 | 地址拆分为 4 张表,查询需要 3 个 JOIN |
| 10 | 命名不规范 | 🟢 建议 | userName 应为 user_name(@map) |
| 11 | 缺少字段 | 🟢 建议 | Order 缺少 updatedAt |
| 12 | 缺少约束 | 🟢 建议 | Review 缺少 @@unique([userId, productId]) |
修复后的 Schema
// schema.prisma — 修复后的版本
model User {
id String @id @default(uuid())
email String @unique // ✅ 添加 UNIQUE
username String @unique @map("username") // ✅ 修正命名 + UNIQUE
password String // 应用层确保 bcrypt 哈希
phone String? @map("phone") // ✅ 改为 String
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
addresses Address[]
reviews Review[]
@@map("users")
@@index([email])
}
model Product {
id String @id @default(uuid())
name String @db.VarChar(200) // ✅ 限制长度
description String? @db.Text
price Decimal @db.Decimal(10, 2) // ✅ 改为 Decimal
comparePrice Decimal? @db.Decimal(10, 2) @map("compare_price") // ✅ 改为 Decimal
sku String @unique @db.VarChar(50) // ✅ UNIQUE + 长度限制
stock Int @default(0) // 应用层 + 触发器确保 >= 0
category String @db.VarChar(50)
isAvailable Boolean @default(true) @map("is_available")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orderItems OrderItem[]
reviews Review[]
@@map("products")
@@index([category])
@@index([sku])
}
model Order {
id String @id @default(uuid())
userId String @map("user_id")
status String @default("pending") @db.VarChar(20) // 应用层 + CHECK 约束
totalAmount Decimal @db.Decimal(10, 2) @map("total_amount") // ✅ 改为 Decimal
discount Decimal @default(0) @db.Decimal(10, 2) // ✅ 改为 Decimal
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") // ✅ 添加 updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
items OrderItem[]
@@map("orders")
@@index([userId]) // ✅ 添加索引
@@index([status])
@@index([createdAt])
}
model OrderItem {
id String @id @default(uuid())
orderId String @map("order_id")
productId String @map("product_id")
quantity Int // 应用层 + CHECK > 0
unitPrice Decimal @db.Decimal(10, 2) @map("unit_price") // ✅ 改为 Decimal
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id])
@@map("order_items")
@@index([orderId]) // ✅ 添加索引
@@index([productId]) // ✅ 添加索引
}
// ✅ 简化地址结构(反范式化)
model Address {
id String @id @default(uuid())
userId String @map("user_id")
street String @db.VarChar(200)
city String @db.VarChar(100)
province String @db.VarChar(100)
country String @default("中国") @db.VarChar(100)
zipCode String? @db.VarChar(10) @map("zip_code") // ✅ 改为 String
isDefault Boolean @default(false) @map("is_default")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("addresses")
@@index([userId])
}
model Review {
id String @id @default(uuid())
userId String @map("user_id")
productId String @map("product_id")
rating Int // 应用层 + CHECK (1-5)
comment String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@map("reviews")
@@unique([userId, productId]) // ✅ 防止重复评价
@@index([productId])
@@index([userId])
}-- 补充的数据库级 CHECK 约束(Prisma 不直接支持,需手动迁移)
ALTER TABLE products ADD CONSTRAINT chk_products_price_positive
CHECK (price >= 0);
ALTER TABLE products ADD CONSTRAINT chk_products_stock_non_negative
CHECK (stock >= 0);
ALTER TABLE orders ADD CONSTRAINT chk_orders_total_positive
CHECK (total_amount >= 0);
ALTER TABLE orders ADD CONSTRAINT chk_orders_discount_range
CHECK (discount >= 0 AND discount <= total_amount);
ALTER TABLE orders ADD CONSTRAINT chk_orders_status_valid
CHECK (status IN ('pending', 'confirmed', 'paid', 'shipped', 'delivered', 'cancelled', 'refunded'));
ALTER TABLE order_items ADD CONSTRAINT chk_order_items_quantity_positive
CHECK (quantity > 0);
ALTER TABLE order_items ADD CONSTRAINT chk_order_items_price_positive
CHECK (unit_price >= 0);
ALTER TABLE reviews ADD CONSTRAINT chk_reviews_rating_range
CHECK (rating >= 1 AND rating <= 5);案例分析
关键决策点:
-
Float → Decimal 迁移:这是最紧急的修复。在 3 个月的运营中,Float 精度问题已导致约 ¥2,300 的累计差异。迁移时需要分批转换数据类型,并验证转换后的精度。
-
地址表反范式化:将 4 张表合并为 1 张表后,地址查询从 3 个 JOIN 变为 0 个 JOIN,P95 延迟从 120ms 降至 8ms。代价是地址数据存在冗余,但地址变更频率极低,冗余成本可接受。
-
添加 CHECK 约束:Prisma 不直接支持 CHECK 约束,需要通过手动迁移添加。这是”ORM 抽象遮蔽”问题的典型案例——ORM 的便利性不应成为放弃数据库级约束的理由。
-
索引策略:添加外键索引后,订单列表查询的 P95 延迟从 800ms 降至 15ms(数据量约 50 万行)。
修复效果:
| 指标 | 修复前 | 修复后 | 改善 |
|---|---|---|---|
| 订单列表 P95 延迟 | 800ms | 15ms | 98% ↓ |
| 地址查询 P95 延迟 | 120ms | 8ms | 93% ↓ |
| 金额精度误差 | ¥2,300/3 月 | ¥0 | 100% ↓ |
| 重复邮箱账号 | 23 个 | 0 | 100% ↓ |
| 无效评分(<1 或 >5) | 7 条 | 0 | 100% ↓ |
5. 反模式快速诊断决策树
当你审查 AI 生成的数据库代码时,可以按照以下决策树快速定位问题:
AI 生成了数据库 Schema
│
├─ 查询是否需要 JOIN 超过 3 张表?
│ ├─ 是 → 🔄 过度范式化(反模式 1)
│ │ → 考虑反范式化,合并低变更频率的关联表
│ └─ 否 → 继续检查
│
├─ 外键字段是否都有索引?
│ ├─ 否 → 🔍 缺少索引(反模式 2)
│ │ → 为所有外键和 WHERE 常用字段添加索引
│ └─ 是 → 继续检查
│
├─ 金额字段是否使用了 FLOAT/DOUBLE?
│ ├─ 是 → 📏 类型错误(反模式 3)
│ │ → 改为 NUMERIC/DECIMAL
│ └─ 否 → 检查其他类型(时间戳、电话、邮编)
│
├─ 迁移文件是否包含 DROP/RENAME?
│ ├─ 是 → 💥 不安全迁移(反模式 4)
│ │ → 使用安全的多步迁移策略
│ └─ 否 → 检查锁表风险和回滚计划
│
├─ 关键字段是否有 NOT NULL/UNIQUE/CHECK?
│ ├─ 否 → 🔓 缺少约束(反模式 5)
│ │ → 添加业务规则对应的约束
│ └─ 是 → 继续检查
│
└─ 命名风格是否一致(全部 snake_case)?
├─ 否 → 📝 命名不规范(反模式 6)
│ → 统一命名风格,使用 @map 映射
└─ 是 → ✅ Schema 基本合格6. 高级数据库 Prompt 技巧
6.1 分阶段 Schema 设计
对于复杂系统,不要一次性让 AI 生成完整 Schema。采用分阶段方式:
## 阶段 1:核心实体识别
请根据以下业务需求,识别核心实体和它们之间的关系。
只输出实体列表和关系图(Mermaid ER 图),不要生成 DDL。
[业务需求描述]
## 阶段 2:Schema 设计
基于上一步确认的实体和关系,生成完整的 DDL。
遵循以下规范:[粘贴 Steering 规则]
## 阶段 3:索引优化
基于以下查询模式,为 Schema 添加索引。
[列出常见查询]
## 阶段 4:安全审查
请对最终 Schema 进行安全审查,检查六大反模式。6.2 对比式 Prompt
让 AI 生成多个方案并对比,而非直接给出单一方案:
请为以下需求设计两种 Schema 方案:
- 方案 A:严格范式化(3NF)
- 方案 B:适度反范式化(优化读取性能)
对比两种方案的:
1. 查询复杂度(JOIN 数量)
2. 写入复杂度(数据一致性维护)
3. 存储空间
4. 适用场景
推荐方案并说明理由。6.3 约束驱动 Prompt
在 Prompt 中明确列出业务约束,强制 AI 生成约束:
请设计 Schema,必须包含以下业务约束(每个约束必须体现在 DDL 中):
1. 用户邮箱全局唯一
2. 订单金额必须 ≥ 0
3. 商品库存必须 ≥ 0
4. 评分范围 1-5
5. 每个用户对每个商品只能评价一次
6. 订单状态只能是:pending/confirmed/shipped/delivered/cancelled
7. 折扣率范围 0-100%
8. 结束日期必须晚于开始日期
对于每个约束,请在 DDL 中使用 CHECK、UNIQUE 或其他约束实现,
并在注释中标注对应的业务规则编号。6.4 迁移安全 Prompt
在请求迁移时,明确要求安全检查:
请为以下 Schema 变更生成迁移文件。
## 变更内容
[描述变更]
## 安全要求(必须满足)
1. 必须包含 up 和 down 迁移
2. 破坏性操作必须先备份数据
3. 大表操作必须使用 CONCURRENTLY
4. 数据回填必须分批执行
5. 迁移必须与当前运行代码兼容
6. 必须提供迁移前检查命令和迁移后验证命令
## 环境信息
- 数据库:[PostgreSQL / MySQL]
- 受影响表数据量:[行数]
- 部署方式:[滚动更新 / 蓝绿部署]避坑指南
❌ 常见错误
-
盲目信任 AI 生成的 Schema
- 问题:AI 生成的 Schema 看起来”能用”,但可能隐藏性能和安全问题
- 正确做法:每次 AI 生成 Schema 后,使用六大反模式检查清单逐项审查
-
只在应用层做验证,不在数据库层加约束
- 问题:应用层验证可以被绕过(直接 SQL、数据导入、API 漏洞),脏数据进入数据库
- 正确做法:数据库约束是最后一道防线,关键业务规则必须在数据库层强制执行
-
在没有 Steering 规则的情况下让 AI 设计数据库
- 问题:AI 会使用随机的命名风格、数据类型和约束策略
- 正确做法:在 CLAUDE.md / Kiro Steering / .cursorrules 中配置数据库规则后再开始设计
-
一次性让 AI 生成完整的复杂 Schema
- 问题:复杂 Schema 一次性生成容易出错,且难以审查
- 正确做法:分阶段设计(实体识别 → Schema 生成 → 索引优化 → 安全审查)
-
直接应用 AI 生成的迁移文件到生产环境
- 问题:AI 生成的迁移可能包含破坏性操作、锁表风险或不兼容变更
- 正确做法:使用
--create-only生成迁移后人工审查,在类生产环境测试后再应用
-
忽略 ORM 的类型映射问题
- 问题:ORM 的默认类型映射可能不是最优的(如 Prisma 的 Float 映射为 double precision)
- 正确做法:显式指定数据库类型(如
@db.Decimal(10, 2)),不依赖 ORM 默认映射
-
为每个字段都创建索引
- 问题:过多索引严重影响写入性能,增加存储开销
- 正确做法:只为实际查询需要的字段创建索引,每张表不超过 8 个索引
-
使用 AI 生成的 Schema 作为最终版本
- 问题:AI 生成的 Schema 是”初稿”,不是”终稿”
- 正确做法:AI 生成 → 人工审查 → 性能测试 → 安全审计 → 最终确认
✅ 最佳实践
- Steering 规则先行:在项目开始前,先配置数据库 Steering 规则,确保 AI 从第一行代码就遵循规范
- 分阶段设计:复杂 Schema 分阶段设计,每阶段人工审查确认后再进入下一阶段
- 约束驱动设计:在 Prompt 中明确列出业务约束,强制 AI 在 DDL 中实现
- 迁移安全第一:所有迁移文件必须人工审查,破坏性操作必须有备份和回滚计划
- 定期审查:每月使用六大反模式检查清单审查一次数据库 Schema
- 监控驱动优化:基于实际查询性能数据(pg_stat_statements、慢查询日志)优化索引,而非猜测
- 文档同步:Schema 变更时同步更新数据字典和 API-数据库映射文档
- 测试覆盖:关键约束和迁移必须有对应的集成测试验证
相关资源与延伸阅读
- Prisma Schema 参考文档 — Prisma Schema 语法完整参考,包含所有属性、类型和约束的详细说明
- Drizzle ORM Schema 声明指南 — Drizzle ORM 的 Schema 定义语法和最佳实践
- PostgreSQL CREATE INDEX CONCURRENTLY 文档 — PostgreSQL 官方文档,详解无锁索引创建的机制和限制
- Use The Index, Luke — SQL 索引设计经典教程 — 覆盖 B-Tree 原理、复合索引设计、部分索引等核心概念的免费在线教程
- Bytebase 数据库 Schema 变更管理 — 团队数据库迁移审查流程和最佳实践
- PostHog 安全 Django 迁移指南 — 零停机迁移的实战经验,适用于所有 ORM 框架
- pganalyze 索引建议文档 — AI 驱动的 PostgreSQL 索引分析和建议工具
- Prisma Optimize 查询优化指南 — Prisma 官方 AI 驱动查询分析工具的使用指南
- Database Naming Conventions — dev.to — 数据库命名规范的深度讨论和行业最佳实践
- 零停机数据库迁移设计 — 设计安全迁移策略的系统化方法(2025)
参考来源
- Prisma Optimize GA 发布公告 (2024-10)
- PlanetScale AI-Powered Postgres Index Suggestions (2025-05)
- Endor Labs: Design Flaws in AI Generated Code (2025-06)
- Bytebase: Top TypeScript ORM 2025 (2025-05)
- PostHog: Safe Django Migrations (2025)
- PostgresCompare: Destructive PostgreSQL Operations (2026-02)
- How to Design Database Migrations That Never Need Rollback (2025-06)
- 4 Best AI SQL Tools in 2026 (2026-06)
- Oracle: Prompt Engineering with SQLcl MCP Server (2025-03)
- Database Naming Standards (2023-02,经典参考)
- Leapcell: Building Resilient Databases with Reversible Migrations (2025-02)
- Use The Index, Luke (经典参考)
📖 返回 总览与导航 | 上一节:29d-ORM与查询优化 | 下一节:30a-AI辅助架构设计概览