资讯详情

1700万K12题库落库:MySQL表结构、LaTeX公式与全文检索实践

📅 2026/10/31 11:18:59 | 华诺云谱 👁 阅读
1700万K12题库落库:MySQL表结构、LaTeX公式与全文检索实践
简介面向在线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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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-setutf8mb4不要依赖客户端配置。mysql --default-character-setutf8mb4 -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_ESCAPESSQL 文件里的反斜杠处理规则会变公式清洗脚本要跟着调整ngram_token_size决定后面全文索引的最小切词长度默认值不是每个环境都一样建索引前必须确认。参数上--default-character-setutf8mb4只影响当前连接不会改服务端配置适合导入前临时对齐。再看库和表的实际字符集避免“建库时对了、建表时继承了旧默认值”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 DBk12_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-setutf8mb4 -u root -p$MYSQL_PWD $DB $f echo $f done.log doneset -euo pipefail让脚本在任一步失败时停下避免“错了还继续导下一批”。done.log是断点续跑的后悔药导入失败后修完当前批次重跑脚本会自动跳过前面的文件。MYSQL_PWD从环境变量取不要把密码写进脚本--default-character-setutf8mb4每一批都要带不能只在第一批带。如果资源包给的是单个大文件又确认不能按行切常见做法是用流式解析器按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, flagsre.S) s re.sub(r\\\((.*?)\\\), r$\1$, s, flagsre.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, flagsre.S) s re.sub(r\$.*?\$, , s, flagsre.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再转义$xy$里的会被浏览器当成标签开头。渲染失败时先看原始字段里是不是lt;、gt;这种 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或者回滚段暴涨。原因单文件一个大事务或者脚本autocommit0后忘了提交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;现象前端公式显示成源码或者$xy$里的把后面内容吞掉。原因先做了 HTML 转义再交给公式渲染器或者用innerHTML拼公式片段普通文本和公式片段没有分开处理。解决普通文本走textContent公式片段整段交给渲染器不在中间手工拼 HTML渲染前抽样含、、的公式确认显示和原始字段一致。公式渲染失败时先看接口返回的是不是已经是 HTML 实体不要在前端反复转义。现象组卷查询按学科和题型筛题翻到后面越来越慢。原因联合索引顺序不对或者查询条件里对subject_id做了函数运算也可能是ORDER BY id DESC和过滤条件冲突优化器走了主键扫描。解决用EXPLAIN看key和rows联合索引按“等值条件在前、范围条件在后”排分页深翻用“上一页最大 id”游标不要LIMIT 1000000, 20。索引不是越多越好组卷、检索、回显各留一条主路径就够。6. 进阶验证用 EXPLAIN 和抽样回读确认题库真的可用导入完成不等于题库可用。我一般先跑三条验证组卷查询走不走索引、公式抽样回读是否一致、全文检索有没有命中。组卷查询用EXPLAIN看执行计划重点看type是不是range或refkey是不是走了idx_subject_type_diffrows是不是在可接受范围。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三件套再谈全量导入。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑