Skip to content

01 · 连接数据库与准备练习数据 ​

对应第 1~3 章。本页为本站原创练习,后续单元都使用这里的三张表。返回书籍导读。

本单元的目标是建立一套能解释、能检查的数据模型。学完后,应能回答:一个报名记录为什么只存两个编号,为什么课程改名不会破坏报名记录,以及为什么同一门课程的现价与历史收入可能不同。先理解模型,再执行脚本,后面的连接、聚合和事务才有明确对象。

第 1 章:把业务信息组织成表 ​

数据库存放相关数据,表描述一类实体,行是具体记录,列约束记录的属性。主键唯一标识一行,外键表达表之间的引用关系。

课程报名可以拆成三张表:learners 保存学员,courses 保存课程,enrollments 保存报名关系。报名表的组合主键 (learner_id, course_id) 表示同一学员不能重复报名同一门课程。课程现价与报名实付金额分别保存,避免调价后历史收入跟着变化。

数据库、数据库软件和一条连接分别是什么 ​

名词本练习中的对应物它解决的问题
DBMSMySQL 服务端进程管理存储、执行 SQL、处理并发和权限
数据库showdocs_mysql_book为一组相关表和对象提供命名空间
表courses描述同一类对象的属性和约束
行编号 101 的课程表示某个具体对象
列title、price让相同属性可以统一检索、比较和计算
结构定义列名、类型、主键、外键规定哪些数据可以进入表
会话一次客户端连接保存当前库、事务状态和会话变量

同一个服务端可以有多个数据库;不同库可以各自有一张 courses。因此 showdocs_mysql_book.courses 比只写 courses 更明确。MySQL 中 CREATE SCHEMA 可作为 CREATE DATABASE 的同义写法,但讨论“表的 schema”时,通常是在说结构定义,要根据上下文判断。

为什么不把所有数据放进一张大表 ​

设想每条报名都保存“学员姓名、邮箱、课程名、课程价格”。陈一报名两门课程后,邮箱重复出现两次;修改邮箱时漏改一行,就会出现同一个人有两个不同邮箱的矛盾。课程尚未被报名时,这张报名大表又没有合适的位置存课程信息。

拆表后,学员资料只在学员表维护,课程资料只在课程表维护,报名表只记录两者的关联及报名当时的事实。查询时再通过编号连接。这里的拆分依据是实体和事实的职责,不是“列越少越好”;一个事实只有确定的归属,后续才容易维护一致性。

paid 是报名时实付金额,price 是课程当前标价。今天把课程 101 从 39 元调到 49 元,过去两次报名仍各付了 39 元。计算历史收入必须求和 enrollments.paid,不能拿当前价格乘报名人数。这是后续报表最重要的口径之一。

主键、唯一约束和外键 ​

主键必须唯一且非空。一张表只有一套主键,但这套主键可以由多列组成。报名表不要求 learner_id 单独唯一,也不要求 course_id 单独唯一;要求的是两列组合不重复。因此 (1,101) 和 (1,102) 可以共存,第二条 (1,101) 会被拒绝。

外键回答另一个问题:被引用的学员和课程是否存在。它不会自动帮你写连接查询,也不会默认级联删除。在本练习的定义下,存在报名引用时删除对应课程会失败。删除没有报名的课程则可能成功,是否允许还要由业务规则决定。

学员姓名不能代替编号:姓名会重名,也可能修改。即使当前三个人恰好不重名,也不能把当前数据的巧合当作长期约束。给邮箱加 UNIQUE 是否合理则取决于业务:允许共用邮箱的场景不应强加唯一性。

SQL 的四类常见动作 ​

SELECT 描述要得到什么结果,优化器决定具体访问路径;它不是要求你手写遍历每一行的循环。INSERT / UPDATE / DELETE 改变记录,CREATE / ALTER / DROP 改变结构,START TRANSACTION / COMMIT / ROLLBACK 控制一组修改的生效边界。后两类不能混为一谈:能回滚普通写入,不代表能回滚所有结构变更。

第 2 章:区分客户端和服务端 ​

MySQL 服务端管理数据并执行语句;mysql 命令行和 Workbench 都是连接它的客户端。安装客户端不代表已经启动了数据库服务。

已有本地实验实例时,在终端连接;把用户名换成实际实验账号,密码在提示时输入:

sh
mysql --host=127.0.0.1 --port=3306 --user=YOUR_LAB_USER --password

在客户端里检查版本和会话环境:

sql
SELECT VERSION(), CURRENT_USER(), DATABASE();
SELECT @@sql_mode, @@autocommit;
SHOW DATABASES;

DATABASE() 为 NULL 表示尚未选择默认数据库。连接失败时先检查服务、端口和账号;需要环境搭建步骤可阅读起步课程。

一条命令经过了哪些环节 ​

终端先找到 mysql 客户端程序,客户端再联系指定主机和端口,服务端验证账号,连接成功后才进入 SQL 会话。mysql --version 只说明客户端版本;SELECT VERSION() 才是当前连接服务端的版本。连接到错误实例时,SQL 仍可能成功,因此先核对目标比反复重试密码更重要。

--password 不带值会交互询问密码;不建议把真实密码直接写进命令行或文档。127.0.0.1 明确使用 TCP;某些系统中 localhost 会使用本地套接字,连接方式不同可能匹配不同的配置或账号。CURRENT_USER() 显示实际用于权限判断的账号,USER() 可帮助检查客户端提交的用户与来源。

现象说明下一步
找不到 mysql 命令客户端程序或 PATH 有问题检查安装位置,不要修改服务端账号
无法连接服务端主机、端口、服务或连接方式有问题核对目标地址和服务状态
Access denied已进入认证或授权阶段核对用户、来源主机与权限
No database selected连接成功但未选库执行 USE showdocs_mysql_book
Unknown database当前实例没有这个库或名称错误核对实例后查看 SHOW DATABASES

命令行与 Workbench 的执行习惯 ​

在 mysql 客户端中,语句通常以分号结束。出现 -> 提示符表示客户端仍在等待当前语句补完;检查分号、引号和括号。输入 \c 可取消尚未发送的语句。SQL 关键字可以写大写以便阅读,字符串统一用单引号,避免不同 sql_mode 对双引号产生不同解释。

在 Workbench 中,先建立连接并打开 SQL 编辑器,再选择默认 schema。执行选中语句与执行整个脚本是不同操作:包含多个实验时,先选中一个完整代码块,观察结果面板和错误面板,再继续下一块。错误发生在当前哪条语句,应以结果日志为准,不能只看最后一个结果集。

第 3 章:建立独立练习库 ​

以下脚本在专用实验实例中执行一次,需要建库、建表和写入权限。库名已存在时应先核对用途;不要删除未知数据库来重跑脚本。只读练习可以重复运行,写入练习按各页说明回滚或清理。

sql
CREATE DATABASE showdocs_mysql_book
  CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE showdocs_mysql_book;

CREATE TABLE learners (
  learner_id INT PRIMARY KEY,
  learner_name VARCHAR(40) NOT NULL,
  email VARCHAR(100) NULL
) ENGINE=InnoDB;

CREATE TABLE courses (
  course_id INT PRIMARY KEY,
  title VARCHAR(80) NOT NULL,
  category VARCHAR(30) NOT NULL,
  price DECIMAL(8,2) NOT NULL,
  summary TEXT NOT NULL
) ENGINE=InnoDB;

CREATE TABLE enrollments (
  learner_id INT NOT NULL,
  course_id INT NOT NULL,
  paid DECIMAL(8,2) NOT NULL,
  enrolled_at DATE NOT NULL,
  PRIMARY KEY (learner_id, course_id),
  FOREIGN KEY (learner_id) REFERENCES learners (learner_id),
  FOREIGN KEY (course_id) REFERENCES courses (course_id)
) ENGINE=InnoDB;

INSERT INTO learners (learner_id, learner_name, email) VALUES
  (1, '陈一', 'chen@example.test'),
  (2, '林二', NULL),
  (3, '周三', 'zhou@example.test');

INSERT INTO courses (course_id, title, category, price, summary) VALUES
  (101, 'SQL 入门', '数据库', 39.00, 'Learn SQL queries and filtering'),
  (102, 'MySQL 事务', '数据库', 79.00, 'Learn MySQL transactions and locking'),
  (103, 'Linux 入门', '运维', 49.00, 'Learn Linux commands and files'),
  (104, 'SQL 报表', '数据库', 59.00, 'Learn SQL grouping and reports');

INSERT INTO enrollments (learner_id, course_id, paid, enrolled_at) VALUES
  (1, 101, 39.00, '2026-10-01'),
  (1, 102, 79.00, '2026-10-02'),
  (2, 101, 39.00, '2026-10-03');

SHOW TABLES;
SHOW CREATE TABLE enrollments;
SELECT COUNT(*) AS enrollment_count, SUM(paid) AS revenue FROM enrollments;

预期有 3 张表、3 位学员、4 门课程和 3 条报名记录,报名总收入为 157.00。DECIMAL 保存固定小数位金额;email 允许 NULL,表示邮箱未知。

逐段理解建表脚本 ​

CREATE DATABASE 建立命名空间,USE 只改变当前连接的默认数据库,不会切换其他连接。CHARACTER SET utf8mb4 规定默认字符编码,COLLATE 规定默认比较规则;具体差异在第六单元实验。

INT 适合本练习的小规模编号。VARCHAR(40) 限制字符数,不等于所有字符固定占 40 字节。DECIMAL(8,2) 总共最多 8 位十进制数字,其中 2 位在小数点后;FLOAT 的二进制近似表示不适合直接替代本例的精确金额。DATE 保存日期,不包含时分秒。

NOT NULL 规定必须有值,但不会自动禁止空字符串。email NULL 则明确允许“尚不知道邮箱”。外键列的类型应与被引用列兼容;这里全部使用同样的 INT。先建立学员和课程,再建立报名关系,插入数据也遵循先父记录、后子记录的顺序。

使用 InnoDB 是为了获得事务和外键能力。表名、列名在整个系列中统一小写,减少不同操作系统下表名大小写规则造成的差异。不要把“在本机能运行”推断成“改名大小写后到所有环境都能运行”。

把基线数据看清楚 ​

后续所有固定结果都以以下三张表为起点。先在脑中手算,再用 SQL 验证。

learner_idlearner_nameemail
1陈一chen@example.test
2林二NULL
3周三zhou@example.test
course_idtitlecategoryprice
101SQL 入门数据库39.00
102MySQL 事务数据库79.00
103Linux 入门运维49.00
104SQL 报表数据库59.00
learner_idcourse_idpaidenrolled_at
110139.002026-10-01
110279.002026-10-02
210139.002026-10-03
sql
SELECT 'learners' AS object_name, COUNT(*) AS row_count FROM learners
UNION ALL
SELECT 'courses', COUNT(*) FROM courses
UNION ALL
SELECT 'enrollments', COUNT(*) FROM enrollments;

DESCRIBE courses;
SHOW CREATE TABLE enrollments;
SELECT learner_id, course_id, paid, enrolled_at
FROM enrollments ORDER BY learner_id, course_id;

这里先使用 UNION ALL 把三个计数合并展示,其完整语义在第三单元解释。DESCRIBE 适合快速查看列;完整约束、引擎和默认值以 SHOW CREATE TABLE 为准。COUNT(*) 是实际行数,不能用某些元数据面板中的估计行数替代。

练习库已存在时不要直接重复执行全部初始化脚本:再次建库会报已存在,重复插入会触发主键冲突。回滚能恢复本系列多数写入实验;已经提交的数据和 DDL 则应按对应小节的清理步骤处理。若确实要重置,先确认它是自己创建的专用练习库,再重新建库导入,不能对工作库照做。

自测 ​

练习一:修改课程名会不会改变报名归属? 报名表保存的是课程编号,因此改名后仍关联同一课程。查询显示的新名字来自课程表;历史实付金额仍来自报名表。这同时说明,若业务要求保留“当时的课程名”,需要额外设计历史快照,当前模型没有声称具备这个能力。

练习二:下面三种记录哪些应被拒绝? 第二条 (1,101) 报名、学员 999 的报名、学员 3 报名课程 104。前两种分别违反主键和外键;第三种在现有约束下合法。先根据约束判断,不必为了检验而留下额外数据。

练习三:为什么报名人数不是 3? 报名表有 3 行,但实际参加过报名的学员只有陈一和林二,共 2 人。行数、不同学员数和学员总数是三种口径,分别为 3、2、3。下一步可以用第二单元的去重观察这一点,第三单元再用聚合计算。

练习四:在两个窗口分别连接数据库,只在一个窗口执行 USE,另一窗口的 DATABASE() 会变化吗? 不会,因为默认库属于会话状态。先在每个窗口核对连接目标,再执行只读检查;这也为后面的双会话事务实验打基础。

能准确解释三张表、每个约束和这四道题后,再进入查询部分。仅得到“脚本执行成功”,还不等于理解了模型。

MySQL 官方入门教程 · 下一单元:检索与过滤

继续阅读