问卷表与题目表应通过questionnaire_id外键关联,题目表中该字段非空并设ON DELETE CASCADE,同时添加索引;若需题目复用则引入中间表。

问卷表和题目表如何用外键关联才不翻车
问卷系统最常崩在「一对多」关系设计上。别把所有题目硬塞进一张表,也别让 questionnaire_id 在题目表里允许为 NULL——这会导致查问卷时漏题或关联失败。
正确做法是两张主表加一张中间表(如果支持题目复用);若题目只归属单个问卷,直接在题目表设 questionnaire_id 外键并加索引:
CREATE TABLE questionnaire ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, status TINYINT DEFAULT 1 -- 1=启用, 0=停用 );CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, questionnaire_id INT NOT NULL, content TEXT NOT NULL, type ENUM('single', 'multiple', 'text') NOT NULL, sort_order TINYINT DEFAULT 0, FOREIGN KEY (questionnaire_id) REFERENCES questionnaire(id) ON DELETE CASCADE );
-- 给外键字段加索引(InnoDB 外键自动建索引,但显式声明更稳妥) CREATE INDEX idx_question_qid ON question(questionnaire_id);
-
ON DELETE CASCADE能避免删问卷后残留“孤儿题目”,但上线前务必确认业务是否真允许级联删除 - 别用
TEXT存选项内容(如单选的 A/B/C),应拆到option表,否则改选项时要字符串替换,极易出错 -
sort_order用TINYINT足够(最多 255 题),比用INT节省空间且排序更快
用户答题记录怎么存才查得快又不锁表
答题记录是写多读少、但偶尔要按用户/问卷聚合统计的场景。别用一张大宽表存所有答案,更别把 JSON 字符串塞进一个字段里——后期想统计“第3题选A的人数”就得全表扫描+JSON 解析,慢且无法走索引。
推荐三张表分层存储:
CREATE TABLE answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, questionnaire_id INT NOT NULL, submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, ip VARCHAR(45), INDEX idx_user_qid (user_id, questionnaire_id), INDEX idx_qid_time (questionnaire_id, submitted_at) );CREATE TABLE answer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL, question_id INT NOT NULL, value TEXT, -- 单选存选项ID,多选存逗号分隔ID列表(简单场景),文本题存原文 FOREIGN KEY (sheet_id) REFERENCES answer_sheet(id) ON DELETE CASCADE );
CREATE INDEX idx_answer_qid ON answer(question_id);
-
answer_sheet记录一次提交的元信息,answer记录每道题的答案,一一对应——这样加新题型不用改表结构 -
value字段不用ENUM(扩展性差),也不建议直接存选项文字(翻译/修改成本高),优先存关联 ID(比如option_id),复杂逻辑放应用层 - 高频查询“某问卷提交人数”直接查
answer_sheet表,别 COUNT JOIN;要“各题选项分布”再 JOINanswer和option表
MySQL 8.0 的 JSON 函数能用在哪几个关键点
不是所有地方都适合上 JSON。仅在以下三个明确场景用 JSON 类型才真正提效:
DESTOON B2B网站管理系统是一套完善的B2B(电子商务)行业门户解决方案。系统基于PHP+MySQL开发,采用B/S架构,模板与程序分离,源码开放。模型化的开发思路,可扩展或删除任何功能;创新的缓存技术与数据库设计,可负载千万级别数据容量及访问。
- 问卷配置中的动态字段:比如每份问卷可自定义「提交后跳转链接」「关闭时间」,这些非通用字段用
JSON存在questionnaire.config字段里,比加一堆 nullable 列干净 - 用户补充信息:如「填写人所在部门」「职级」等非结构化字段,存在
answer_sheet.ext_info中,用JSON_CONTAINS()做简单筛选 - 导出原始数据时保留完整结构:后台导 Excel 需要原样输出选项文字和用户填写内容,用
JSON_OBJECT()+JSON_ARRAYAGG()拼装比应用层拼接更稳
但注意:JSON 字段无法被普通索引加速,要用生成列 + 函数索引才能高效查询。例如想按「部门」查提交记录:
ALTER TABLE answer_sheet ADD COLUMN dept VARCHAR(50) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(ext_info, '$.department'))) STORED,ADD INDEX idx_dept (dept);
并发提交时怎么防止重复记录或脏计数
用户手抖连点“提交”、或者前端没禁用按钮,会导致同一份答卷插入多次。靠应用层防重(如 UUID + 唯一索引)只是兜底,核心要在数据库层做约束。
- 在
answer_sheet上加联合唯一索引:UNIQUE KEY uk_user_qid (user_id, questionnaire_id, DATE(submitted_at)),限制同用户当天只能交一次(按需调整粒度) - 如果允许重填,但要保留历史版本,就把
submitted_at改成DATETIME(3)(毫秒级),再加UNIQUE KEY uk_user_qid_ts (user_id, questionnaire_id, submitted_at) - 统计类字段(如“已提交人数”)绝不要用
UPDATE ... SET count = count + 1,而应在事务中先SELECT ... FOR UPDATE锁住问卷行,再更新——否则高并发下会少计
实际部署时,answer_sheet 表容易成为热点,如果日提交量超 10 万,考虑按 questionnaire_id 分表,或迁移到 TiDB 这类支持自动分片的引擎。









