简介:面向K12在线教育题库建设者与内容运营者,这份MySQL题库资源包集中解决数学、物理、化学公式在录入、存储与网页端显示的常见难题。包内含完整数据库脚本、章节与知识点划分样本,以及题目属性设置参考,可帮助从业者快速搭建题库结构并理解LaTeX公式呈现逻辑。资源共583个文件,以574张png示例图为主,另有2份docx说明文档、2份html演示页、2份js渲染脚本、1份sql数据文件和若干辅助文件,压缩包仅2.03MB,轻量易用,目录结构清晰便于按模块查阅。已有1106人浏览学习,适合K12教育产品设计、题库运营及技术开发等初级到中级人员。对照文档中的数据结构图和MathJax渲染示例,读者能直接获得从数据建模到公式显示的一体化参考方案,也可将其中的章节划分、题目属性设置思路复用到自己的题库系统。
1. 一个「中小学题库mysql.zip」,值不值得花一个下午拆开看
「中小学题库mysql.zip」这种压缩包,经常出现在网盘、QQ 群和课程配套资源里,下载是一回事,能真正把题用起来是另一回事。我见过太多次这样的场景:一位老师兴冲冲解压,双击打开里面的 .sql 文件,编辑器直接卡死;或者在图形客户端里导入,半小时后报错,满屏乱码。
这个 zip 的核心资产其实只有两样:一份建表脚本和一份数据脚本。前者决定了你知道题库怎么组织,后者决定了你能不能查出想要的题。但真正让人翻车的,往往不是数据本身,而是导入姿势、字符集内外不一致、以及建表脚本里那些隐性的顺序依赖。这篇文章会从表结构入手,讲清楚怎么安全导入、怎么验证数据能不能用、以及常见的 5 个坑。
我默认你装的是 MySQL 8.0 或 5.7,标题里带 zip 也只是个载体。看懂下面的步骤,你拿到任何类似题库包都能在半小时内跑起来。
2. 先看表结构再谈导入:这套题库的库表和字段,以及在设计上的取舍
很多人的习惯是双击 zip、解压、立刻导入,结果导入一半报错,才回头去翻建表脚本。我一般会反过来:先用文本查看器把 .sql 文件的开头几百行读完,把表结构弄清楚,再动手。因为导入失败的大多数原因,在表结构里已经写了答案。
2.1 最常见的四张表:主表、选项表、科目表、知识点关联
网上下载的中小学题库包,表结构基本逃不出下面这个套路。先看科目表,再看题目主表,然后是选项表和知识点关联表。
科目表最简单,一般只存 id 和科目名:
CREATE TABLE `subject` ( `id` smallint unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '科目名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科目表';题目主表是整个库的核心,字段通常在 10 个上下。常见设计是:id、科目 ID、题型、难度、题干、答案、解析、来源、状态、创建时间。下面这份建表语句我做了微调,但思路和网上题库包基本一致:
CREATE TABLE `question` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `subject_id` smallint unsigned NOT NULL COMMENT '科目ID,关联 subject.id', `type` tinyint unsigned NOT NULL COMMENT '题型:1单选 2多选 3判断 4填空 5解答', `difficulty` tinyint unsigned NOT NULL COMMENT '难度等级:1到5', `knowledge_point` varchar(100) NOT NULL COMMENT '知识点编码或路径', `stem` text NOT NULL COMMENT '题干正文', `answer` text COMMENT '参考答案', `analysis` text COMMENT '解析', `source` varchar(100) DEFAULT '' COMMENT '题目来源', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_subject_type` (`subject_id`,`type`), KEY `idx_difficulty` (`difficulty`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目主表';这里knowledge_point单独用一个字符串字段,是省事设计。更规范的做法是拆出知识点表和题目-知识点关联表,因为一题可以对应多个知识点,多对多关系用字符串逗号分隔会很难查。不过很多现成题库为了简化,就这么干了,你要接受它的取舍。
选项表只在有选择题时才有意义,而且一条题目的选项行数不固定,所以必须单独存:
CREATE TABLE `question_option` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `question_id` bigint unsigned NOT NULL COMMENT '关联 question.id', `option_order` tinyint unsigned NOT NULL COMMENT '选项顺序:1代表A', `option_text` text NOT NULL COMMENT '选项内容', PRIMARY KEY (`id`), KEY `idx_question_id` (`question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目选项表';还有一类表叫关联表或归档表,比如题目-知识点关联。如果你在 zip 里看到它,说明这个题库的设计比较规范;如果只有主表加一个知识点逗号字段,说明作者为了省事做了扁平化。两种情况都能用,但后续查「某个知识点下有哪些题」的写法完全不同——前者要用 JOIN,后者要用 LIKE。
2.2 打开建表脚本时,我按什么顺序读表结构
拿到 .sql 文件不要从第 1 行读到最后一题,那样太慢。我读建表脚本的顺序分四步。
第一步看文件头部的注释和 SET 语句。很多脚本开头是SET NAMES utf8mb4;、SET FOREIGN_KEY_CHECKS=0;,这两行决定了导入后的字符集和外键校验状态。如果脚本里没有,导入时就要自己补。
第二步用编辑器的搜索功能,或者命令行的 grep,把所有CREATE TABLE拉出来。这一步能快速看到这个库一共几张表,谁先建谁后建。外键依赖的表通常排在被依赖的表后面,这个顺序如果乱了,直接导入一定会报「表不存在」的错误。
第三步挑题目主表看字段注释。注释里一般写着题型编号、难度等级的含义,这比去读一万行 INSERT 高效得多。你可以用一条简单命令抽取表结构:
grep -n "CREATE TABLE" school_questions.sql sed -n '1,80p' school_questions.sql第一条命令定位所有建表语句的行号,第二条命令看文件开头 80 行。这样你还没导入数据库,就已经知道有哪些表、主表长什么样了。这个动作几乎每次导入都能帮我避免一次翻车,尤其是刚拿到的包可能建表语句和插入数据混杂在一起时。
第四步是看 INSERT 语句的字段列表。如果 INSERT 里写明了(id, subject_id, type, ...),那说明字段一一对应;如果只写VALUES没写字段名,那说明数据录入顺序和建表顺序完全一致,改表结构时就要小心,索引错位会导致整批数据写进错误的列。
读完这四步,你对这个题库的了解就超过了 90% 直接双击导入的人。而且你可以在导入前先做一个决定:是原样导入,还是先用脚本把建表语句里的字符集、引擎统一改成你服务器的当前配置。
3. 把 SQL 灌进 MySQL 的两种姿势:命令行 source 和图形客户端导入
表结构看完了,接下来才是真正动手的时候。这里唯一的硬性要求是:不要用文本编辑器打开整个 .sql 文件去「查看」,更不要把它复制粘贴进查询窗口。下面两种方式都可行,但我推荐第一条。
3.1 命令行导入最大的坑是字符集,一条指令把参数说透
命令行导入是处理大文件最稳的方式,因为不用经过客户端内存中转。假设你的题库文件名是school_questions.sql,最小可用的导入命令是这样:
mysql -uroot -p \ -h127.0.0.1 \ -P3306 \ --default-character-set=utf8mb4 \ school_db < school_questions.sql命令里每一项都值得说清楚。-h127.0.0.1是连本机 MySQL,如果不加这一项,很多 MySQL 客户端会默认走本机 socket,在某些系统上反而慢。-P3306注意是大写的 P,指定端口;小写-p才是密码参数,这两个别写反,写反了要么提示密码错误,要么提示端口错误。
--default-character-set=utf8mb4是导入中文题库的核心参数。MySQL 服务端的默认字符集可能是 latin1 或 utf8mb3,如果你不指定,客户端发送的语句会被按服务端默认字符集解释,中文字段非常容易变成乱码。这个参数的作用是让客户端告诉服务端「我发的 SQL 是用 utf8mb4 编码的」,服务端也会用 utf8mb4 去解析。绝大多数线上题库脚本都是 utf8mb4,少数老包是 gbk,如果是 gbk 就把参数改成gbk,先看脚本头部的 SET NAMES 再定。
school_db是目标数据库名,在你执行命令之前,这个库得先建好。用下面的语句建库,注意字符集和排序规则也一并指定:
mysql -uroot -p -e "CREATE DATABASE school_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"utf8mb4_general_ci适合题库这种查询为主的场景,排序规则更宽松;如果你需要精确按 Unicode 规则排序,可以用utf8mb4_unicode_ci。但对中文题库来讲,一般就够用了。
另一种命令行导入方式是先进 MySQL 再执行 source:
mysql -uroot -p -h127.0.0.1 --default-character-set=utf8mb4进入交互界面后输入:
USE school_db; SOURCE /home/user/school_questions.sql;SOURCE 后面的路径是服务器本地路径,不是 Windows 盘符路径,这一点很多人第一次用会写错。SOURCE 和重定向<的最大区别在于,SOURCE 会在终端里逐行显示执行结果,导入过程肉眼可见,但输出会非常长,刷屏刷到你想放弃。我一般只在导入中途出问题、需要定位是哪一段报错时才用 SOURCE,平时直接用重定向。
3.2 图形客户端导入时,先关掉外键校验再看错误日志
如果你更习惯用图形客户端,比如 DBeaver 或者官方自带的 Workbench,操作路径基本是:连上数据库、选中目标库、选择运行 SQL 脚本文件、然后等结果。这个流程本身没问题,但有几个参数会被默认值坑到。
第一个是字符集选项。很多图形客户端的「运行 SQL 脚本」对话框里,默认编码是系统区域设置的编码,Windows 中文系统可能默认 GBK,你要手动改成 UTF-8。
第二个是导入超时时间。MySQL 的 max_allowed_packet 默认值是 64MB,如果脚本里某一行插入语句特别长,超过了这个限制,导入会直接断开。你可以先执行下面的命令查看当前值:
SHOW VARIABLES LIKE 'max_allowed_packet';如果偏小,可以在当前会话里临时调大:
SET GLOBAL max_allowed_packet = 134217728;这个参数指单个包的最大字节数,128MB 基本能覆盖绝大多数题目。注意它是 GLOBAL 级别,新连接才生效,图形客户端要重新连一次才能读到新值。
第三个是外键校验。如果脚本里的建表顺序不合理,或者你导入的目标库已经有部分旧表,图形客户端导入时会一个接一个报「外键约束失败」。常见做法是在导入前手动执行一条:
SET FOREIGN_KEY_CHECKS = 0;导入完成后再恢复为 1。有些脚本自己就带了这个语句,导入过程中你也看到日志里跑过它,那就没必要手动加。但问题在于图形客户端大多数情况是一个语句一个语句执行,如果脚本没写,遇到外键报错会停在那里等你点确认,而不是自动跳过。
图形客户端导入还有个隐藏问题:大事务。有些脚本用BEGIN和COMMIT包住所有插入语句,几十万行数据放在一个事务里,一旦中途失败,所有数据回滚,前面导入的时间全部白费。命令行重定向的方式反而不会这样逐事务保留,虽然中途失败会有部分数据,但至少能快速重来。
4. 十万道题能不能用,看三个验证查询:抽题、判重、性能摸底
库导进去之后,第一件事不是急着写高大上的功能,而是验证这套题「能不能干活」。我一般做三个验证查询:抽一页题、找重复题、看一条慢查询的执行计划。这三个问题覆盖了日常使用频率最高的场景。
4.1 抽一套 20 题的卷子,先认识 ORDER BY RAND 的代价
最常见的需求是:按科目、题型、难度随机抽 20 道题。很多新手第一反应是写成这样:
SELECT id, stem, difficulty FROM question WHERE subject_id = 1 AND type = 1 AND difficulty BETWEEN 3 AND 4 ORDER BY RAND() LIMIT 20;这个语句在 1 万行以内跑起来问题不大,数据量到了 10 万行就开始卡,几十万行基本没法用。原因是ORDER BY RAND()会让 MySQL 对满足条件的每一行都生成一个随机数,然后排序取前 20。这意味着它要先扫描全表、把所有行放进临时表,才能排序。对中小学题库这种想要做随机组卷的场景,几乎每个请求都会带这个操作,性能不可接受。
我一般这样改:先查出满足条件的 id 集合,在这个小集合里随机,再回表取数据。更具体地说,可以用子查询缩小随机范围:
SELECT q.id, q.stem, q.difficulty FROM question q JOIN ( SELECT id FROM question WHERE subject_id = 1 AND type = 1 AND difficulty BETWEEN 3 AND 4 ORDER BY RAND() LIMIT 20 ) t ON q.id = t.id;这个写法仍然会扫描所有符合条件的行,但子查询里只拿 id,不碰 text 字段,临时表的体积小很多,排序负担也小。如果表里数据已经有几十万行,还是慢,那就要靠索引覆盖了。
(subject_id, type, difficulty)三列的联合索引是这个查询的关键。有了它,WHERE 的等值条件加范围条件都能走索引,子查询的扫描范围会被索引直接框住。如果你没建这个索引,可以把主表里的idx_subject_type扩展成三列联合索引,注意 B+ 树对范围条件之后的列无法继续使用,所以 difficulty 应该放在最后。
4.2 用 MD5 找重复题:text 字段直接 GROUP BY 的翻车现场
题库包里重复题非常普遍,尤其是从多个渠道汇总出来的包。判断重复题的标准往往是题干相同。有些人直接用 stem 字段 GROUP BY:
SELECT stem, COUNT(*) AS cnt FROM question GROUP BY stem HAVING cnt > 1 LIMIT 50;这个语句在数据量大时有两个问题。第一,GROUP BY 一个 text 字段会消耗大量的排序和临时表空间,而且 text 在 GROUP BY 时只能用前缀,即使字段内容不同也可能被归到同一组。第二,InnoDB 的临时表会从内存转磁盘,几十万行这个查询能把磁盘 IO 打满。
我一般会给题目主表加一个 MD5 列,存题干的哈希值。MD5 在这里不是用来加密,而是把不定长文本变成定长 32 位字符串,索引和分组都快得多:
ALTER TABLE question ADD COLUMN stem_md5 char(32) GENERATED ALWAYS AS (MD5(stem)) STORED; ALTER TABLE question ADD INDEX idx_stem_md5 (stem_md5);第一条语句里的GENERATED ALWAYS AS ... STORED是生成列,MySQL 在插入和更新时会自动计算 MD5 值,不需要你手动维护。之后查重就可以这样:
SELECT q.id, q.stem_md5, q.stem FROM question q JOIN ( SELECT stem_md5, MIN(id) AS keep_id, COUNT(*) AS cnt FROM question GROUP BY stem_md5 HAVING cnt > 1 LIMIT 50 ) d ON q.stem_md5 = d.stem_md5 AND q.id != d.keep_id;这条语句能列出「应该被删除的重复项」,保留每组里 id 最小的那一条。需要注意,MD5 是 128 位摘要,理论上有碰撞可能,但对题库查重这种场景,碰撞概率低到可以忽略。如果你要更严格,可以把 stem 和 answer 两个字段拼起来做 MD5,减少不同答案同题干的干扰。
还有一类近似重复是「题干里有空格不同、标点全角半角不同」,MD5 查不出来。这种只能靠归一化处理后再比,比如把全角字符转半角、去掉所有空白、去掉 HTML 标签,再算一次normal_stem_md5。不过那是清洗数据的活儿,不是 SQL 能直接解决的,我通常只在导入前做预处理。
4.3 性能摸底:EXPLAIN 只看这几列就够
题库系统做完能不能放心上线,我在验证阶段一定会跑一次 EXPLAIN 看执行计划。以刚才的抽题查询为例:
EXPLAIN SELECT q.id, q.stem FROM question q JOIN ( SELECT id FROM question WHERE subject_id = 1 AND type = 1 AND difficulty BETWEEN 3 AND 4 LIMIT 20 ) t ON q.id = t.id;执行计划里重点看三列:type、key、rows。
type列是访问类型,常见取值从好到差依次是 const、eq_ref、ref、range、index、ALL。ref和range说明用上了索引,ALL说明全表扫描。如果看到ALL,需要马上检查 WHERE 条件有没有匹配索引。
key列显示实际用到的索引名,如果显示 NULL,说明这条查询没走索引。rows列是 MySQL 估算的扫描行数,这个数字比实际行数大 10 倍以上时,一般就是索引失效或者统计信息过旧。
我还习惯看一个容易忽略的细节:Extra列里如果出现Using temporary或Using filesort,说明查询里产生了临时表或文件排序。短时间跑一次没事,要是这个查询被频繁调用,磁盘 IO 会被拖垮。前面的 GROUP BY 查重就属于这种情况。
验证完这三个查询,你就知道这个题库库是「能用了」还是「还得修」。绝大多数网上下的题库包,导入之后缺的不是数据,而是索引。所以性能摸底之后,顺手补上联合索引和 MD5 生成列,这两步的价值比后面任何花哨的功能都大。
5. 导入题库的五个高频坑:现象、原因、改法
这一章是血泪经验的集合。题库导入翻车的点位其实很集中,我把高频问题按「现象→原因→解决」拆开,每一条都是可以直接照抄的改法。
5.1 打开表全是问号:utf8mb4 没有贯穿全程
现象:导入执行不报错,但查询出来的题干、答案全是乱码,常见的是问号串,也有口字型的方块。
原因:SQL 文件本身是 utf8mb4 编码,但你用系统默认的 gbk 或 latin1 编码导入,或者建库时字符集没指定,服务端按默认字符集解析了插入语句。整条链路只要有一个环节不是 utf8mb4,中文就会坏。
解决:先查库和表的字符集:
SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = 'school_db'; SELECT table_collation FROM information_schema.TABLES WHERE table_schema = 'school_db' AND table_name = 'question';如果显示不是 utf8mb4,需要把字符集改掉。数据还没导入时,重建库最干净:
mysql -uroot -p -e "DROP DATABASE school_db; CREATE DATABASE school_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"已经导入但乱码的表,修改字符集后数据不会自动从乱码变回可读,因为写入时已经损坏。这种情况别浪费时间想着修复,直接换编码重新导入一次。切记导入命令必须带--default-character-set=utf8mb4,别依赖脚本头部的 SET NAMES,因为有些人下载的包里那个 SET NAMES 被注释掉了。
5.2 解压出来不是 .sql 而是又一个 zip:先看压缩包结构
现象:解压第一层得到一堆 zip、rar、7z 文件,往里翻还有多层目录,根目录路径带着中文和空格,命令行操作时各种报路径不存在。
原因:发布者打包时没有整理目录,把总压缩包塞进了另一个压缩包,或者用中文目录名直接打包。很多图形解压工具默认继承原压缩包目录结构,导致脚本路径变得很深。
解决:建议不要用鼠标双击解压,改用命令行解压到指定目录,并且先列出压缩内容再决定:
unzip -l school_questions.zip unzip school_questions.zip -d ./quiz_unpack-l参数只列内容不真正解压,你会第一眼看到有没有嵌套压缩包。-d指定解压到当前目录下的 quiz_unpack,避免和默认同名目录搅在一起。解压完成后用 find 检查所有 .sql 文件的路径:
find ./quiz_unpack -name "*.sql" -exec ls -lh {} \;如果还有嵌套的 zip,再按层解一次。这里有个判断标准:最终你拿出来给 mysql 导入的是一个 .sql 或多个 .sql 文件,不是又一个压缩包。如果发现目录里既有 .sql 又有 .zip,优先怀疑 zip 里才是更新版本,看看两边的文件大小和日期,别导了一份旧数据还蒙在鼓里。
5.3 几百 MB 的 SQL 文件,用编辑器打开只会卡死
现象:导入前想预览一下文件内容,双击用文本编辑器打开,结果编辑器假死几分钟,然后提示文件过大;或者导入到一半提示 lost connection,前面导的一堆数据都回滚了。
原因:SQL 数据文件动辄几百 MB,普通文本编辑器会一次性读入内存,内存耗尽自然卡死。导入中断则是连接超时或 max_allowed_packet 限制导致。
解决:预览大文件别用图形编辑器,用命令行分段看:
head -100 school_questions.sql grep -n "CREATE TABLE" school_questions.sql | head -50 sed -n '5000,5050p' school_questions.sqlhead看文件头部的建库建表语句,grep -n提取所有 CREATE TABLE 的位置,sed指定行号区间看某一段插入数据。这三个命令处理几百 MB 文件都是秒开,也不会把内存吃满。
导入中断的解决方式分两步。第一步服务器端调大超时和包大小:
SET GLOBAL wait_timeout = 28800; SET GLOBAL max_allowed_packet = 268435456;第二步导入时用 nohup 或把它放到后台,避免终端断开导致导入中断:
nohup mysql -uroot -p -h127.0.0.1 --default-character-set=utf8mb4 school_db < school_questions.sql > import.log 2>&1 &导入过程会在后台运行,日志写入 import.log,你随时可以用tail -f import.log查看进度。这一步能在最大程度上避免「手机锁屏、终端断连、导入中止」的悲剧。
5.4 外键约束导致导入到一半中断:检查建表顺序和引擎
现象:导入脚本执行到第几百条时报Cannot add or update a child row: a foreign key constraint fails,或者建表阶段提示引用了一张不存在的表。
原因:脚本里建表顺序不合理,子表先于父表创建;或者父表数据还没插入,子表数据就来了;还有一种情况是把表建成了 MyISAM 引擎,MyISAM 不支持外键约束,导入提示直接报错。
解决:最简单的兜底是在导入前强制关闭外键校验。命令行的做法是在同一个 mysql 会话先执行:
SET FOREIGN_KEY_CHECKS = 0; USE school_db; SOURCE /path/to/school_questions.sql; SET FOREIGN_KEY_CHECKS = 1;如果用的是重定向导入,可以把这行写在 SQL 文件的最前面。不过我不建议直接改原文件,更稳的方式是生成一个包装脚本:
echo "SET FOREIGN_KEY_CHECKS=0;" > import_wrapper.sql cat school_questions.sql >> import_wrapper.sql echo "SET FOREIGN_KEY_CHECKS=1;" >> import_wrapper.sql mysql -uroot -p -h127.0.0.1 --default-character-set=utf8mb4 school_db < import_wrapper.sql原理是在同一次连接里,关闭外键检查后,MySQL 不再逐条校验子表和父表的匹配关系,导入完再打开,保证后续日常操作的外键一致性。关掉期间不会破坏数据,只要数据本身是自洽的,之后数据库也不会出问题。
顺便检查一下表的引擎,如果主表是 MyISAM,建议转成 InnoDB。数据没导入前可以直接改建表语句里的 ENGINE,已经导入的话执行:
ALTER TABLE question ENGINE=InnoDB;InnoDB 支持事务、行级锁和外键,比 MyISAM 适合题库这种读写混合且需要在线修改的应用场景。
5.5 重复导入把自增主键干翻车:TRUNCATE 与业务唯一键
现象:第一次导入成功,第二次重新导入同一份文件时,报Duplicate entry '1024' for key 'PRIMARY',或者导入成功但数据翻倍,id 乱跳。
原因:INSERT 语句里显式写了 id 值,第二次导入时和已有主键冲突。如果 INSERT 没写 id,只写其他字段,那自增主键会从上次最大值继续,数据看起来正常,但和之前导入的内容重复叠加。
解决:如果想清掉旧数据重新导入,用 TRUNCATE 而不是 DELETE。TRUNCATE 会重置自增计数器和所有数据,而且速度远快于 DELETE:
TRUNCATE TABLE question; TRUNCATE TABLE question_option;如果不想清库,只想追加新题,那就不能依赖自增主键去重,得给题目加一个业务唯一键,比如「来源+题库内题号」。关于这个做法,我在下一章详细展开,这里先记住一个原则:只要有「同一份 zip 导两次」的可能,主键自增 ID 从来不是判断重复的依据。
排查主键撞车还有一种情况值得注意:两个 sql 文件都从 1 开始编号,但各自对应不同科目,如果直接合并导入到一个表,第二次会全部撞车。这种多文件题库包,要么分表存,要么导入前统一重排 id,我一般优先选择前者,把文件按科目拆开分别导入对应表,省去大量改 id 的麻烦。
6. 从「能查」到「能用」:给题库加增量更新,顺便躲开全文索引的坑
很多老师拿到题库包,导入后查了几次题就觉得完事了。但题库不是一次性静态资源,教研组会持续往里加题、改答案、修正解析。这里有两个进阶操作,决定了这个库半年后是越来越值钱还是沦为废数据。
6.1 增量更新:用业务唯一键做主键冲突的后悔药
为题目主表增加一个业务唯一键biz_key,它代表「这道题在原始题库包里的唯一身份」。如果原始数据里有题号,就拼上来源前缀;没有题号,就用题干 MD5 的前 16 位。
ALTER TABLE question ADD COLUMN biz_key varchar(64) DEFAULT ''; UPDATE question SET biz_key = CONCAT('src01_', id); ALTER TABLE question ADD UNIQUE KEY uk_biz_key (biz_key);之后每次有新的题目数据,不再先查有没有重复,而是直接插入时触发唯一键处理。MySQL 8.0.19 之后的写法是:
INSERT INTO question (subject_id, type, difficulty, stem, answer, analysis, biz_key) VALUES (1, 1, 3, '这是一道新题干的题', '答案', '解析', 'src01_1024') AS new ON DUPLICATE KEY UPDATE stem = new.stem, answer = new.answer, analysis = new.analysis, updated_at = NOW();这条语句的意思是:如果 biz_key 不存在,就正常插入;如果已存在,就更新题干、答案和解析,而不是报错或者插入重复行。我第一次用这套逻辑时也犹豫过,后来发现对题库管理来说,它就是我常说的后悔药——同一道题被不同人录了多遍,最终只会留下一行最新版本。
6.2 全文索引对中文题干不友好:短文本检索该用的兜底方案
有人会建议在 stem 字段上加全文索引来实现题干搜索。MySQL 8.0 的全文索引对中文支持依赖 ngram 分词插件,效果中规中矩。对题干这种几十字的短文本,全文索引的分词粒度经常导致误匹配:搜「三角」能出来「三角形相似」,但搜「全等」反而被分词拆碎查不到。
我的习惯是:题目正文的复杂检索不依赖数据库全文索引。先用结构化字段过滤,再在业务层做字符串匹配。具体来说,把知识点、题型、难度做成下拉筛选,题干搜索用 LIKE 限定在已经缩小的结果集里,这样既不用引入额外组件,也能跑得动。等到题量超过百万行、业务确实需要语义搜索时,再考虑外置搜索服务,那个迁移成本不是一开始就该承担的。
我养成的习惯是每次拿到题库包,先建唯一键再导入,先跑查重再上线。经历过一次重复导入导致数据翻倍的教训之后,这套流程我再也没跳过。希望这篇笔记能帮你看清这类数据库压缩包的里子和坑,照着上面的步骤走一遍,一个下午把它变成能正经干活的东西。
本文还有配套的精品资源,点击获取