小表可用 order by rand() 抽题;大表因全表扫描+排序性能差,易翻车,应改用主键范围随机或应用层随机。

怎么用 ORDER BY RAND() 抽题不翻车
直接说结论:ORDER BY RAND() 在小表(比如题库 question 表。
常见错误现象:SELECT * FROM question ORDER BY RAND() LIMIT 20 在并发 50+ 考生同时组卷时,SHOW PROCESSLIST 里一堆 Copying to tmp table,CPU 瞬间拉满。
- 真正要的是「均匀随机」,不是「全表排序后截断」——可以改用主键范围采样
- 如果题库有逻辑删除(
status = 1),必须把WHERE status = 1放在ORDER BY前,否则RAND()仍会对所有行(含已删)计算 - MySQL 8.0+ 可考虑
TABLESAMPLE SYSTEM (n),但注意它不保证精确行数,且对WHERE条件支持弱,慎用于精确组卷
按知识点/难度分层抽题的 SQL 怎么写才可控
真实考试不能纯随机:比如「Java 基础占 40%,中等难度占 60%」。硬塞一个 ORDER BY RAND() 无法满足分布要求,得拆成多段查询再合并。
使用场景:试卷结构固定(如单选 30 题:其中「集合类」8 题、「异常处理」5 题、「IO」4 题……),且每类题目需独立控制难度比例。
- 用
UNION ALL拼接各分类子查询,每个子查询加WHERE category = 'xxx' AND difficulty IN (1,2)+ORDER BY RAND() LIMIT N - 务必给每个子查询加
LIMIT,否则某类题少于预期数量时,UNION ALL会补空——MySQL 不会自动重试或告警 - 避免在
UNION ALL外再套一层ORDER BY RAND()混淆题序,打乱分类结构;真要打乱整体顺序,用应用层 shuffle 更稳
SELECT * FROM ( (SELECT * FROM question WHERE category = 'collection' AND difficulty IN (1,2) ORDER BY RAND() LIMIT 8) UNION ALL (SELECT * FROM question WHERE category = 'exception' AND difficulty IN (1,2) ORDER BY RAND() LIMIT 5) ) AS paper;
怎么防止同一套卷被重复生成(尤其高并发下)
多个考生几乎同时请求组卷,若没做隔离,可能拿到完全相同的题目组合——这不是 bug,是 RAND() 在同一事务内种子相同 + 查询无锁导致的正常现象。
性能影响:加行锁或唯一约束会拖慢吞吐,但比发错卷强得多。
- 最简方案:组卷前先插入一条带唯一标识的记录到
exam_paper_template表(如paper_id = UUID()),用INSERT ... SELECT ... ON DUPLICATE KEY UPDATE防重 - 关键点:唯一约束必须包含能体现「卷结构」的字段,比如
(category, difficulty, count)组合,而不是只靠paper_id - 别依赖
SELECT FOR UPDATE锁整张题库表——锁粒度太大,并发一上来就排队
为什么不能把随机逻辑全丢给 MySQL,而要留一部分给应用层
因为 MySQL 的随机能力太“笨”:它没法感知业务规则变化(比如临时加一条“禁止同一题在 24 小时内重复出现”),也没法复用缓存或预热结果。
容易被忽略的地方:题干里带图片或公式时,SQL 层无法判断资源加载耗时;而应用层可提前批量查出题干 ID,再异步加载富文本内容,避免组卷接口卡在 IO 上。
- 建议分工:MySQL 只负责「从可用题池中按条件捞出候选 ID 列表」,随机排序、去重、防重逻辑交给 Go/Python 等语言处理
- 题库量大时,用 Redis 缓存热门标签(如
category:collection:difficulty:2)对应的 ID 集合,减少 DB 压力 - 别让组卷 SQL 承担「生成试卷 PDF」或「计算总分」这类事——那是下游服务的事,混在一起会让问题定位变模糊










