在搭建一个轻量的问答类应用时,MySQL 是非常合适的存储选择。它成熟稳定,支持事务,也能通过简单的表结构表达题目、选项和作答关系。下面我们从实际场景出发,一步步设计数据库。

一、核心数据实体分析
一个简单的问答系统通常包含三类核心数据:题目、答案以及用户的作答记录。题目描述提问内容,答案关联题目并标记是否正确,作答记录则保存某个用户在某次练习中对某题的选择。如果不加区分地混在一张表里,查询某用户正确率或随机抽题都会变得很慢。
我们把实体拆分为三张表:question 表存题干,answer 表存选项,user_answer 表存答题流水。这样的结构既符合范式,也方便单独建索引。例如题目表按分类检索,答案表按题目 ID 聚簇,作答表按用户 ID 分片查询。
1.1 题目表结构设计
question 表至少需要 id、title、category 和 created_at 字段。id 用自增主键,title 保存题干文本,category 用于分类筛选,created_at 记录录入时间。示例如下:
CREATE TABLE question ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL COMMENT '题干', category VARCHAR(50) NOT NULL DEFAULT 'default' COMMENT '分类', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
上面使用 utf8mb4 字符集,能存表情和特殊符号。给 category 加索引可加速按分类抽题:
ALTER TABLE question ADD INDEX idx_category (category);
1.2 答案表与作答表
answer 表通过 question_id 关联题目,is_correct 标记正确项。user_answer 表记录 uid、question_id、answer_id 和是否答对。建表语句如下:
CREATE TABLE answer ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, question_id INT UNSIGNED NOT NULL, content VARCHAR(255) NOT NULL COMMENT '选项内容', is_correct TINYINT(1) NOT NULL DEFAULT 0, INDEX idx_qid (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE user_answer ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, uid INT UNSIGNED NOT NULL, question_id INT UNSIGNED NOT NULL, answer_id INT UNSIGNED NOT NULL, is_right TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_uid (uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这种拆分让新增题目只写 question 和 answer,用户答题只写 user_answer,互不阻塞。统计正确率时直接对 user_answer 做聚合即可。
二、基础数据操作示例
有了表结构,就可以进行常规的增删查改。下面演示插入一道单选题并让用户作答的事务写法。
2.1 插入题目与答案
在一个事务里插入题目和多个选项,保证原子性。先插题目拿到 ID,再插答案:
START TRANSACTION;
INSERT INTO question (title, category) VALUES ('MySQL 默认端口是多少?', 'db');
SET @qid = LAST_INSERT_ID();
INSERT INTO answer (question_id, content, is_correct) VALUES
(@qid, '3306', 1),
(@qid, '8080', 0),
(@qid, '443', 0);
COMMIT;
使用 LAST_INSERT_ID() 获取刚插入的题目主键,避免手动查表。事务确保题目和选项要么都成功,要么都回滚。
2.2 用户答题与统计
用户作答时写入 user_answer,并标记对错。之后可用聚合查询看正确率:
INSERT INTO user_answer (uid, question_id, answer_id, is_right) VALUES (1001, 1, 2, 0); SELECT COUNT(*) AS total, SUM(is_right) AS correct, ROUND(SUM(is_right)/COUNT(*), 2) AS rate FROM user_answer WHERE uid = 1001;
该查询返回某用户总答题数、正确数和正确率。若数据量大,可给 uid 和 created_at 建联合索引提升范围统计速度。
三、随机抽题与性能注意点
问答系统常需要随机出题。直接用 ORDER BY RAND() 在百万数据上很慢,可改用偏移量随机法。
3.1 高效随机抽题
先取题目总数,应用层算随机偏移,再限量查询:
SELECT COUNT(*) INTO @cnt FROM question; SET @offset = FLOOR(RAND() * @cnt); PREPARE stmt FROM 'SELECT id,title FROM question LIMIT ?,1'; EXECUTE stmt USING @offset;
这种方式避免全表排序,只需一次计数和一次索引定位。如果按分类抽题,把 COUNT 和查询都加上 category 条件即可。
3.2 其他优化建议
给 question 的 category、user_answer 的 uid 建索引是基本操作。若回答选项固定,answer 表可缓存到应用内存,减少联表。定期归档老答题记录到历史表,能让主表保持轻量。
整体来看,用 MySQL 开发简单问答系统重点在于清晰拆表、合理使用事务与索引。按上述结构落地,足以支撑日活几千的练习型产品。