AI 辅助数据库 Schema 设计——从需求到 DDL 的工程化
AI 辅助数据库 Schema 设计——从需求到 DDL 的工程化
上个月我们在做一个新功能——用户可以创建自定义问卷,问卷支持多种题型(单选、多选、填空、评分),结果要能做统计分析。
按照以前的习惯,我会对着需求文档自己设计表结构,手写 DDL,然后和团队 review。这次我想试试:让 AI 参与这个设计过程,看看能省多少脑力。
结果有点超出预期——不是因为 AI 直接给出了完美的方案,而是因为 AI 在设计过程中提出了我没想到的问题:
"问卷结果的统计是实时的还是预计算的?如果是实时的,当用户量大时如何保证性能?答案表是按问卷分表还是全局一张表?如果题目类型不同,答案的存储格式是统一的还是分开的?"
这几个问题,如果在设计阶段没想清楚,后期改表结构的代价会非常高。AI 帮我提前踩了这些雷。
这篇文章就讲这件事:怎么让 AI 真正参与数据库设计,而不是只让它写 SQL。
让 AI 参与设计的正确姿势
大多数人用 AI 辅助数据库设计的方式是:把需求直接粘给 AI,让它给出建表语句。
这种方式有明显问题:AI 会给你一个"看起来合理"的 Schema,但它不了解你的业务规模、访问模式、团队规范,给出的方案往往只能作为参考,不能直接用。
更好的方式是对话式设计:把 AI 当做一个有经验的 DBA,和它一起讨论设计决策,让它提出问题、发现遗漏、给出多个方案并分析权衡。
这需要你的 Prompt 设计得好。
Schema 设计 Prompt 的工程化
阶段一:需求澄清
@Service
@Slf4j
public class DbSchemaDesignService {
private final ChatClient chatClient;
/**
* 阶段一:让 AI 从需求中澄清设计关键问题
*/
public SchemaDesignSession startDesignSession(String requirementsText) {
String systemPrompt = """
你是一名有 10 年以上经验的数据库架构师,擅长 MySQL/PostgreSQL 的 Schema 设计。
你的工作方式:
1. 首先理解业务需求,提出关键设计问题
2. 在得到回答后,逐步细化方案
3. 给出多个方案并分析各自的权衡(tradeoff)
4. 最终输出符合规范的 DDL
设计原则:
- 遵循第三范式(3NF),除非有明确性能原因需要反范式
- 所有表必须有主键(推荐自增 bigint 或 UUID)
- 外键关系必须有索引
- 时间字段统一使用 DATETIME,时区处理在应用层
- 所有字段必须有 COMMENT
- 软删除使用 deleted_at 字段(DATETIME,为 null 时表示未删除)
在第一步,请只提出你认为最重要的 5-8 个澄清问题,不要直接给出 Schema。
""";
String userPrompt = """
以下是我的功能需求,请帮我澄清关键的设计决策点:
%s
""".formatted(requirementsText);
String clarifyingQuestions = chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content();
log.info("Schema 设计澄清问题生成完成");
return SchemaDesignSession.builder()
.sessionId(UUID.randomUUID().toString())
.requirements(requirementsText)
.clarifyingQuestions(clarifyingQuestions)
.systemPrompt(systemPrompt)
.conversationHistory(new ArrayList<>())
.build();
}
/**
* 阶段二:根据回答,生成多方案对比
*/
public SchemaOptions generateSchemaOptions(SchemaDesignSession session,
String answersToQuestions) {
session.getConversationHistory().add(
new ConversationTurn("assistant", session.getClarifyingQuestions()));
session.getConversationHistory().add(
new ConversationTurn("user", "回答如下:\n" + answersToQuestions));
String requestMultipleOptions = """
基于以上回答,请给出 2-3 个不同的 Schema 设计方案。
每个方案需要包含:
1. 方案名称和核心思路(1-2 句话)
2. 核心表结构(只画 ER 图或描述,不需要完整 DDL)
3. 优点(2-3 条)
4. 缺点和局限(2-3 条)
5. 适用场景(什么情况下选这个方案)
不要急着给出完整 DDL,先让我选择方向。
""";
session.getConversationHistory().add(
new ConversationTurn("user", requestMultipleOptions));
String optionsResponse = callWithHistory(session);
session.getConversationHistory().add(
new ConversationTurn("assistant", optionsResponse));
return SchemaOptions.builder()
.sessionId(session.getSessionId())
.optionsDescription(optionsResponse)
.build();
}
/**
* 阶段三:基于选定方案,生成完整 DDL
*/
public String generateDdl(SchemaDesignSession session,
String selectedOption,
String additionalConstraints) {
String ddlRequest = """
我选择方案:%s
额外约束和规范:
%s
请生成完整的 DDL,要求:
1. 使用 MySQL 8.0 语法
2. 所有字段有 COMMENT
3. 包含必要的索引(主键、外键、查询频繁的字段)
4. 包含创建时间(created_at)和更新时间(updated_at)字段
5. 软删除字段(deleted_at DATETIME DEFAULT NULL)
6. 使用 InnoDB 存储引擎,UTF8MB4 字符集
7. 在 DDL 之后,列出还需要额外创建的索引(CREATE INDEX 语句)
""".formatted(selectedOption, additionalConstraints);
session.getConversationHistory().add(
new ConversationTurn("user", ddlRequest));
String ddl = callWithHistory(session);
session.getConversationHistory().add(
new ConversationTurn("assistant", ddl));
return ddl;
}
private String callWithHistory(SchemaDesignSession session) {
// 构建完整的对话历史
var promptBuilder = chatClient.prompt()
.system(session.getSystemPrompt());
// 添加对话历史
StringBuilder historyText = new StringBuilder();
for (ConversationTurn turn : session.getConversationHistory()) {
historyText.append(turn.role().toUpperCase()).append(":\n")
.append(turn.content()).append("\n\n");
}
if (!historyText.isEmpty()) {
return chatClient.prompt()
.system(session.getSystemPrompt() + "\n\n以下是对话历史:\n" + historyText)
.user(session.getConversationHistory().get(
session.getConversationHistory().size() - 1).content())
.call()
.content();
}
return chatClient.prompt()
.system(session.getSystemPrompt())
.user(session.getConversationHistory().get(0).content())
.call()
.content();
}
}实际演示:问卷系统的 Schema 设计
我把这个过程用真实的例子演示一遍。
需求描述:
用户可以创建自定义问卷。
问卷包含多个题目,题目类型支持:单选题、多选题、填空题、评分题(1-10分)。
用户提交答案后,可以查看统计数据:
- 每道题的各选项选择次数和占比
- 填空题的所有答案列表
- 评分题的平均分、分布
- 问卷的完成率(开始填写/提交完成)
预计每天最多 10 万份提交,支持 1000 份并发答卷。AI 的澄清问题(精简版):
1. 问卷是否支持版本控制?(修改题目后旧数据是否仍需可查)
2. 单选/多选的选项是固定的还是支持"其他"自定义输入?
3. 统计数据是实时计算还是定时预聚合?(影响答案表设计)
4. 答卷是否支持分批保存(先填几题,暂存,后来继续)?
5. 用户是否需要登录才能填写(有无匿名答卷场景)?
6. 问卷是否有截止日期、填写次数限制等管控功能?
7. 数据是否需要导出(Excel/CSV)?对这些问题的回答:
1. 支持版本控制,修改后需保留历史版本
2. 单选/多选支持"其他"选项,并填写文字
3. 统计数据可以接受 T+5min 的延迟,用定时预聚合
4. 支持暂存草稿
5. 需要登录,有用户 ID
6. 有截止日期和总填写次数限制
7. 需要导出AI 给出的方案对比:
方案 A:答案表按题型分表(单选答案表、多选答案表、填空表、评分表)
- 优点:查询单种题型统计数据时效率高,字段清晰
- 缺点:一次获取完整答卷需要 JOIN 多张表,导出复杂
方案 B:答案统一存储(JSON 字段存答案内容)
- 优点:表结构简单,扩展灵活
- 缺点:无法直接用 SQL 做统计,需要应用层处理,索引失效
方案 C:EAV(Entity-Attribute-Value)模式 + 预聚合统计表
- 优点:完整答卷一次查询,统计用预聚合表,查询性能好
- 缺点:EAV 模式本身有额外复杂度,数据完整性约束弱
经过讨论,选择方案 C 的变体:答案表用统一格式存储(但 answer_value 根据题型有不同语义),预聚合统计表分离。
最终生成的 DDL(节选):
-- 问卷表
CREATE TABLE questionnaire (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '问卷ID',
creator_id BIGINT UNSIGNED NOT NULL COMMENT '创建者用户ID',
title VARCHAR(200) NOT NULL COMMENT '问卷标题',
description TEXT COMMENT '问卷说明',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1=草稿, 2=发布, 3=关闭',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
max_responses INT UNSIGNED COMMENT '最大回答数量限制(NULL表示不限)',
expires_at DATETIME COMMENT '截止时间(NULL表示不限)',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
INDEX idx_creator_id (creator_id),
INDEX idx_status (status),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷表';
-- 问题表
CREATE TABLE question (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '问题ID',
questionnaire_id BIGINT UNSIGNED NOT NULL COMMENT '所属问卷ID',
questionnaire_version INT NOT NULL COMMENT '关联的问卷版本',
sort_order SMALLINT NOT NULL DEFAULT 0 COMMENT '显示顺序',
question_type TINYINT NOT NULL COMMENT '题型: 1=单选, 2=多选, 3=填空, 4=评分',
title VARCHAR(500) NOT NULL COMMENT '题目内容',
is_required TINYINT NOT NULL DEFAULT 1 COMMENT '是否必答: 1=是, 0=否',
config JSON COMMENT '题目配置(单选/多选的选项,评分的范围等)',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
INDEX idx_questionnaire_id (questionnaire_id),
INDEX idx_questionnaire_version (questionnaire_id, questionnaire_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题表';
-- 答卷表(用户提交的一份完整回答)
CREATE TABLE response (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '答卷ID',
questionnaire_id BIGINT UNSIGNED NOT NULL COMMENT '问卷ID',
questionnaire_version INT NOT NULL COMMENT '回答时的问卷版本',
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1=草稿, 2=已提交',
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始填写时间',
submitted_at DATETIME COMMENT '提交时间',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_user_questionnaire (user_id, questionnaire_id),
INDEX idx_questionnaire_id (questionnaire_id),
INDEX idx_status (questionnaire_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答卷表';
-- 答案表(每道题的具体回答)
CREATE TABLE answer (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '答案ID',
response_id BIGINT UNSIGNED NOT NULL COMMENT '答卷ID',
question_id BIGINT UNSIGNED NOT NULL COMMENT '问题ID',
-- 对于单选/评分:answer_value 存单个值(选项ID或分数字符串)
-- 对于多选:answer_value 存 JSON 数组(选项ID列表)
-- 对于填空:answer_value 存文字内容
answer_value TEXT COMMENT '答案内容(含义根据题型不同)',
other_text VARCHAR(500) COMMENT '选择"其他"时填写的文字',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_response_question (response_id, question_id),
INDEX idx_response_id (response_id),
INDEX idx_question_id (question_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答案表';
-- 统计预聚合表(定时任务更新,T+5min)
CREATE TABLE question_statistics (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '统计ID',
question_id BIGINT UNSIGNED NOT NULL COMMENT '问题ID',
questionnaire_id BIGINT UNSIGNED NOT NULL COMMENT '问卷ID(冗余,方便查询)',
-- 对于单选/多选:statistics_data 存各选项的 count
-- 对于评分:存 avg, count, distribution
-- 对于填空:只存 count(内容太多不聚合)
statistics_data JSON NOT NULL COMMENT '统计数据(结构因题型而异)',
response_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '参与统计的答卷数',
calculated_at DATETIME NOT NULL COMMENT '最后计算时间',
UNIQUE KEY uk_question_id (question_id),
INDEX idx_questionnaire_id (questionnaire_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目统计预聚合表';AI Schema 设计的常见失误和校验方法
失误一:忘记软删除索引
AI 经常忘记在查询条件里加上 deleted_at IS NULL 对应的索引覆盖。比如这个查询:
SELECT * FROM questionnaire WHERE creator_id = ? AND deleted_at IS NULL;AI 给出的索引可能只有 idx_creator_id (creator_id),而没有把 deleted_at 包含进来。对于有大量软删除数据的表,这会有明显的性能问题。
校验方法:让 AI 生成 DDL 之后,把你的主要查询模式列出来,让它验证每个查询都有对应的索引:
public String validateIndexCoverage(String ddl, List<String> queryPatterns) {
String validationPrompt = """
以下是数据库 DDL:
%s
以下是主要的查询模式:
%s
请验证:
1. 每个查询模式是否都有适合的索引覆盖?
2. 有哪些查询会导致全表扫描?
3. 建议补充哪些索引?
对每个查询给出 EXPLAIN 的预期结果(type 字段)。
""".formatted(ddl, String.join("\n", queryPatterns));
return chatClient.prompt()
.system("你是一名 MySQL 查询优化专家。")
.user(validationPrompt)
.call()
.content();
}失误二:字段长度不合理
AI 不了解你的业务规模,可能会给所有 VARCHAR 字段设 255,或者给金额字段用浮点数而不是 DECIMAL。
校验清单(让 AI 检查):
public String auditFieldTypes(String ddl) {
String auditPrompt = """
请对以下 DDL 做字段类型审计,重点检查:
1. 金额字段:是否使用 DECIMAL(M,N) 而非 FLOAT/DOUBLE?
2. 状态枚举字段:VARCHAR 还是 TINYINT?
3. VARCHAR 长度:是否合理(避免全部 255)?
4. JSON 字段:是否在应该用 JSON 类型的地方用了 TEXT?
5. 时间字段:是否使用了 TIMESTAMP(存在 2038 问题),建议改为 DATETIME?
6. 大文本字段:是否考虑了分库/分表或对象存储?
DDL:
%s
请列出所有发现的问题和建议修改方案。
""".formatted(ddl);
return chatClient.prompt()
.system("你是一名有丰富经验的数据库设计审查专家。")
.user(auditPrompt)
.call()
.content();
}失误三:没有考虑数据增长
AI 设计 Schema 时通常不会主动考虑数据量增长,但数据库设计一旦上线,修改成本极高。
public String analyzeScalability(String ddl, BusinessScale scale) {
String scalabilityPrompt = """
基于以下业务规模,分析这个 Schema 的可扩展性:
业务规模预估:
- 日新增数据:%s
- 峰值 QPS:%s
- 预计 1 年后数据量:%s
DDL:
%s
请分析:
1. 哪些表会成为性能瓶颈?
2. 是否需要考虑分区表(Partition)?
3. 哪些表可能需要分库分表,按什么字段分?
4. 建议添加哪些归档策略(定期清理/归档旧数据)?
5. 是否有设计上的隐患,随着数据增长会暴露的?
""".formatted(
scale.getDailyNewRecords(),
scale.getPeakQps(),
scale.getEstimated1YearRecords(),
ddl);
return chatClient.prompt()
.system("你是一名专注于大规模数据库设计的架构师。")
.user(scalabilityPrompt)
.call()
.content();
}团队规范的自动化检查
每个团队都有自己的 DDL 规范(命名规范、必备字段等),可以让 AI 按规范做自动检查:
@Component
public class DdlConventionChecker {
private final ChatClient chatClient;
// 团队规范描述
private static final String TEAM_CONVENTIONS = """
命名规范:
- 表名:小写字母 + 下划线,单数形式(questionnaire 而非 questionnaires)
- 字段名:小写字母 + 下划线
- 索引名:idx_字段名(单字段),idx_字段1_字段2(联合索引)
- 唯一索引名:uk_字段名
必备字段:
- 所有表必须有 id(BIGINT UNSIGNED AUTO_INCREMENT)
- 所有表必须有 created_at(DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)
- 所有表必须有 updated_at(DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)
- 需要软删除的表加 deleted_at(DATETIME DEFAULT NULL)
禁止项:
- 禁止在代码里硬删除(DELETE 语句),必须使用 deleted_at
- 禁止使用 FLOAT/DOUBLE 存储金额,必须用 DECIMAL
- 禁止 VARCHAR(255) 作为默认长度,必须根据业务合理设置
- 外键字段必须建立索引
""";
public ConventionCheckResult check(String ddl) {
String checkPrompt = """
请按照以下团队规范检查 DDL,列出所有不符合规范的地方:
团队规范:
%s
待检查 DDL:
%s
请以 JSON 格式输出检查结果:
{
"violations": [
{
"type": "naming/required_field/forbidden",
"table": "表名",
"field": "字段名(如适用)",
"description": "违规描述",
"suggestion": "建议修改方案"
}
],
"passed": true/false,
"summary": "总结"
}
""".formatted(TEAM_CONVENTIONS, ddl);
String response = chatClient.prompt()
.system("你是一名严格的代码规范检查工具。")
.user(checkPrompt)
.call()
.content();
try {
return new ObjectMapper().readValue(response, ConventionCheckResult.class);
} catch (Exception e) {
log.error("规范检查结果解析失败", e);
return ConventionCheckResult.parseError(response);
}
}
}总结
AI 参与数据库设计的价值有两个层面:
第一层:节省时间。写 DDL、生成注释、格式化代码,这些机械性工作 AI 比人快得多。
第二层:提升质量。更重要的是,AI 能提出"你没想到的问题"——版本控制怎么做、数据量增长怎么处理、统计是实时还是预聚合……这些是设计阶段的决策点,一旦上线后改起来很痛。
但要达到第二层,你必须用对 Prompt——不是让 AI"直接给方案",而是让它"和你一起讨论"。
把 AI 当 DBA 顾问,不是当 DDL 生成器。
