简介:面向中小学在线教育从业者与题库系统开发者,这套数据库资料围绕K12题库的存储结构、章节与知识点建设、题目属性设置展开,重点解决数学、物理、化学等学科公式录入与前端显示难题,并以LaTeX与MathJax的配合给出可参考的实现路径。压缩包共583个文件,约2.03MB,主体为574个PNG图片,用于展示公式渲染与页面效果;另有2份DOCX说明文档、HTML与JS示例页面、SQL建表脚本及TXT说明,便于对照数据库结构与显示方案。已有1109人学习下载。读者可从中获取题库字段设计、章节知识点组织、公式排版与页面展示的完整样本,也可借助样本题库理解题目属性的配置方式,适合正在搭建免费K12题库、需要处理理科公式显示与数据结构设计的团队参考。
1. 一个 1700 万条的中小学题库 MySQL 备份,很多人第一反应是双击导入、然后盯着进度条看。真装过就知道,这套东西最容易出问题的地方不在装 MySQL 本身,而在版本和字符集:同一份 dump 在 5.7 上跑完,数学题库里的 LaTeX 公式可能整段变成frac{1}{2},物理题库的单位符号变成问号,整个过程一句报错都没有。等接上前端渲染才发现题目是残的,回头重导一次又是几个小时。这份东西适合正在搭 K12 题库、需要离线数学题库和物理题库的开发者,也适合想把 LaTeX 公式显示链路一次配对、不想反复返工的人。
2. 环境准备:MySQL 8.0 的磁盘、字符集与五个必改参数
2.1 先算磁盘和内存,别装到一半才发现不够
1700 万条题目不是 1700 万行纯文本。一条题干平均几百字节,加上选项、答案、解析、知识点标签、难度系数、来源地区、年份,单行落到磁盘上按 1.5~2.5 KB 估,表数据就在 25~40 GB 这个量级;题干和解析上再建索引,二级索引少说再加 5~10 GB。真正要命的是导入期间这三份东西同时存在:原始 dump 文件、正在膨胀的表空间、索引构建时的临时文件。所以我一般按 dump 大小的 2.5 倍去申请磁盘,宁可空着也别卡在 99% 上。
| 资源 | 下限 | 建议 | 为什么这么定 |
|---|---|---|---|
| 磁盘 | 120 GB SSD | 300 GB 以上 NVMe | dump 原文件、导入后表空间、索引临时空间三份并存 |
| 内存 | 8 GB | 32 GB 起 | buffer pool 越大,建二级索引时回表读盘越少 |
| CPU | 2 核 | 8 核 | 导入后半程基本是索引排序,吃并行 |
| MySQL | 8.0.28 | 8.0.32 及以上 | 8.0.30 起 redo 日志参数改名,老写法会被静默忽略 |
机械盘不是不能跑,只是导入时间会从一两个小时拉到十几个小时,中间任何一次中断都得重来。内存这块有个常见误区:机器 64 GB 就把 buffer pool 设成 60 GB,结果系统层和连接线程抢不到内存,导入到一半被 OOM killer 干掉。留出 25%~30% 给操作系统,是踩过坑之后相对稳妥的取值。
提示:如果目标机器只能装 5.7,先别急着导。8.0 的 dump 头部通常会写
utf8mb4_0900_ai_ci,5.7 不认这个排序规则,会直接抛ERROR 1273 (HY000): Unknown collation,得先把文件里的排序规则整篇替换成utf8mb4_unicode_ci再导。
2.2 my.cnf 的五个参数:导入速度和稳定性的取舍
默认配置直接导 1700 万条,慢到你会怀疑机器坏了。原因不复杂:每次事务提交都刷盘、redo 日志太小导致频繁 checkpoint、单条 INSERT 批量一大就撞上包大小上限。下面这份是我在导入期用的基线,装完再逐条改回去。
[mysqld] # 导入期基线:先保证能装进去,装完再回调成生产值 innodb_buffer_pool_size = 24G # 约为物理内存的 60%~70% innodb_redo_log_capacity = 4G # 8.0.30+ 用这个;老版本写 innodb_log_file_size = 2G innodb_flush_log_at_trx_commit = 2 # 导入期放宽,装完立刻改回 1 max_allowed_packet = 512M # 单条批量 INSERT 大时的硬门槛 innodb_flush_method = O_DIRECT # 绕过页缓存,避免双份内存占用 character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION逐个说清楚为什么这么写。innodb_buffer_pool_size是影响最大的一项,索引构建阶段需要把待排序的数据尽量放在内存里,给足了导入速度能差出三四倍。innodb_redo_log_capacity在 8.0.30 之后取代了innodb_log_file_size,如果你照着老文章写innodb_log_file_size,8.0.32 上启动不报错但也不生效,属于典型的「配置了等于没配」。innodb_flush_log_at_trx_commit = 2把每次提交的刷盘改成每秒一次,导入快很多,代价是机器断电可能丢最后 1 秒的数据——导入期间可以接受,导完必须改回 1,不然生产环境天天在赌运气。
max_allowed_packet要客户端和服务端两边都放大。只改服务端,客户端仍然会在读到超长语句时断开,报的是ERROR 2006 MySQL server has gone away,看起来像服务端挂了,其实是客户端自己先放弃。sql_mode这一行是故意不带NO_BACKSLASH_ESCAPES的,原因在第 4 章展开,这是整套 LaTeX 公式能不能保住的关键。
2.3 建库、建账号:排序规则选错后面全白干
库不要用默认字符集建。MySQL 8.0 的默认已经是 utf8mb4,但很多镜像和托管服务仍然保留着 latin1 或者 utf8mb3 的历史配置,一旦库建成 utf8mb3,四字节字符写不进去,公式里的某些符号和生僻字会在插入时被截断,而且严格模式下才报错,非严格模式下直接丢字符。
-- 库级别的字符集和排序规则要显式写死,不吃服务器默认值 CREATE DATABASE k12_bank DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; -- 应用账号只给增删改查,导入用 root 或单独的迁移账号 CREATE USER 'bank_rw'@'10.%' IDENTIFIED BY '换成你自己的口令'; GRANT SELECT, INSERT, UPDATE, DELETE ON k12_bank.* TO 'bank_rw'@'10.%'; FLUSH PRIVILEGES;utf8mb4_0900_ai_ci是 8.0 才有的排序规则,基于 Unicode 9.0,中文按通用规则排序,ai表示重音不敏感、ci表示大小写不敏感,对题库这种以中文检索为主的场景够用。要做严格区分大小写的标签匹配,就单独在那一列上写COLLATE utf8mb4_0900_as_cs,不要动库级别。应用账号用网段通配10.%而不是%,少一个被扫的风险点;导入用单独的账号,导完可以删,权限收敛在这一步做比事后补要省事。
3. 导入 1700 万条:把一个大 SQL 拆成能跑完的批次
3.1 导入前的四道保险
这一步大多数人会跳过,然后在导入到第 40 分钟时被迫从头再来。四件事,做完再敲回车。
第一,校验文件完整性。网上下来的大文件在传输中被截断是常事,md5sum对一遍,或者至少确认文件尾部有完整的-- Dump completed收尾注释。少了这一行,说明文件本来就是断的,导进去也是半个库。
第二,看 dump 头部的 SET 语句。前 60 行里通常有SET NAMES、SET @@SESSION.SQL_MODE、SET FOREIGN_KEY_CHECKS之类的声明,这些声明会覆盖你 my.cnf 里的设置。有些 dump 会写SET @@sql_mode = 'NO_BACKSLASH_ESCAPES',这一行就是公式杀手。
第三,关掉 binlog。导入 1700 万条会产生几十 GB 的 binlog,除非这台机器是从库需要同步,否则纯属浪费 IO 和磁盘。会话级执行SET sql_log_bin=0即可,需要 SUPER 权限。
第四,先用小表试导一次。挑一张行数最少的表单独导,确认字符集、排序规则、字段类型都对得上,再上主表。这一步花五分钟,能省掉两小时。
3.2 用 mysql 客户端导入:参数怎么给
不推荐用图形客户端导大文件,一是看不到详细报错,二是断线重连的行为不可控。命令行最稳。
# 1) 先确认 dump 里到底声明了什么,别盲目相信默认值 head -n 60 k12_mysql.sql | grep -Ei 'character_set|collate|sql_mode|names' # 2) 正式导入:把会话级参数拼在 SQL 前面一起喂进去 { echo "SET sql_log_bin=0;" echo "SET unique_checks=0;" echo "SET foreign_key_checks=0;" cat k12_mysql.sql } | mysql \ --default-character-set=utf8mb4 \ --max-allowed-packet=512M \ -h 127.0.0.1 -P 3306 -u root -p \ k12_bank--default-character-set=utf8mb4是客户端侧的声明,告诉服务端「我发过来的字节按 utf8mb4 解释」,这一项漏了就会出现中文变问号。--max-allowed-packet是客户端侧的上限,必须和服务端max_allowed_packet对齐或者更小。中间那三行 SET 只影响当前这条连接,不改全局,所以安全:unique_checks=0让导入时跳过唯一性校验,等导完再统一验证;foreign_key_checks=0允许子表先于父表灌入,题库这种题目表、选项表、知识点关联表互相引用的结构,不关外键检查几乎导不进去。
想看进度就装个pv,把cat换成pv k12_mysql.sql,能实时看到已经喂进去多少 MB,估算剩余时间。别小看这个,1700 万条的导入过程没有进度反馈,很容易在 40 分钟时误以为卡死然后 Ctrl+C。
注意:用管道的时候,
mysql进程失败退出,前面的cat可能是正常结束的,整条管道的退出码默认取最后一个命令。脚本里记得加set -o pipefail,否则导入失败也会显示成功。
3.3 中途断了怎么续:按表切分再灌
1700 万条一口气导,中间断的概率不低:磁盘满了、连接超时、机器重启。断了之后整个文件从头再来,成本太高。我的做法是先按表把大文件切碎,再一张表一张表灌,断了只补那一张。
# 按 dump 里的表结构注释切分,一个表一个文件 mkdir -p split awk '/^-- Table structure for table `/{ match($0, /`[^`]+`/); tbl = substr($0, RSTART+1, RLENGTH-2); f = "split/" tbl ".sql"; print "SET FOREIGN_KEY_CHECKS=0;" > f; } f { print >> f }' k12_mysql.sql这段 awk 的逻辑是:遇到-- Table structure for table \xxx`这种注释行,就从反引号里抠出表名当作输出文件名,同时新文件第一行补一句关外键检查;剩下的行原样追加到当前文件。切完之后split/` 目录里每个文件对应一张表,文件名就是表名,出问题一眼能定位。
# 逐表导入,失败立刻停,别让错误一路滚下去 set -o pipefail for f in split/*.sql; do echo "==> 正在导入 $(basename "$f") $(date '+%H:%M:%S')" mysql --default-character-set=utf8mb4 --max-allowed-packet=512M \ -h 127.0.0.1 -u root -p k12_bank < "$f" \ || { echo "导入失败: $f"; exit 1; } done顺序上有个讲究:先导没有外键依赖的字典表(学科、年级、知识点),再导题目主表,最后导选项和关联表。虽然关掉了foreign_key_checks,物理顺序不影响成功与否,但导完立刻做行数对账时,父表先有数据能省掉一轮排查。另外,set -o pipefail放在循环外面,循环里任何一次 mysql 非零退出都会让整批停下,这比「导完了才发现有张表是空的」要好得多。
4. LaTeX 公式显示:反斜杠、字符集链路和索引的三处配置
4.1 反斜杠是第一个坑:sql_mode 决定公式的生死
数学题库里的公式长这样:$\frac{a}{b}$、$\sqrt{x^2+y^2}$。在 MySQL 的字符串字面量里,反斜杠是转义符。这意味着 dump 文件里如果写的是'$\frac{a}{b}$',MySQL 解析时会把\f当成一个不认识的转义序列,直接把反斜杠丢掉,存进去就变成$frac{a}{b}$。前端 MathJax 拿到这个东西,渲染出来是一堆斜体字母,而不是分数。
正确做法有两种:dump 里把反斜杠写成两个\\frac,导入时用默认转义模式;或者开启NO_BACKSLASH_ESCAPES,让反斜杠变成普通字符。两种都不能选错,也绝对不能混用——dump 按双反斜杠写,导入时又开了NO_BACKSLASH_ESCAPES,存进去就是两个反斜杠,LaTeX 里\\是换行,公式照样是坏的。
-- 导入前看一眼当前 sql_mode,确认没有 NO_BACKSLASH_ESCAPES SELECT @@GLOBAL.sql_mode, @@SESSION.sql_mode; -- 抽样验证:匹配一个字面反斜杠,在 LIKE 里要写成四个 SELECT id, LEFT(stem, 100) AS sample FROM questions WHERE subject = 'math' AND stem LIKE '%\\\\frac%' LIMIT 5;第二段 SQL 的转义看着别扭,解释一下:MySQL 先解析字符串字面量,'\\\\'变成两个反斜杠;LIKE 又把这两个反斜杠解释成一个字面反斜杠去匹配。所以四个才能匹配到数据里的一个。如果这条查询返回 0 行,而表里明明有分数题,基本可以断定反斜杠在导入时被吃掉了。
4.2 连接层字符集:从 client 到 result 的四段链路
字符集不是「设一个地方就完事」。MySQL 有四个环节:客户端发过来的字节按character_set_client解释,服务端转成character_set_connection,存表时转成列的字符集,查询结果按character_set_results返回。四段里任何一段是latin1,中文和公式符号就会在那一环损坏。
-- 四段链路必须全是 utf8mb4 SHOW VARIABLES LIKE 'character_set%'; -- character_set_client / connection / results / database / server 都应为 utf8mb4# 应用侧连接串(Connector/J 8.x),三个参数别省 jdbc:mysql://10.0.0.12:3306/k12_bank ?characterEncoding=UTF-8 &connectionCollation=utf8mb4_0900_ai_ci &rewriteBatchedStatements=truecharacterEncoding=UTF-8在 Connector/J 8.x 里会映射到 utf8mb4,不需要再写useUnicode=true(8.x 已经废弃这个参数,写了反而在部分版本告警)。connectionCollation显式指定排序规则,避免服务端默认值和库级别不一致导致字符串比较行为漂移。rewriteBatchedStatements=true是给写场景的,批量插入题目时能把多次 round-trip 合并成一条多值 INSERT,速度差一个数量级。用 PHP 的 PDO 就写charset=utf8mb4在 DSN 里,别用SET NAMES手工发,那个不影响连接池里的已有连接。
4.3 题干字段的索引:什么时候该建,什么时候是自杀
题干列通常是text或longtext。直接ALTER TABLE ... ADD INDEX (stem)会失败,或者更糟——在部分配置下被当成前缀索引悄悄建出来,索引本身几百 GB,查询还根本用不上。题库的真实检索需求只有三类:按学科加年级筛、按知识点筛、按题干内容搜。前两类都是短字段,第三类才需要全文索引。
-- 学科、年级、难度这些短字段,走普通联合索引 ALTER TABLE questions ADD KEY idx_subject_grade_diff (subject, grade, difficulty); -- 题干前缀只用于「以某段开头」这种场景,24 个字符足够 ALTER TABLE questions ADD COLUMN stem_prefix varchar(64) GENERATED ALWAYS AS (LEFT(stem, 24)) STORED, ADD KEY idx_stem_prefix (stem_prefix); -- 中文题干搜关键词必须用 ngram 解析器,默认按空格分词对中文无效 ALTER TABLE questions ADD FULLTEXT KEY ft_stem (stem) WITH PARSER ngram;生成列stem_prefix用STORED而不是VIRTUAL,前者占磁盘但查询时不用现算,题库是读多写少的场景,值这个空间。WITH PARSER ngram是关键,MySQL 默认的全文分词器按空格和标点切,一整个中文句子会被当成一个词,搜「二次函数」什么都搜不到;加上 ngram 之后按ngram_token_size(默认 2)滑动切分,两个字以上的中文词才命中得了。这个索引建在 1700 万行的表上要跑挺久,建议等数据全部导完、行数对账无误之后再建,边导边建会拖慢整个导入过程。
5. 常见问题排查:导入这套题库最容易翻车的五个现场
现象一:导入到一半报ERROR 2006 (HY000): MySQL server has gone away,客户端还在,服务端日志没异常。
原因基本是max_allowed_packet不够。dump 里一条批量 INSERT 可能带上千行题目,拼起来的语句超过包上限,服务端直接掐断连接。注意这个参数有两份:服务端一份,客户端一份,只改一边等于没改。
解决:服务端max_allowed_packet=512M写进 my.cnf 并重启,客户端命令行显式加--max-allowed-packet=512M。改完先在 dump 里用grep -n 'INSERT INTO' | awk '{print length}' | sort -rn | head看最长的一条有多少字节,心里有个数。
现象二:中文全变成问号???,但表结构和行数都对。
原因多半是客户端连接字符集不对。--default-character-set没写,客户端按系统 locale 猜,服务端收到字节后按character_set_client解释,猜错就是一堆问号。还有一种情况是库建成 utf8mb3,四字节字符写入时被截断。
解决:SHOW VARIABLES LIKE 'character_set%'看四段链路,全部改成 utf8mb4;库用ALTER DATABASE k12_bank CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci修,但已经损坏的存量数据修不回来,必须重导。
现象三:数学题库里公式的分数、根号全乱了,\frac变成frac,或者变成两个反斜杠。
原因是sql_mode和 dump 的转义约定不匹配,具体机制在第 4 章讲过。这个坑最阴的地方在于导入过程零报错,数据能查出来,只是内容不对。
解决:先SELECT @@sql_mode确认没有NO_BACKSLASH_ESCAPES,再按 4.1 的 LIKE 查询抽样验证。已经导坏的表没有就地修复的捷径,老老实实 truncate 重导,别想着用REPLACE批量补反斜杠,容易把本来正确的数据改坏。
现象四:导入跑了十几个小时还没结束,SHOW PROCESSLIST显示一直在creating sort index或者update。
原因通常是三个叠加:innodb_flush_log_at_trx_commit还是默认的 1,每次事务提交都刷盘;binlog 没关,写入量翻倍;二级索引在导入过程中同步构建,每插一行都要维护索引。
解决:会话里关 binlog,把innodb_flush_log_at_trx_commit临时改成 2,导完再改回 1;如果表上已经有大量索引,先DROP掉非唯一索引,数据灌完再统一建。删索引前记得把建索引的语句导出来存一份,不然重建时对不齐字段。
现象五:COUNT(*)的数比预期少了两三百万,但导入过程没有任何报错。
原因是 dump 文件本身被截断,或者管道里前面的命令失败却没被察觉。如果你用的是cat file | mysql,cat读到文件末尾就正常退出,mysql 收不到完整数据也不会报错,整条管道的退出码还是 0。
解决:导入脚本里加set -o pipefail;导完立刻和 dump 文件里的INSERT行数做一次粗对账;确认文件尾部有完整的-- Dump completed注释。这套题库是离线资源,重新拿一份比修补一份残库要省时间。
6. 装完怎么验证:三条对账 SQL 和一次抽样渲染
导入成功的标志不是「mysql 没报错」,而是「数据能对得上、公式能渲染出来」。我一般跑三步,缺一步都不算装完。
-- 1) 总量对账:各表行数和 dump 里的规模量级对得上 SELECT 'questions' AS tbl, COUNT(*) AS rows_cnt FROM questions UNION ALL SELECT 'options', COUNT(*) FROM options UNION ALL SELECT 'knowledge_points', COUNT(*) FROM knowledge_points; -- 2) 分学科对账:数学题库、物理题库各自的行数是否合理 SELECT subject, COUNT(*) AS cnt FROM questions GROUP BY subject ORDER BY cnt DESC; -- 3) 公式完整性:$ 个数为奇数的行,基本就是被截断或转义坏掉的 SELECT COUNT(*) AS broken_formula FROM questions WHERE (LENGTH(stem) - LENGTH(REPLACE(stem, '$', ''))) % 2 = 1;第二条按学科分组,看的是分布有没有异常。如果语文、英语加起来占了九成,数学和物理只有零星几千条,那多半是导入时某张分表被跳过了。第三条是我自己加的土办法:行内 LaTeX 公式用$...$包裹,正常的题干$一定是偶数个,奇数个意味着有一半公式没闭合,通常是反斜杠转义出问题把后面的美元符号吃掉了。这条查询在 1700 万行上会扫全表,加个LIMIT或者放到从库上跑。
最后一步是抽样渲染,SQL 查不出来但前端一定会现原形。随机取 20 条数学题、10 条物理题,把题干字段原样丢给前端的渲染组件,肉眼过一遍:分式是不是真的上下结构、根号有没有罩住内容、上下标有没有掉下来。我见过最隐蔽的一次是\text{}里的中文渲染成了空白,SQL 层面完全正常,只有在浏览器里才看得出来。
| 检查项 | 通过标准 | 不通过先查什么 |
|---|---|---|
| 总行数 | 与 dump 规模同量级 | 文件完整性、pipefail |
| 学科分布 | 数学、物理占比正常 | 分表是否漏导 |
| 公式闭合 | broken_formula接近 0 | sql_mode、反斜杠转义 |
| 抽样渲染 | 分式根号正常显示 | 前端渲染组件、字段截断 |
从那以后我每次导完这套库,不管多急都强制走一遍这三条 SQL 加十分钟抽样渲染,确认没问题才把连接串交给应用侧。数据导错这件事最贵的地方不在重导,而在你根本不知道它是错的。希望帮到你。
本文还有配套的精品资源,点击获取