简介:面向在线K12教育从业者与题库开发者的中小学题库MySQL资料包,聚焦试题存储、公式录入与前端显示等常见难题,适合用于章节体系、知识点标注和题目属性设计的参考。压缩包共583个文件,约2.03MB,以574张PNG图片素材为主,另含2份docx说明文档、2个HTML示例页、2个JavaScript脚本、1个SQL数据库文件及txt、gif等辅助文件,可配合MathJax与LaTeX展示公式并快速了解题库结构。内容围绕MySQL题库表结构、样本题库和公式显示方案展开,文档给出数据结构图与题库说明,示例页和脚本演示公式渲染流程,便于读者参照搭建题库、设置题目属性、规划章节与知识点。已有1109人学习下载,适合希望少走公式排版与数据库建模弯路的在线教育开发者、教研人员及题库产品团队参考。
1. 先别急着 source:这份 1700 万题库压缩包到底解决什么问题
拿到中小学题库mysql.zip后直接source导入,十有八九会先撞上公式乱码和查询超时。这份免费 K12 题库落库包的核心价值不在“1700 万题库”这个数字,而在题干、选项、答案里保留了 LaTeX 公式显示所需的原始标记;数学题库和物理题库占比高,公式密度大,字段设计和导入方式一旦偷懒,后面检索和渲染都会翻车。
适合做题库中台、组卷系统、刷题应用的开发者,也适合需要本地跑通 K12 题库原型的团队。先把它当成一个需要体检的数据源,而不是一个双击就完事的压缩包。
2. 拆表结构:题干、选项、答案和公式字段怎么分
2.1 为什么一张大表撑不住数学题库和物理题库
1700 万题库如果塞进一张宽表,最容易出问题的地方不是磁盘,而是行长度和更新代价。数学题库的题干经常带分式、根式、矩阵,物理题库的题干带单位、矢量、上下标,单条题干几百到几千字符很常见;再把 A/B/C/D 选项、标准答案、解析、知识点全塞在同一行,表会变得又宽又碎。组卷时只查题干和学科,更新时只改解析,都会拖着整行跑。
我一般会拆成四张核心表:questions存题目主体,question_options存选项,question_answers存答案和解析,question_knowledge存题目与知识点的关联。拆表的判断标准不是“范式越高越好”,而是查询路径:组卷按学科、题型、难度筛题目,走主表;回显选项按question_id查选项表;批改和解析按question_id查答案表;知识点统计走关联表。这样每张表的行宽可控,索引也更短。
公式不单独拆一张表。LaTeX 公式和题干、选项的上下文强绑定,拆出去之后每次渲染都要多做一次连接,收益很低。更实用的做法是在主表保留原始题干stem,同时加一个去公式后的检索字段stem_plain。原始字段负责显示,检索字段负责命中,两个字段各干一件事,后面第 4 章会用到。
提示:先在小库跑通再全量,任何“先导入再说”的操作都会把排查成本放大。
2.2 LaTeX 公式字段的边界:行内、块级与转义
LaTeX 公式显示最怕两种脏数据:一种是行内公式和块级公式混用,$x^2$、$$x^2$$、\(x^2\)、\[x^2\]全都有;另一种是反斜杠被多吞了一层,\frac变成rac,或者\\frac原样留在库里。导入前要先定边界:行内统一成$...$,块级统一成$$...$$,命令只保留单反斜杠。这个规则听起来简单,但 SQL 文件里的反斜杠本身还会被 MySQL 客户端解释一次,所以“文件里看起来是一个反斜杠”和“库里存的是一个反斜杠”不是一回事。
字段类型用MEDIUMTEXT,不要用TEXT赌题干长度。MEDIUMTEXT的上限足够覆盖长题干和长解析,代价是行外存储,但这个代价比截断公式低得多。字符集统一utf8mb4,排序规则用兼容性更好的utf8mb4_unicode_ci;数学符号里的≤、≥、α、β和中文混排时,字符集不一致会直接表现为乱码,不是前端字体问题。
stem_format这个枚举字段值得留:text表示纯文本,latex表示整题以公式为主,mixed表示图文公式混排。它不参与复杂计算,但能让前端决定用哪条渲染路径,也能让清洗脚本快速圈出公式密度高的数学题库和物理题库。
2.3 最小可用建表 SQL 与字段参数
下面这套表结构不是唯一解,但适合先跑通 1700 万题库的导入、检索和回显。主表把题干、检索文本、格式、难度和去重哈希放在一起,选项和答案各走一张表,知识点用轻量关联表。
CREATE DATABASE k12_bank DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE k12_bank; CREATE TABLE questions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '题目主键', subject_id SMALLINT UNSIGNED NOT NULL COMMENT '学科:1 数学,2 物理,按资源映射', question_type TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1 单选 2 多选 3 填空 4 解答', stem MEDIUMTEXT NOT NULL COMMENT '原始题干,保留 LaTeX', stem_plain MEDIUMTEXT NULL COMMENT '去公式后的检索文本', stem_format ENUM('text','latex','mixed') NOT NULL DEFAULT 'mixed' COMMENT '渲染策略', difficulty TINYINT UNSIGNED NOT NULL DEFAULT 3 COMMENT '1-5', source_hash CHAR(64) NOT NULL COMMENT 'SHA-256 去重', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_source_hash (source_hash), KEY idx_subject_type_diff (subject_id, question_type, difficulty) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE question_options ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, question_id BIGINT UNSIGNED NOT NULL, option_label CHAR(1) NOT NULL COMMENT 'A/B/C/D', content TEXT NOT NULL COMMENT '选项内容,保留 LaTeX', is_correct TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_question_label (question_id, option_label), KEY idx_question (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE question_answers ( question_id BIGINT UNSIGNED NOT NULL, answer_text MEDIUMTEXT NOT NULL, analysis MEDIUMTEXT NULL, answer_format ENUM('text','latex','mixed') NOT NULL DEFAULT 'mixed', PRIMARY KEY (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE question_knowledge ( question_id BIGINT UNSIGNED NOT NULL, knowledge_id INT UNSIGNED NOT NULL, PRIMARY KEY (question_id, knowledge_id), KEY idx_knowledge (knowledge_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;主键用BIGINT UNSIGNED,因为 1700 万题库还会继续长,INT在几年后可能不够用,换主键类型的代价比一开始就用大整型高得多。subject_id和question_type用SMALLINT、TINYINT,这两个字段基数小,索引短,组卷查询走idx_subject_type_diff时扫描量更可控。source_hash存 SHA-256 的十六进制串,固定CHAR(64),不要用变长字符串;唯一索引会拦住重复题,但导入时也可能因为重复题中断,这个坑第 5 章会讲。
stem_plain先允许为空,导入完成后再用脚本回填,不要在主表插入时同步计算正则。1700 万行里同步跑复杂正则,导入时间会被拖长,失败重跑的成本也高。question_options上用(question_id, option_label)唯一键,防止同一道题出现两个 A 选项;question_answers用question_id做主键,保证一题一答。外键先不加,全量导入时外键检查会带来顺序依赖和额外锁,应用层保证关联关系更稳。
3. 导入与清洗:1700 万题库分批落库的四个动作
3.1 导入前的字符集体检
导入前先看服务端、库、表、连接四层字符集,不要只看SHOW VARIABLES里的一个值。常见翻车是库是utf8mb4,表也建对了,但客户端连接用了默认字符集,SQL 文件里的数学符号在传输阶段就坏了。命令行导入时显式加--default-character-set=utf8mb4,不要依赖客户端配置。
mysql --default-character-set=utf8mb4 -u root -p -e " SELECT @@character_set_server AS server_charset, @@collation_server AS server_collation, @@sql_mode AS sql_mode; SHOW VARIABLES LIKE 'ngram_token_size'; "这段检查的逻辑很直接:character_set_server决定新建库表的默认字符集,sql_mode里如果开了NO_BACKSLASH_ESCAPES,SQL 文件里的反斜杠处理规则会变,公式清洗脚本要跟着调整;ngram_token_size决定后面全文索引的最小切词长度,默认值不是每个环境都一样,建索引前必须确认。参数上,--default-character-set=utf8mb4只影响当前连接,不会改服务端配置,适合导入前临时对齐。
再看库和表的实际字符集,避免“建库时对了、建表时继承了旧默认值”:
SELECT @@character_set_database, @@collation_database; SHOW CREATE TABLE questions;SHOW CREATE TABLE的输出里重点看DEFAULT CHARSET和COLLATE,如果题干字段不是utf8mb4,不要先导数据再改表,先重建表或者ALTER到正确字符集,再导入。数学题库里的希腊字母、物理题库里的单位符号都吃字符集,乱码之后靠前端修是修不回来的。
注意:导入前备份一份空库结构,清洗脚本跑错时还能回滚结构,不要只备份数据。
3.2 大 SQL 分批与断点续跑
大 SQL 最怕“一个文件一个事务”。单文件导入到一半失败,回滚段膨胀,锁等待上来,重跑又从第一行开始。先判断文件能不能按行切:统计INSERT行数和分号数量,如果一条INSERT跨多行,直接split会把语句切断。
# 仅用于判断文件形态,不要直接拿结果当切分依据 grep -c '^INSERT INTO' questions.sql grep -c ';' questions.sql两个数接近,说明大概率一行一条语句,可以按行切;差距很大,说明存在多行扩展插入,按行切会制造语法错误。确认能切之后再分批,每批导入完记录文件名,失败时从日志里跳过已完成批次。
set -euo pipefail DB=k12_bank touch done.log for f in part_*.sql; do if grep -qxF "$f" done.log; then echo "跳过已完成: $f" continue fi echo "开始导入: $f" mysql --default-character-set=utf8mb4 -u root -p"$MYSQL_PWD" "$DB" < "$f" echo "$f" >> done.log doneset -euo pipefail让脚本在任一步失败时停下,避免“错了还继续导下一批”。done.log是断点续跑的后悔药,导入失败后修完当前批次,重跑脚本会自动跳过前面的文件。MYSQL_PWD从环境变量取,不要把密码写进脚本;--default-character-set=utf8mb4每一批都要带,不能只在第一批带。
如果资源包给的是单个大文件,又确认不能按行切,常见做法是用流式解析器按INSERT语句边界切,或者让上游按表导出。不要用sed按分号硬切,公式里出现分号、字符串里出现分号都会把语句切坏。导入过程中盯SHOW PROCESSLIST和错误日志,看到Lock wait timeout先停,不要继续加批次。
3.3 清洗 LaTeX 反斜杠与空答案
清洗脚本先做 dry-run,输出变更前后的 diff,抽样看数学题库和物理题库各 100 条,再决定是否全量写库。下面这个函数只处理三类问题:换行统一、命令前多一层反斜杠、行内/块级边界统一。它不负责修复杂语法,复杂公式不要指望正则全修。
import re def normalize_latex(s: str) -> str: if s is None: return "" s = s.replace("\r\n", "\n").replace("\r", "\n") # 把误写成双反斜杠的命令还原成单反斜杠,例如 \\frac -> \frac s = re.sub(r"\\\\([a-zA-Z]+)", r"\\\1", s) # 行内公式边界统一为 $...$,块级统一为 $$...$$ s = re.sub(r"\\\[(.*?)\\\]", r"$$\1$$", s, flags=re.S) s = re.sub(r"\\\((.*?)\\\)", r"$\1$", s, flags=re.S) return s.strip()re.sub(r"\\\\([a-zA-Z]+)", r"\\\1", s)匹配“两个反斜杠加字母”的命令前缀,替换成“一个反斜杠加字母”。这个规则对\\frac有效,但矩阵里的换行\\后面如果紧跟字母,也可能被误伤,所以必须抽样。re.S让.匹配换行,块级公式跨行时才能替换;如果题干里有大量跨行矩阵,先跑 dry-run 看 diff,不要直接全量。
空答案统计用一条 SQL 就够,先知道脏数据比例再决定补不补:
SELECT COUNT(*) AS total, SUM(answer_text IS NULL OR answer_text = '') AS empty_answer, SUM(analysis IS NULL OR analysis = '') AS empty_analysis FROM question_answers;empty_answer占比高时,不要急着用“略”填充,先按学科和题型分组看,数学题库解答题缺答案和物理题库选择题缺答案是两种问题。清洗脚本写库时按主键分批更新,每批 1000 到 5000 行提交一次,别开一个大事务把所有更新包进去。
4. LaTeX 公式检索与显示:从 MySQL 到前端的完整链路
4.1 公式在 MySQL 里怎么存才不翻车
原始题干存stem,检索文本存stem_plain,这两个字段不要混。stem保留$...$和$$...$$,前端按公式片段渲染;stem_plain去掉公式标记和命令,只留中文、数字和普通符号,给全文索引用。把全文索引直接建在stem上,公式里的反斜杠、花括号、上下标会参与切词,查“二次函数”时可能被公式噪声干扰。
去公式脚本不要改原始字段,只生成检索字段。下面这段逻辑先删块级公式,再删行内公式,最后删残留的 LaTeX 命令,连续空白压成一个空格。
import re def strip_latex(s: str) -> str: if s is None: return "" s = re.sub(r"\$\$.*?\$\$", " ", s, flags=re.S) s = re.sub(r"\$.*?\$", " ", s, flags=re.S) s = re.sub(r"\\[a-zA-Z]+\*?(?:\[[^\]]*\])?(?:\{[^{}]*\})?", " ", s) return re.sub(r"\s+", " ", s).strip()strip_latex只用于stem_plain,不覆盖stem。删命令的正则只处理一层花括号参数,嵌套很深的公式会留下残余,但检索字段不需要百分百干净,能去掉大部分噪声就够。回填时按id分批更新,先处理数学题库和物理题库,再处理其他学科,失败时能快速定位是哪一批。
4.2 用 ngram 全文索引做题干检索
中文题干检索用 ngram 全文索引比LIKE '%关键词%'靠谱,但前提是索引列正确、切词长度匹配。建索引前确认ngram_token_size,再在stem_plain上建全文索引。
SHOW VARIABLES LIKE 'ngram_token_size'; ALTER TABLE questions ADD FULLTEXT KEY ft_stem_plain (stem_plain) WITH PARSER ngram;ngram_token_size是切词长度,查询词长度小于它时可能查不到;这个参数改了要重建索引,不要只改会话变量就以为生效。WITH PARSER ngram只对支持 ngram 的 InnoDB 表生效,建索引前先看表引擎。1700 万行上直接加全文索引会跑很久,放在导入完成后、业务低峰期做,失败时先看磁盘和临时空间。
查询时MATCH的列必须和索引列一致:
SELECT id, stem, MATCH(stem_plain) AGAINST ('二次函数' IN BOOLEAN MODE) AS score FROM questions WHERE MATCH(stem_plain) AGAINST ('二次函数' IN BOOLEAN MODE) ORDER BY score DESC LIMIT 20;IN BOOLEAN MODE支持+、-、*等符号,但用户输入里如果带这些符号,直接拼进去会改变查询语义。检索入口先做一次安全化,把布尔符号替换成空格,再按词拼查询串。
import re def to_boolean_query(q: str) -> str: q = re.sub(r'[+\-*"@()~<>]', ' ', q or "") terms = [t for t in re.split(r'\s+', q) if t] return " +".join(terms)to_boolean_query把每个词前加+,表示必须包含;如果产品需要“任一命中”,把+去掉换成空格。score只用于排序,不要当精确匹配率展示给用户。查询词为空时直接返回空结果,不要让MATCH跑全表。
4.3 公式渲染链路与转义边界
前端拿到的是原始stem,不是stem_plain。渲染时先按$$...$$和$...$分段,普通文本用textContent写入,公式片段整段交给公式渲染器,不要手工拼 HTML。下面这段只演示分段和文本节点写入,具体公式渲染器按项目现有依赖接。
function renderMixedText(el, raw) { const parts = String(raw || "").split(/(\$\$[\s\S]*?\$\$|\$[^$]*?\$)/g); for (const part of parts) { if (!part) continue; const node = document.createElement("span"); if (part.startsWith("$$") || part.startsWith("$")) { node.className = "math-source"; node.textContent = part; // 交给公式渲染器,不手工拼 HTML } else { node.textContent = part; // 普通文本走 textContent,避免 XSS } el.appendChild(node); } }分段正则先匹配块级再匹配行内,避免$$被行内规则拆坏。普通文本用textContent,公式片段也先用textContent占位,再由渲染器处理;不要先innerHTML再转义,$x<y$里的<会被浏览器当成标签开头。渲染失败时先看原始字段里是不是<、>这种 HTML 实体,实体化过的公式再交给渲染器,通常会显示成源码。
5. 避坑与排查:导入、检索、渲染三个阶段的血泪记录
5.1 导入阶段:反斜杠被吞和字符集乱码
现象:导入后题干里的\frac{1}{2}变成rac{1}{2},或者\alpha变成换行加lpha。原因:SQL 文件里的反斜杠被 MySQL 客户端当成转义符,\f、\n、\t被解释成控制字符;如果sql_mode开了NO_BACKSLASH_ESCAPES,规则又反过来,同一个文件在不同环境表现不一致。解决:导入前打印sql_mode,清洗脚本按目标环境统一反斜杠层数;SQL 文件里公式命令用双反斜杠写入,导入后抽样比对原始文件和库内stem的哈希。不要用“看起来正常”判断,抽 100 条含\frac、\sqrt、\alpha的题逐条看。
SELECT @@sql_mode; SELECT id, LEFT(stem, 120) AS stem_head FROM questions WHERE stem LIKE '%frac%' OR stem LIKE '%sqrt%' LIMIT 20;现象:数学符号≤显示成?,物理单位Ω变成乱码。原因:库表是utf8mb4,但客户端连接不是,或者导入时用了默认字符集;也可能是源文件本身不是 UTF-8。解决:建库、建表、连接、导入命令四层统一utf8mb4,用SHOW CREATE TABLE确认字段字符集,用SELECT HEX(stem)看存储字节。源文件编码不确定时先用file -i看编码,转成 UTF-8 再导入,不要直接改库表字符集硬扛。
现象:导入到 60% 报Lock wait timeout exceeded,或者回滚段暴涨。原因:单文件一个大事务,或者脚本autocommit=0后忘了提交;InnoDB 的 undo 一直涨,锁等待把后续批次也拖死。解决:按批提交,每批 1000 到 5000 行;导入前确认没有其他长事务,失败后先看SHOW ENGINE INNODB STATUS和错误日志,不要直接重跑。生产库不要为了导入临时改持久化参数,测试库改完也要改回来。
现象:唯一键uk_source_hash冲突,导入中断。原因:同一道题多来源重复,或者清洗后题干相同但原始 id 不同;也可能是空哈希被多条记录命中。解决:导入前先统计重复哈希,空哈希单独补;导入语句用INSERT IGNORE或ON DUPLICATE KEY UPDATE前,先确认业务要保留哪一条。不要为了导入成功直接删唯一索引,重复题会在组卷时冒出来。
5.2 检索与渲染阶段:索引失效与公式二次转义
现象:建了全文索引,查“二次函数”还是没结果。原因:索引建在stem上而不是stem_plain,公式噪声把关键词切碎;或者查询词长度小于ngram_token_size;也可能是stem_plain还没回填,整列是空。解决:先查SHOW VARIABLES LIKE 'ngram_token_size',再查stem_plain的非空比例,最后用MATCH对索引列做最小样例。改ngram_token_size必须重建索引,不要只改会话变量。
SELECT COUNT(*) AS total, SUM(stem_plain IS NOT NULL AND stem_plain <> '') AS filled FROM questions; EXPLAIN SELECT id FROM questions WHERE MATCH(stem_plain) AGAINST ('二次函数' IN BOOLEAN MODE) LIMIT 20;现象:前端公式显示成源码,或者$x<y$里的<把后面内容吞掉。原因:先做了 HTML 转义再交给公式渲染器,或者用innerHTML拼公式片段;普通文本和公式片段没有分开处理。解决:普通文本走textContent,公式片段整段交给渲染器,不在中间手工拼 HTML;渲染前抽样含<、>、&的公式,确认显示和原始字段一致。公式渲染失败时先看接口返回的是不是已经是 HTML 实体,不要在前端反复转义。
现象:组卷查询按学科和题型筛题,翻到后面越来越慢。原因:联合索引顺序不对,或者查询条件里对subject_id做了函数运算;也可能是ORDER BY id DESC和过滤条件冲突,优化器走了主键扫描。解决:用EXPLAIN看key和rows,联合索引按“等值条件在前、范围条件在后”排;分页深翻用“上一页最大 id”游标,不要LIMIT 1000000, 20。索引不是越多越好,组卷、检索、回显各留一条主路径就够。
6. 进阶验证:用 EXPLAIN 和抽样回读确认题库真的可用
导入完成不等于题库可用。我一般先跑三条验证:组卷查询走不走索引、公式抽样回读是否一致、全文检索有没有命中。组卷查询用EXPLAIN看执行计划,重点看type是不是range或ref,key是不是走了idx_subject_type_diff,rows是不是在可接受范围。
EXPLAIN SELECT id, stem FROM questions WHERE subject_id = 1 AND question_type = 1 AND difficulty BETWEEN 2 AND 4 ORDER BY id DESC LIMIT 20;如果key是PRIMARY且rows很大,说明优化器在扫主键,先把ORDER BY id DESC的深分页改成游标条件,再检查联合索引顺序。公式抽样回读不要只看一条,按学科各抽 100 条含$的题干,把库内stem和原始文件对应行做哈希对比;对不上就回到第 3 章的清洗脚本,别在前端打补丁。全文检索用最小样例确认MATCH能命中,再逐步加长查询词,观察ngram_token_size的影响。
从那以后我每次拿到这种题库压缩包,都先跑一遍字符集、公式抽样和EXPLAIN三件套,再谈全量导入。希望帮到你。
本文还有配套的精品资源,点击获取