1. 为什么这两份SQL脚本是入门MySQL绕不开的起点
当年学MySQL的时候,几乎人手一本《MySQL必知必会》。这本书最大的特点是"边看边练",每一章都有能直接跑通的SQL示例。但示例要能跑,前提是有一份能复现出同样数据的环境。官方配套的create.sql和populate.sql就是干这个的:前者负责把六张演示表建出来,后者负责往表里塞进足够撑起全书示例的数据。可以说,没有这两份脚本,书里从SELECT到存储过程的每一章,你都只能干瞪眼。
很多初学者把这两份脚本当成"双击一下就跑完"的一次性工具,能跑通就不再管了。但恰恰是这两份脚本,藏着学习关系型数据库设计的最佳模板。我后来带过不少新人,发现凡是能把create.sql逐行讲明白的,SQL功底都不差;凡是只会照着执行、从没认真读过内容的,遇到自己建表就懵。这套结论我用了很多年,基本没失灵过。
这篇内容适合谁?刚入门MySQL、想跟着书里例子敲一遍代码的新手;想看看一套演示用业务库到底怎么设计的学习者;哪怕是有几年经验、但一直没认真读过一套完整建表脚本的人,也值得把这两份脚本从头到尾扫一遍,收获不会小。
1.1 没有这两份脚本,整本书的示例寸步难行
这本书的章节安排是从最简单的单表查询一路讲到存储过程、触发器、事务。每一章的示例都建立在同一套数据之上,如果每次都在临时造数据,不仅浪费时间,而且你练出来的查询结果跟书上的完全对不上,很容易卡在"我这结果到底对不对"的困惑里。
create.sql和populate.sql就是为这件事生的。它们构建了一套稳定的、已知结果的迷你业务库,让你每做完一个练习,都能拿自己的输出跟书上的示例输出直接对照。这就是为什么我强烈建议:每次开始一个新章节之前,如果上一章的操作改动了表结构或数据,就先重新执行一遍这两份脚本,让数据回到"出厂状态"。脚本里做了DROP TABLE IF EXISTS处理,就是为了让你能反复执行、反复重置。
1.2 create.sql和populate.sql的分工
简单来说,create.sql管结构,populate.sql管数据。
执行create.sql之后,你的库里会出现六张互相咬合的表,模拟的是一个挺典型的商品销售场景。执行populate.sql之后,这些表里就有了足够你折腾几周的测试数据。两者必须按顺序执行:先建表后填数,这一点在后面的章节里会看得更清楚。
我见过一种错误的用法:有人只跑create.sql,到了查询章节发现SELECT出来全是空表,然后开始怀疑自己SQL写错了。真相往往只是少跑了populate.sql。所以跑完两份脚本后,第一时间数一数每张表的行数,这是后面所有操作的前提。
1.3 适合什么人认真读这两份脚本
如果你是照着书学的顺序型读者,建议在学到第4章"检索数据"之前,先把脚本完整跑通。如果你已经学完一遍、正在复习,我更推荐你换个视角:别把它们当脚本,当成一份"别人写的优秀建表作业"来拆解。我后面会逐个表拆开讲,你对照自己手里的文件看,会发现一份演示脚本里其实藏着一整套正规库设计的讲究。
2. 建表脚本的核心设计:一个商品销售库的逐表拆解
逐行读create.sql,你会发现它没用任何花哨的语法,也没有故意炫技的写法,但每一处选择都有自己的道理。这种克制恰恰是它值得反复读的原因。
2.1 六张表各自管什么
先看总览,这六张表的职责可以用一张表说清楚:
| 表名 | 职责 | 关键字段 | 跟谁关联 |
|---|---|---|---|
| vendors | 供应商 | vend_id(主键)、vend_name、vend_address 等 | 被 products 引用 |
| products | 商品 | prod_id(主键)、vend_id(外键)、prod_name、prod_price | 引用 vendors |
| customers | 客户 | cust_id(主键)、cust_name、cust_contact 等 | 被 orders 引用 |
| orders | 订单主表 | order_num(主键)、order_date、cust_id(外键) | 引用 customers |
| orderitems | 订单明细 | order_num + order_item 联合主键、prod_id(外键) | 引用 orders 和 products |
| productnotes | 商品备注 | note_id(主键)、prod_id(外键)、note_text | 引用 products |
这六张表的组合不是随随便便挑出来的。vendors和products是一对多,一家供应商能供多个商品;orders和customers也是一对多,一个客户能下多个订单;orders和orderitems是一对多,一张订单里有多行商品明细;而orderitems通过prod_id又挂回products,等于在products和orders之间搭了一座桥,让"一个订单包含多个商品、一个商品出现在多个订单里"这种多对多关系有了落脚点。
后面你学JOIN、子查询、聚合函数、分组统计,所有示例几乎都是在这张表网里打转。如果这张网设计得不合理,你会发现自己做练习时总得穿越好几张表才能拿到想要的数据,学习成本会高出一大截。
2.2 联合主键与外键:orderitems表里藏着的设计课
这六张表里,我最想让初学者反复看的是orderitems表。它的主键不是单独的一列自增id,而是(order_num, order_item)联合主键。
这个设计很值得琢磨。订单明细天然就有"某个订单里的第几行商品"这种语义,用订单号加行号联合起来做唯一标识,既不会重复,又直接表达了业务含义,还省掉了一个没意义的代理主键列。我见过不少人学完这本书好几年,回头自己写订单明细表,还是习惯性加一行自增id。倒也不是不能用,但跟书里这个设计一比,差距就在思路:主键不是"表里必须有一列叫id",而是"到底哪一列或哪几列能唯一标识一行"。为了想明白这一点,这份create.sql就值得反复读。
外键方面的写法,不同流传版本会有差异。有的版本显式写了FOREIGN KEY约束,有的版本为了兼容老版本MySQL,只建索引没加约束。你拿到手的脚本以实际内容为准。我的建议是,如果手里是没有显式外键的版本,可以自己动手补上约束再跑一遍,亲自体会一下约束带来的保护和报错,这比干看书理解外键要深刻得多。
2.3 字段类型选择的讲究
create.sql里字段类型的选择也很有意思。products表的prod_price用的是DECIMAL(8,2)而不是FLOAT,这个细节非常关键。
金额这种东西,用FLOAT算会出现二进制浮点误差。你在MySQL里跑一下SELECT 0.1+0.2,结果可能不是0.3而是0.30000000000000004。做演示或许看不出大问题,但做金额计算,误差会一路累积。DECIMAL是定点数,按十进制位存储,做金额加减乘除是精确的。书里选择DECIMAL(8,2),等于在你还没学到"浮点数的坑"之前,就先帮你避开了。
此外,客户表里的地址、邮箱多用VARCHAR,长度都控制在够用的范围内,比如VARCHAR(50)。订单日期order_date用DATETIME。这些选择透露出的是一套"够用就好"的价值观:演示库不需要给每个字段预留无限长度,字段越短,索引越省空间,写入和查询的开销也越小。很多人生产环境里拍脑袋给VARCHAR(500),往往就是没看过这种克制设计的缘故。
create.sql的建表语句里,每个CREATE TABLE都提前用DROP TABLE IF EXISTS做清理。这里有一个顺序讲究:带外键的子表要排在父表前面删。比如先删orderitems、再删orders、最后删customers,这样才不会有外键悬空。脚本里通常已经按这个顺序排好了,但如果你以后自己写清理脚本,记得别把父子顺序搞反。
3. 填充数据的顺序逻辑与数据量设计
populate.sql看起来就是一长串INSERT语句,但它能讲出来的东西,比表面看到的要多得多。
3.1 为什么插入顺序被外键锁死
外键约束的核心含义是:子表里引用的ID,父表里必须已经存在。这个规则直接决定了populate.sql里的插入顺序。
正确的顺序是这样的:
- vendors —— 供应商没有被别人引用,可以先插;
- products —— 需要vend_id,所以必须在vendors之后;
- customers —— 没有依赖,插哪都行,一般跟着orders前面;
- orders —— 需要cust_id,所以必须在customers之后;
- orderitems —— 需要order_num和prod_id,所以在orders和products之后;
- productnotes —— 需要prod_id,所以在products之后。
你要是试着把第2步和第5步对调,在启用外键约束的版本里,MySQL会直接甩给你一个报错,内容类似"Cannot add or update a child row: a foreign key constraint fails"。这不是脚本坏了,是数据依赖没满足。
有意思的是,很多配套版本的populate.sql在文件组织上并没有刻意把这些表的INSERT分得泾渭分明,而是按文件的自然顺序平铺下来。但因为作者知道依赖关系,所以整体上文件顺序天然就是"先父后子"。你如果好奇为什么populate.sql的顺序长这样,答案就在这里。
换句话说:外键约束不只是写在建表语句里的一张"声明",它会在你插入、更新、删除时被MySQL强制执行。跑一遍populate.sql,等于用实际动作把这个规则复习了一遍。
3.2 每张表的数据量为什么是这么多
populate.sql里每张表的数据量都不是随手定的,我按常见版本大概说几个数:
- vendors:5个供应商;
- products:10个商品,恰好分布在5家供应商下,每家2个;
- customers:50个客户;
- orders:5到10条订单;
- orderitems:十几到二十几条订单明细;
- productnotes:3条商品备注,长短不一。
这些数字是有讲究的。
10个商品分给5家供应商,每家正好2个,这样你学GROUP BY的时候,按vend_id分组的统计结果非常规整,能一眼验证对错。50个客户这个量特别关键。书里讲ORDER BY排序、LIMIT分页、按州聚合、子查询找交集这些示例时,都需要一个"不大不小"的数据池。如果只有5个客户,很多查询一眼就能看穿结果,练习就失去了意义;而50个客户,正好让你在验证结果时既有一定工作量,又不至于眼花缭乱。
至于productnotes表,只有3条备注却故意让它们长短不一,这是为后面全文本搜索章节准备的。没有一条足够长的note_text,全文检索和LIKE的匹配效果就完全体现不出来。这些小设计,恰恰是练习数据最值得抄作业的地方。很多人给测试库随便怼几行数据,然后抱怨"单表查询看不出效果",其实就是数据量没设计好。
3.3 脚本里容易被忽略的存储过程段落
还要提醒一点:某些版本的populate.sql并不只是INSERT,文件靠后的部分可能会包含存储过程或者函数的定义,为书里后面讲过程化编程的章节做准备。如果你在文件里看到CREATE PROCEDURE、DELIMITER $$之类的内容,别慌,这很正常。
也正因为可能有这种多语句段落,执行populate.sql时最好用支持多语句的客户端工具。某些网页版的在线SQL执行器只允许一次跑一条语句,拿它跑这种脚本,大概率会卡在DELIMITER的解析上,然后你以为是脚本有问题,其实是工具不对。
4. 跑通脚本的实操全记录:环境、命令和五个高频报错
光看懂还不够,必须亲手跑通。这一节我把实际操作中会遇到的事情完整交代一遍。
4.1 执行前的三件准备
第一,MySQL服务必须活着。Windows上检查服务管理器里的MySQL服务,macOS上可以用brew services list看一下,先确保mysqld在运行。
第二,客户端能连上。命令行执行mysql -uroot -p,输入密码能进到mysql>提示符就行。连不上的话先排查端口、账号、密码,基础问题先解决。
第三,也是最容易被忽略的:要先确认这两份脚本默认用的是哪个数据库。市面上流传的《MySQL必知必会》配套脚本,有的开头自带CREATE DATABASE IF NOT EXISTS crashcourse,有的只写了USE crashcourse,还有的既不带建库也不带USE。第一次执行之前,花十秒钟把文件前几行打开看一眼,比盲目执行省事得多。
如果脚本里没有建库语句,你就手动补上:
CREATE DATABASE IF NOT EXISTS crashcourse DEFAULT CHARACTER SET utf8mb4; USE crashcourse;4.2 三种执行方式与我的建议
方式一:客户端内SOURCE。登录进MySQL之后,直接执行SOURCE /绝对路径/create.sql;。注意路径里的反斜杠在MySQL客户端里可能引发转义问题,Windows下也建议用正斜杠。
方式二:操作系统shell管道方式。在终端里执行mysql -uroot -p < create.sql。这种方式跟客户端里的SOURCE是两个环境,别搞混。
方式三:图形工具整段执行。Workbench、Navicat这类工具,直接打开SQL文件,执行整段脚本,报错信息能直接定位到某一行,对新手最友好。
我的建议是:前几遍用图形工具跑,因为能看到每一条语句的执行状态;熟练之后再切到命令行SOURCE,更贴近真实工作流。跑的时候,先执行create.sql,等它完全结束,再执行populate.sql,别两段连着一起跑,否则建表还没完成就开始插数据,容易出幺蛾子。
4.3 五个高频报错完整对照
跑脚本报错是家常便饭,关键是会看报错。我把高频问题整理成一张表:
| 报错内容 | 真正原因 | 解决办法 |
|---|---|---|
| ERROR 1046 (3D000): No database selected | 脚本没带USE语句,当前会话也没选中库 | 先建库并执行USE crashcourse;再跑脚本 |
| ERROR 1366 (HY000): Incorrect string value | 客户端和服务器字符集不一致,中文内容插入失败 | 连接后先执行SET NAMES utf8mb4;或者连接参数加--default-character-set=utf8mb4 |
| ERROR 1216 / 1452: Cannot add or update a child row | 插入顺序违反外键约束 | 按"先父后子"的顺序执行,或确认缺少依赖数据 |
| ERROR 1419: You may not create stored function | 服务器开了log_bin,创建存储函数权限受限 | 以root登录执行SET GLOBAL log_bin_trust_function_creators=1; |
| ERROR 1064: You have an error in your SQL syntax | 常见原因是文件被Windows记事本存成了带BOM的编码 | 用编辑器把脚本另存为UTF-8无BOM,再执行 |
前三个几乎是新手必踩。尤其是ERROR 1366,凡是脚本里带了中文内容而客户端字符集不匹配的时候就会出现。这个问题不是脚本本身坏了,而是连接环境的字符集没对齐,一条SET NAMES utf8mb4就能解决。
4.4 跑完之后怎么验证这没白跑
没有报错不等于成功。跑完populate.sql之后,最直接的验证是看看每张表到底有多少行:
USE crashcourse; SHOW TABLES; SELECT COUNT(*) FROM vendors; SELECT COUNT(*) FROM products; SELECT COUNT(*) FROM customers; SELECT COUNT(*) FROM orders; SELECT COUNT(*) FROM orderitems; SELECT COUNT(*) FROM productnotes;对照我前面说的量级,如果某张表是0行,基本可以断定中途报错中断了,脚本后面的INSERT没执行到。这种情况别想办法救,把脚本从头到尾重新完整跑一遍。
还有一种更贴近书中练习的验证方式:跑一个最小化的两表联结查询。只要商品和供应商这两张核心表的数据是通的,后面的章节基本不会有大问题:
SELECT prod_name, vend_name FROM products INNER JOIN vendors ON products.vend_id = vendors.vend_id ORDER BY prod_name;正常会返回10行商品,每行带着对应的供应商名称。看到这个结果,说明跑脚本这关过了。
5. 把这两份脚本用出花来:进阶练习与改造路线
脚本跑通只是起点。真正把这套数据价值榨干的方法,是把它当成一块反复打磨的试验田。
5.1 按章回炉:把脚本当成练习题场地
我的建议是不要把这两份脚本当成一次性启动器。每学到新章节,都回到这几张表上做延伸练习。学到ORDER BY,就试试对customers表按州和联系人排序;学到通配符LIKE,就在products表的prod_desc上练手;学到子查询,就拿"订购了某商品的所有客户"这种经典题目反复变着花样写。
等你把这些例子都跑熟了,再尝试一个更狠的练法:合上书,模仿create.sql的风格,从零设计一个属于你自己的小库,比如一个简单的借书系统,或者一个打卡系统。自己设计表结构、决定主外键、规划插入顺序,再写一套填充数据。这样走完一遍,你对这两份脚本的理解就不只是"能跑",而是"知道为什么这么写"。
5.2 用mysqldump反向拆解建表写法
这里有个进阶玩法。你在脚本建好的库里已经动手改过、加过自己的数据,这时候用mysqldump把当前表结构导出来,跟官方create.sql做对比:
mysqldump -uroot -p --no-data crashcourse > schema_my.sql打开导出的schema_my.sql,你会发现MySQL自己生成的语句多了很多反引号、表级选项、外键约束的完整声明。跟官方版本逐表对比,你能直观理解"SQL脚本有无数种写法,关键是语义一致"这件事。我自己当年就是靠这个对比,把外键约束到底在干什么彻底搞明白的。
5.3 三种改造方向:补外键、加索引、扩数据
方向一是补外键。如果手里的create.sql没有显式FOREIGN KEY,你可以手动加上,重新跑一遍,体验约束带来的保护,也体验违反约束时的报错。
方向二是加索引并研究执行计划。比如给orders表的order_date加个索引:
ALTER TABLE orders ADD INDEX idx_order_date (order_date); EXPLAIN SELECT * FROM orders WHERE order_date > '2005-01-01';对比加索引前后EXPLAIN结果里的扫描行数和type列,你对索引的理解会比看书深刻得多。
方向三是扩数据量。把50个客户扩展到500个,甚至把订单数据造到几千条,拿去做分页查询、性能对比和窗口函数练习,效果完全是另一个维度。数据一多,很多"书上说的性能问题"才真的浮出水面。
5.4 我踩过的三个坑
最后交代一点个人教训,全是真金白银换来的。
第一个坑是直接改原脚本。有一阵子我为了练手,直接在create.sql里增加了一个字段,结果后面章节的示例全乱套,因为书里的INSERT和查询都是按原字段名写的。正确做法是复制一份脚本再改,原版留着跑书上的例子。
第二个坑是学习过程中没重置数据。有些章节练习会修改表结构、删除数据或改动字段值,做完之后如果你不重置,下一章查询的结果就跟书上对不上。养成习惯:每次开新章节之前,把create.sql和populate.sql重新SOURCE一遍。反正有DROP TABLE IF EXISTS兜底,这套脚本天生就是为反复执行设计的。
第三个坑和编码有关。在Windows上如果脚本文件被记事本存成了带BOM的UTF-8,第一行会混进一个看不见的BOM字符,导致第一条语句解析报错。解决方案很粗暴但有效:用VS Code、Notepad++这类工具把文件另存为UTF-8无BOM。
5.5 从照搬到设计:一份属于你自己的create.sql
写到这,我想说一句实在话。很多人学完这本书,觉得"书看完了、例子跑完了",但一遇到自己建表就露怯。问题不在练习不够,而在没有真正消化这份脚本背后的设计决策。你能不能用今天拆解过的思路,重新审视甚至重新实现一份自己的"create.sql"?如果能,你的SQL水平就真的上了一个台阶。
我自己带新人的时候,特别喜欢把这份脚本打印出来,让新人逐行讲一遍每段CREATE TABLE的意图。能讲明白的,基本不需要再补基础;讲不明白的,回去翻书、照着脚本多练几遍。这套方法我用了很多年,效果一直很稳定。要说这套脚本最大的价值,其实不是让你学会了几个命令,而是让你第一次见到一份"正经业务库"应该长什么样。从这个角度看,它值得你反复翻出来品。