
简介MySQL 第五版配套的 samp_db 样例数据库 ZIP 包面向数据库课程学习者、MySQL 初学者及需要教学演示环境的教师解决缺少可直接导入、可反复练习的示例库问题。压缩包体积约 162KB上游暂未标注文件总数与类型明细具体内容以实际解压为准体量轻便适合课堂演示、课后练习与本地环境快速搭建。资源围绕 samp_db 样例库展开可用于熟悉建表、主外键关联、索引设计以及多表 JOIN、GROUP BY/HAVING 聚合查询、子查询等 SQL 练习也能帮助理解存储过程、触发器、视图和事务在业务表结构中的应用。目前已有 170 人学习适合作为 MySQL 5.x 特性入门与查漏补缺的辅助材料。使用时应遵守版权规定尊重所有者权益。1. 一份二十来张表的样例库为什么值得单独下载装完 MySQL 之后最难受的阶段不是不会写 SQL而是写出来没数据可查。手敲几行INSERT造出来的表十有八九只有干净漂亮的整数和英文名跑GROUP BY看不出分组差异跑EXPLAIN看不到索引效果跑LEFT JOIN也永远遇不到 NULL。这份 mysql sampdb 样例库的 ZIP 包解决的正是这个「空库焦虑」解压—建库—导入之后你手上就有了一套带真实数据分布的关系型库学生、课程、成绩、考试事件、缺勤几张表互相挂得住字段类型覆盖 DATE、ENUM、可空数值、变长字符串拿它练连接查询、聚合、日期区间过滤、索引验证都能跑出结果。它适合三类人刚学完语法想找数据集练手的新手、要给团队做 SQL 规范演示的工程师、需要一份稳定可复现的测试库来做执行计划对比的人。不适合指望它当生产数据模型的人——这是教学库字段设计刻意保留了粗糙感正好用来练改表。2. 先摸清 sampdb 的表结构七张表的分工与字段选型拿到压缩包别急着source先花二十分钟把表关系看明白。这套库的价值不在数据量而在结构上的「教学意图」每张表都对应一类典型查询场景字段类型也不是随手写的。2.1 七张表各管什么不同版本的脚本字段名会有出入但主体结构稳定通行划分如下表名主键数据量级用途studentstudent_id几十行学生档案姓名、性别、生日、地址、电话coursecourse_id十几行课程目录开课院系与课程名grade_eventevent_id十几行考试事件挂课程、日期、类别scorestudent_id event_id数百行成绩明细含 NULL 缺考absencestudent_id date几十行缺勤记录只有日期没有原因membermember_id十几行独立单表练增删改与排序president姓氏字段四十行左右独立单表练字符串函数与多列排序前五张是有外键关系的核心区后两张是完全独立的孤岛表。这个划分很实用练连接时只碰前五张练单表操作时不带任何 JOIN互不干扰。导入前先DESC student;看一眼实际字段比照脚本对一遍能避免后面写 SQL 时字段名对不上白折腾。2.2 字段类型里藏着的考点student.birth用的是 DATE 而不是 VARCHAR这一条就决定了你后面必须写BETWEEN 1990-01-01 AND 1999-12-31而不是字符串模糊匹配。student.sex用 ENUM(M,F)看着省空间实际上是个反面教材——真实业务里性别取值会扩展改一次要ALTER TABLE重建约束。拿它练ALTER TABLE ... MODIFY COLUMN正合适。score.score允许 NULL这是整套库里最有价值的设计。缺考和考 0 分是两回事AVG()会自动跳过 NULLCOUNT(*)却会把缺考算进去。这两个函数的差异用自己造的数据很难体会到用这套库跑一次就记住了。absence.date和grade_event.date都是 DATE 且没有时间部分做区间查询时不会遇到 datetime 截断的玄学问题。2.3 为什么拿样例库练手比自己造数据划算自己造数据最大的问题是分布太均匀成绩都是 60 到 100生日都在同一年地址都是「北京市」。这种数据跑GROUP BY出来的每组行数一样ORDER BY排完看不出区别EXPLAIN里优化器选什么执行计划全凭运气。样例库的数据是「脏」的有满分的、有 0 分的、有 NULL 的生日跨了二十年城市有重复有缺失。这种分布能跑出真实的聚合差异也能让索引效果显现出来——在几百行数据上EXPLAIN虽然不一定走索引但至少能看到type从ALL变成range的过程。练手阶段数据的「不干净」比「规整」有用得多。3. 从 ZIP 到可查询解压、建库、导入、校验四步落地这一步是整个流程里最容易翻车的地方。不是命令难是环境差异大Windows 的路径分隔符、MySQL 5.7 和 8.0 的字符集默认值、客户端和服务器版本不一致任何一个都能让导入卡在半路。3.1 解压前先看压缩包里有什么别直接双击解压先用命令行列一下目录树确认里面是单个.sql文件还是一堆分卷脚本# -l 只列出内容不解压先看清楚再动手 unzip -l sampdb.zip如果输出里有sampdb.sql、create_tables.sql、insert_data.sql这类多个文件说明脚本是拆开的必须按建表在前、插数据在后的顺序执行。如果只有一个大脚本后面直接整体导入即可。确认清楚之后再解压到一个没有中文和空格的路径# -o 覆盖已存在文件-d 指定目标目录 mkdir -p ~/dbtest/sampdb unzip -o sampdb.zip -d ~/dbtest/sampdb ls -lh ~/dbtest/sampdb-o的作用是无提示覆盖重复解压时不用手动确认。目标目录刻意选在用户主目录下是为了避开 Windows 上C:\Program Files这类需要管理员权限的路径——SOURCE命令在权限不足时会静默失败报错信息还特别模糊。提示解压后先file sampdb.sql看一眼编码。如果是 ISO-8859 或 GBK导入前必须转成 UTF-8否则中文姓名会全变问号而且这个错误在导入阶段不会报等查询时才发现。3.2 建库语句里的字符集与排序规则不要用CREATE DATABASE sampdb;裸建库字符集跟着服务器默认走换个环境结果就不一样。显式指定字符集和排序规则是让这套库可复现的第一步-- 8.0 环境utf8mb4 0900 排序规则 CREATE DATABASE IF NOT EXISTS sampdb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; -- 确认结果Character_set_database 必须是 utf8mb4 SHOW CREATE DATABASE sampdb;utf8mb4和老的utf8实际是 utf8mb3差别在三字节和四字节字符样例库里虽然大概率没有生僻字但养成习惯比事后救火便宜。utf8mb4_0900_ai_ci是 8.0 才有的排序规则ai表示重音不敏感、ci表示大小写不敏感。如果目标环境是 5.7这句会直接报错换成utf8mb4_general_ci即可。先SELECT VERSION();确认版本再决定用哪个比导入失败后回头改库省事。3.3 两种导入方式命令行重定向与 source第一种是命令行重定向适合脚本化、可重复执行的场景# --default-character-set 必须显式指定否则客户端按系统编码解析脚本 mysql -h 127.0.0.1 -P 3306 -u root -p \ --default-character-setutf8mb4 \ sampdb ~/dbtest/sampdb/sampdb.sql-h 127.0.0.1而不是localhost是为了强制走 TCP 而不是 Unix socket行为更一致。--default-character-setutf8mb4是这个命令里最关键的一个参数它决定客户端怎么解析.sql文件的字节流漏掉它文件里的 UTF-8 中文会被当成 latin1 塞进表里存进去就是乱码事后CONVERT也救不回来。第二种是客户端内的SOURCE适合脚本路径有权限问题、或者想边导入边看报错的场景-- 先切库再执行SOURCE 是客户端命令不是 SQL 语句 USE sampdb; SOURCE /home/dev/dbtest/sampdb/sampdb.sql;SOURCE由 mysql 客户端自己解析所以路径要用客户端所在的机器的路径不是服务器的。Windows 下路径反斜杠要写成正斜杠C:/dbtest/sampdb/sampdb.sql否则会被当成转义符。这条命令在 GUI 工具里往往不支持别在图形客户端里试。3.4 导入后的四条校验导入命令没报错不等于数据进对了。跑完必须做四项检查每项针对一类典型失败-- 1. 表数量对不对缺表说明脚本中途断了 SHOW TABLES; -- 2. 主表行数是否合理0 行说明数据段没执行 SELECT COUNT(*) AS student_rows FROM student; -- 3. 统计缺考行验证 NULL 确实被保留而不是被转成 0 SELECT COUNT(*) AS null_scores FROM score WHERE score IS NULL; -- 4. 检查表文件和索引是否损坏 CHECK TABLE score;第一条看结构完整性第二条看数据完整性第三条专门验证 NULL 语义——如果这一步返回 0说明导入过程把 NULL 转成了 0 或者空字符串多半是脚本里用了INSERT ... VALUES时字段类型不匹配被强制转换。第四条CHECK TABLE在 InnoDB 上主要查页一致性日常很少出问题但导入中断过的库值得跑一次。再补一条字符集验证直接查一条带中文的记录SELECT student_id, name FROM student WHERE student_id 1;姓名显示正常就说明整条链路文件编码 → 客户端字符集 → 表字符集是通的。显示成???或乱码回到 3.3 检查--default-character-set。4. 用 sampdb 练真实查询连接、聚合、日期与索引验证数据进库之后重点是把它当成一个可反复折腾的沙盘。以下四类查询是我认为这套库最有训练价值的场景每一类都能对应到实际工作里的常见需求。4.1 三表连接出一张成绩单成绩单是最典型的「事实表挂两个维度表」结构score是事实student和grade_event是维度grade_event再挂course拿到课程名。-- 查某个学生的全部考试记录含课程名和日期 SELECT s.name AS 学生, c.course_name AS 课程, g.date AS 考试日期, sc.score AS 成绩 FROM student AS s JOIN score AS sc ON sc.student_id s.student_id JOIN grade_event AS g ON g.event_id sc.event_id JOIN course AS c ON c.course_id g.course_id WHERE s.student_id 3 ORDER BY g.date;四个表用三个JOIN串起来重点是ON条件的挂接顺序score同时持有student_id和event_id所以它是连接枢纽从它出发向两侧展开最自然。WHERE放在最后过滤单个学生ORDER BY g.date让结果按时间排列。如果换成LEFT JOIN从student出发能查出没参加任何考试的学生——这是JOIN和LEFT JOIN的经典差异用这套库一试便知。字段名如果和你的脚本对不上先DESC score;和DESC grade_event;看实际列名把ON条件里的字段换掉即可逻辑结构不变。4.2 聚合按课程算平均分NULL 不能当 0-- 同时统计总次数、有效成绩次数、平均分与极值 SELECT c.course_name AS 课程, COUNT(*) AS 记录数, COUNT(sc.score) AS 有效成绩数, ROUND(AVG(sc.score), 2) AS 平均分, MIN(sc.score) AS 最低分, MAX(sc.score) AS 最高分 FROM score AS sc JOIN grade_event AS g ON g.event_id sc.event_id JOIN course AS c ON c.course_id g.course_id GROUP BY c.course_id, c.course_name HAVING COUNT(sc.score) 2 ORDER BY 平均分 DESC;这里有两处值得盯住。COUNT(*)和COUNT(sc.score)的结果会不一样差值就是缺考人数因为COUNT(列名)跳过 NULL。AVG()同样跳过 NULL所以缺考不会被当成 0 分拉低平均分——这正是教学库想让你体会的语义。GROUP BY里同时写了c.course_id和c.course_name是因为开启ONLY_FULL_GROUP_BY8.0 默认开启后SELECT里的非聚合列必须全部出现在GROUP BY中。HAVING过滤分组后的结果和WHERE过滤行不是一回事。想验证 NULL 的影响把AVG(sc.score)改成AVG(IFNULL(sc.score, 0))再跑一次平均分会掉下来。这个对比做一遍比看十页文档管用。4.3 日期区间与索引EXPLAIN 看它到底走没走索引-- 给缺勤表的日期列建索引 ALTER TABLE absence ADD INDEX idx_absence_date (date); -- 写法一区间条件能用上索引 EXPLAIN SELECT student_id FROM absence WHERE date BETWEEN 2024-09-01 AND 2024-09-30; -- 写法二函数包裹列索引直接失效 EXPLAIN SELECT student_id FROM absence WHERE YEAR(date) 2024;对比两次EXPLAIN的type列第一条通常是range第二条是ALL全表扫描。原因是索引按列的原始值排序一旦在列上套函数YEAR(date)优化器没法用索引定位只能逐行算。这就是「不要在索引列上做运算」这条规则的实证来源不是背下来的教条。数据量只有几十行时优化器可能嫌走索引不划算而直接全表扫这属于正常现象。想看清差异可以临时关掉索引条件下推或者复制一份数据放大到几万行再测。判断标准始终是type和rows两列别看耗时——几十行数据的耗时全是噪声。4.4 改数据前先开事务练UPDATE和DELETE时最怕手一抖写漏WHERE。这套库虽然能重导但每次重导要几分钟不如养成开事务的习惯-- 显式开启事务确认影响行数后再决定提交还是回滚 START TRANSACTION; UPDATE score SET score score 5 WHERE event_id 1 AND score 60; -- 看看到底改了几行数字不对立刻回滚 SELECT ROW_COUNT() AS affected_rows; ROLLBACK; -- 确认无误就把 ROLLBACK 换成 COMMITROW_COUNT()返回上一条 DML 实际影响的行数是防止误改的最后一道闸。前提是表引擎为 InnoDBMyISAM 不支持事务ROLLBACK会静默变成空操作——这一点在导入老脚本时尤其要注意脚本里如果写了ENGINEMyISAM事务保护就是假的。用SHOW TABLE STATUS LIKE score;确认引擎再动手。5. 导入与使用中的高频翻车点五条排查记录下面五条都是我在不同环境里真实撞过的按「现象 → 原因 → 解决」写遇到同类问题时可以直接对号入座。5.1 导入报 ERROR 1064语法错误还是版本不对现象source执行到一半停住报ERROR 1064 (42000): You have an error in your SQL syntax指向的行看起来完全正常。原因九成是版本差异。脚本里写了utf8mb4_0900_ai_ci、CHECK约束、窗口函数这类 8.0 才有的语法在 5.7 上必然报错反过来 5.7 脚本里的某些写法在 8.0 严格模式下也会挂。另一种可能是用了 GUI 客户端执行SOURCE而该客户端不支持这条命令把整行当 SQL 发给了服务器。解决先SELECT VERSION();确认版本再打开脚本搜COLLATE、CHECK (、ROW_NUMBER这几个关键词判断脚本面向哪个版本。命令行客户端是唯一稳妥的执行入口图形工具只用来查看结果。5.2 中文姓名全变成问号现象导入无报错查询时姓名列显示???或æŽå°这类乱码。原因客户端字符集和文件实际编码不一致。mysql客户端默认按操作系统的 locale 解析输入流Windows 中文环境常是 GBK而脚本文件是 UTF-8字节流被错误解释后再存进表里损坏已经发生。解决导入命令加--default-character-setutf8mb4并且确认表本身也是utf8mb4。已经导坏的没法靠CONVERT修复只能DROP DATABASE重来——这也是为什么先建库、后导入的顺序不能反。注意SET NAMES utf8mb4;只影响当前会话的服务器端解释救不了已经被错误解析的输入流。它和--default-character-set作用在不同环节别混用。5.3 生日字段全是 0000-00-00现象student.birth查出来一片0000-00-00DATE函数算年龄全是负数。原因脚本里用了零日期占位而 8.0 默认sql_mode包含NO_ZERO_DATE和STRICT_TRANS_TABLES遇到零日期会告警甚至报错如果导入时用了非严格模式零值就被静默写入。解决两种路子。要么在导入会话里临时放宽SET SESSION sql_mode;导完再恢复要么把脚本里的0000-00-00批量替换成NULL语义上也更正确。我倾向后者因为零日期在后续任何日期计算里都是定时炸弹。5.4 建表报 1215 或 3730外键挂不上现象ERROR 1215 (HY000): Cannot add foreign key constraint或者 3730 提示被引用表不存在。原因三个常见起因——父表还没建就建子表父子表的字段类型或字符集不完全一致INT对INT UNSIGNED也会失败表引擎是 MyISAM根本不支持外键。解决按依赖顺序执行建表语句被引用的表在前。脚本里如果图省事统一写了SET FOREIGN_KEY_CHECKS0;导完务必改回 1 并手动验证一次关联查询否则脏数据会一直潜伏到某次 JOIN 才暴露。5.5 重复导入报主键冲突现象第二次执行脚本ERROR 1062 (23000): Duplicate entry 1 for key PRIMARY导入中断。原因脚本开头没有DROP TABLE IF EXISTS或者用了INSERT而不是INSERT IGNORE/REPLACE。教学脚本大多假设你在一个全新的空库里执行。解决别用--force硬压错误那会让数据处于半新半旧的混乱状态。正确做法是写一个重置脚本先DROP DATABASE IF EXISTS sampdb;再CREATE从零重建。下面一章就是这个脚本。6. 把它变成可复用的实验基线重置脚本与备份校验样例库真正的价值不是「能跑一次」而是「随时能回到同一个起点」。做执行计划对比、验证索引效果、给新人演示 SQL 注入都需要一个确定性的初始状态手动重导三次以上就该脚本化了。6.1 一键重置脚本#!/usr/bin/env bash # reset_sampdb.sh删库重建并重新导入保证每次实验从同一状态出发 set -euo pipefail DBsampdb SQL_FILE./sampdb.sql MYSQL_USERroot # 密码从环境变量读不写死在脚本里 : ${MYSQL_PWD:?请先 export MYSQL_PWD} mysql -u $MYSQL_USER -e DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB DEFAULT CHARACTER SET utf8mb4; mysql -u $MYSQL_USER --default-character-setutf8mb4 $DB $SQL_FILE # 行数对不上就让脚本以非零码退出方便接进 CI ROWS$(mysql -u $MYSQL_USER -N -B -e SELECT COUNT(*) FROM $DB.student;) if [ $ROWS -lt 10 ]; then echo 导入异常student 表只有 $ROWS 行 2 exit 1 fi echo 重置完成student 表 $ROWS 行set -euo pipefail让任何一步失败都立即中断避免在坏状态上继续操作。MYSQL_PWD通过环境变量传入而不是-p密码是因为命令行参数会出现在进程列表里被别人看到。-N -B去掉表头和边框输出的就是纯数字可以直接参与判断。最后的行数校验是整个脚本的意义所在——没有它脚本「执行成功」和「数据正确」是两件事。6.2 备份与恢复验证一次才算数有了干净基线备份就简单了# --single-transaction 让 InnoDB 在不锁表的情况下拿到一致性快照 mysqldump -u root --single-transaction --routines --triggers \ --default-character-setutf8mb4 sampdb sampdb_backup.sql # 恢复到一个临时库验证不要直接覆盖原库 mysql -u root -e CREATE DATABASE sampdb_check DEFAULT CHARACTER SET utf8mb4; mysql -u root --default-character-setutf8mb4 sampdb_check sampdb_backup.sql mysql -u root -e SELECT COUNT(*) FROM sampdb_check.score;--single-transaction只对 InnoDB 有效MyISAM 表依然会锁。备份文件不做一次恢复验证等于没备份——字符集、sql_mode、外键顺序任何一个环节出问题都只有到恢复那一刻才发现。6.3 把执行计划存成基线优化器行为会随版本、统计信息、数据分布变化同一句 SQL 今天走索引明天可能全表扫。我的习惯是在落地任何索引改动前先把关键查询的EXPLAIN结果导出存档改动后再导一次做 diff。EXPLAIN FORMATJSON输出更细适合做文本比对-- 存一份基线改动索引后再跑一次对比 type 和 rows EXPLAIN FORMATJSON SELECT s.name, AVG(sc.score) FROM student AS s JOIN score AS sc ON sc.student_id s.student_id GROUP BY s.student_id, s.name;这套库小rows的绝对值没意义但type从ALL变ref、key从 NULL 变成具体索引名这两个字段的变化是可靠的信号。早年我在一个测试环境上直接改索引改完查询变快了就收工结果上线后另一条慢查询冒出来——因为我从没在同一份数据上做过前后对比压根不知道优化器把另一条路径换掉了。从那以后我每次动索引都强制先把重置脚本跑一遍、导出基线、改完再导一次 diff两步都过了才提交。这份 sampdb 压缩包就是干这个用的它足够小重置只要几秒又足够真实能跑出你想看的那些差异。希望帮到你。本文还有配套的精品资源点击获取