大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
关系型数据库与 SQL 是什么
这一课你将能做到
- 用自己的话说清楚表、行、列、主键、外键,并说出本站
cf_track/cf_lesson各对应什么 - 连上 MySQL 8,跑通一套贯穿全课程的建库 + 造数脚本,
SHOW TABLES能看到 5 张练习表 - 区分 DQL / DML / DDL 三类 SQL,并执行人生第一条
SELECT
为什么要学
CodeRoadMap 是标准的 Java 后端架构:Spring Boot 提供接口,MySQL 存数据。你在页面上看到的「课程列表」「课时侧栏」「学完打勾」,最终都是后端执行一句 SQL,把表里的行查出来,转成 JSON 返回给前端。
这意味着两件事:
- 排查「页面没数据」时,最快的验证方式不是翻 Java 日志,而是拿同样的条件去库里查一遍——数据在不在,一查便知。
- 读写接口的性能问题,十有八九落在 SQL 上。表设计烂、SQL 写烂,Java 层写得再漂亮也救不回来。
人话:关系型数据库就是一叠规则极严的表格;SQL 是你跟这些表格说话的语言。你说「把 category 是 database 的课程给我」,它就照办——你只描述要什么,不用写循环。
先建立图像
浏览器
↓ HTTP 请求 /api/tracks
Spring Boot(业务逻辑、权限校验)
↓ JDBC / MyBatis:把 SQL 发给数据库
MySQL(数据的真相:表里的行)
cf_user、cf_track、cf_lesson、cf_progress …
| 词 | 人话 | 本站对应 |
|---|---|---|
| 表 table | 一类东西的集合 | cf_track = 课程 |
| 行 row | 一条记录 | 一门「SQL 与关系型数据库」 |
| 列 column | 一个字段 | title、category、level |
| 主键 PK | 唯一认出这一行的身份证 | 自增 id |
| 外键 FK | 指向别的表主键的引用 | cf_lesson.track_id → cf_track.id |
Excel 也能画表格,为什么还要数据库?Excel 单元格想填什么填什么,找重复靠肉眼;数据库每列类型固定、行与行靠键关联、支持多用户并发写入、断电不丢数据、能建索引加速查询。网站要的恰恰是这些。
跟着做
本课程 10 课共用一套练习数据:一个迷你在线课程平台(学生、课程、课时、学习记录、订单)。本课建好后,一直用到最后一课。
步骤 1:连上 MySQL 8
本机装了 MySQL 8 的话,打开终端:
mysql -u root -p
输入密码进入 mysql> 提示符,先确认版本:
SELECT VERSION();
看到 8.x.x 就对了(本站后端用的就是 MySQL 8)。不想装本机,用 Docker 起一个也行:
docker run -d --name mysql8 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:8.0
docker exec -it mysql8 mysql -uroot -p123456
步骤 2:建练习库
DROP DATABASE IF EXISTS sql_practice;
CREATE DATABASE sql_practice DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sql_practice;
DROP 在这里是为了脚本可反复重跑:任何时候把数据玩坏了,回到第一课整段重跑,就回到初始状态。
步骤 3:建表 + 造数(全课程通用脚本)
下面脚本一次建 5 张表、插好数据,后面 9 课全用它。整段复制执行即可:
-- 学生表
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32) NOT NULL,
email VARCHAR(128) NOT NULL UNIQUE,
city VARCHAR(32) NOT NULL,
vip TINYINT(1) NOT NULL DEFAULT 0,
balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 课程表
CREATE TABLE course (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
slug VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(128) NOT NULL,
category VARCHAR(32) NOT NULL, -- java / frontend / database
level VARCHAR(16) NOT NULL DEFAULT 'beginner',
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
status VARCHAR(16) NOT NULL DEFAULT 'draft', -- draft / published
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 课时表(一门课有多节课)
CREATE TABLE lesson (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_id BIGINT NOT NULL,
title VARCHAR(128) NOT NULL,
order_index INT NOT NULL,
duration_min INT NOT NULL DEFAULT 20,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
CONSTRAINT fk_lesson_course FOREIGN KEY (course_id)
REFERENCES course(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- 学习记录表(谁在学哪节课、完成没有)
CREATE TABLE study_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_id BIGINT NOT NULL,
lesson_id BIGINT NOT NULL,
completed TINYINT(1) NOT NULL DEFAULT 0,
score INT DEFAULT NULL,
completed_at DATETIME DEFAULT NULL,
UNIQUE KEY uk_student_lesson (student_id, lesson_id),
CONSTRAINT fk_record_student FOREIGN KEY (student_id)
REFERENCES student(id) ON DELETE CASCADE,
CONSTRAINT fk_record_lesson FOREIGN KEY (lesson_id)
REFERENCES lesson(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- 订单表(ORDER 是 SQL 保留字,所以表名叫 pay_order)
CREATE TABLE pay_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'PAID', -- PAID / REFUNDED
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_order_student FOREIGN KEY (student_id) REFERENCES student(id),
CONSTRAINT fk_order_course FOREIGN KEY (course_id) REFERENCES course(id)
) ENGINE=InnoDB;
-- 造数:7 个学生
INSERT INTO student (id, name, email, city, vip, balance, created_at) VALUES
(1,'老陈','chen@example.com','深圳',1,128.00,'2026-07-01 09:00:00'),
(2,'小林','lin@example.com','杭州',0, 39.00,'2026-07-03 10:00:00'),
(3,'阿珍','zhen@example.com','广州',1,256.50,'2026-07-05 11:00:00'),
(4,'大鹏','peng@example.com','北京',0, 0.00,'2026-07-08 14:00:00'),
(5,'小夏','xia@example.com','深圳',0, 88.00,'2026-07-10 16:00:00'),
(6,'老周','zhou@example.com','上海',1,500.00,'2026-07-12 18:00:00'),
(7,'小吴','wu@example.com','成都',0, 20.00,'2026-07-15 20:00:00');
-- 造数:7 门课程
INSERT INTO course (id, slug, title, category, level, price, status, created_at) VALUES
(1,'java-fundamentals','Java 基础入门','java','beginner',199.00,'published','2026-06-01 10:00:00'),
(2,'java-oop','Java 面向对象进阶','java','intermediate',299.00,'published','2026-06-03 10:00:00'),
(3,'sql-essentials','SQL 与关系型数据库','database','beginner',0.00,'published','2026-06-05 10:00:00'),
(4,'mysql-advanced','MySQL 性能优化实战','database','advanced',399.00,'published','2026-06-08 10:00:00'),
(5,'html-css','前端页面基础','frontend','beginner',99.00,'published','2026-06-10 10:00:00'),
(6,'redis-intro','Redis 缓存入门','database','intermediate',0.00,'draft','2026-06-12 10:00:00'),
(7,'git-essentials','Git 团队协作基础','frontend','beginner',49.00,'draft','2026-06-15 10:00:00');
-- 造数:14 节课时
INSERT INTO lesson (id, course_id, title, order_index, duration_min, status) VALUES
(1,1,'环境搭建与第一个程序',1,25,'published'),
(2,1,'变量与数据类型',2,30,'published'),
(3,1,'流程控制',3,35,'draft'),
(4,2,'类与对象',1,40,'published'),
(5,2,'继承与多态',2,45,'published'),
(6,3,'关系型数据库与 SQL 是什么',1,25,'published'),
(7,3,'SELECT 基本查询',2,30,'published'),
(8,3,'WHERE 过滤与排序',3,30,'published'),
(9,3,'JOIN 多表联结',4,35,'draft'),
(10,4,'索引原理与 B+ 树',1,50,'published'),
(11,4,'执行计划 EXPLAIN 详解',2,55,'published'),
(12,5,'HTML 文档结构',1,20,'published'),
(13,5,'CSS 盒模型',2,28,'published'),
(14,6,'Redis 是什么',1,15,'draft');
-- 造数:17 条学习记录
INSERT INTO study_record (id, student_id, lesson_id, completed, score, completed_at) VALUES
(1,1,6,1,92,'2026-08-10 10:00:00'),
(2,1,7,1,88,'2026-08-11 09:30:00'),
(3,1,1,1,95,'2026-08-05 14:00:00'),
(4,1,2,1,90,'2026-08-06 15:20:00'),
(5,2,6,1,75,'2026-08-15 20:10:00'),
(6,2,12,1,80,'2026-08-16 21:00:00'),
(7,3,6,1,98,'2026-08-01 08:40:00'),
(8,3,7,1,95,'2026-08-02 09:00:00'),
(9,3,8,1,91,'2026-08-03 10:00:00'),
(10,3,10,1,85,'2026-08-09 11:00:00'),
(11,4,1,1,60,'2026-08-20 19:00:00'),
(12,4,6,0,NULL,NULL),
(13,5,7,1,83,'2026-08-25 16:30:00'),
(14,6,6,1,90,'2026-08-12 12:00:00'),
(15,6,10,1,88,'2026-08-13 13:00:00'),
(16,6,11,1,92,'2026-08-14 14:00:00'),
(17,6,4,1,85,'2026-08-18 15:00:00');
-- 造数:6 笔订单
INSERT INTO pay_order (id, student_id, course_id, amount, status, created_at) VALUES
(1,3,1,199.00,'PAID','2026-08-01 09:00:00'),
(2,1,4,399.00,'PAID','2026-08-02 10:00:00'),
(3,3,4,399.00,'PAID','2026-08-03 11:00:00'),
(4,6,5, 99.00,'PAID','2026-08-05 12:00:00'),
(5,2,1,199.00,'REFUNDED','2026-08-06 13:00:00'),
(6,5,5, 99.00,'PAID','2026-08-08 14:00:00');
步骤 4:检查建出来的东西
SHOW TABLES; -- 应看到 5 张表
DESC student; -- 看学生表的列定义
SELECT COUNT(*) FROM student; -- 7
SELECT COUNT(*) FROM lesson; -- 14
DESC(describe)会列出每列的名字、类型、是否允许 NULL、默认值。以后拿到任何陌生表,第一件事就是 DESC 它;更完整的信息用 SHOW CREATE TABLE student; 看。
步骤 5:执行第一条 SELECT
SELECT slug, title, category, price
FROM course
WHERE status = 'published';
应返回 5 行(7 门课里有 2 门是 draft)。这就是 SQL 的基本形状:从哪张表取(FROM)、要哪些列(SELECT)、要满足什么条件(WHERE)。
步骤 6:认识 SQL 三大类
| 类型 | 关键字 | 干什么 | 本课程节奏 |
|---|---|---|---|
| DQL 查询 | SELECT | 只读地问数据 | 第 2–5 课,重头戏 |
| DML 改数 | INSERT UPDATE DELETE | 增删改数据 | 第 6 课 |
| DDL 改结构 | CREATE ALTER DROP | 建表改表删表 | 本课脚本 + 第 7 课 |
日常后端开发 80% 的时间在写 DQL,DML 次之,DDL 通常交给迁移脚本(如 Flyway)统一管理。
讲清楚要点
- 「关系型」的核心是表 + 键:同一份事实只存一次(课程标题只存在 course 表),别处用外键引用,需要时 JOIN 拼回来。这是它和「到处复制粘贴的 Excel」的本质区别。
- SQL 是声明式语言:你描述结果,不描述过程。没有 for 循环、没有中间变量,怎么执行由数据库优化器决定——这也是为什么同一条 SQL 换个索引速度天差地别。
- MySQL 8 是本站方言:字符集统一
utf8mb4(能存 emoji),存储引擎用 InnoDB(支持事务和外键,第 9 课会用到)。PostgreSQL、Oracle 语法有差异,别混着背。 - 主键和外键是骨架:主键保证「一行能被唯一找到」,外键保证「引用的关系必须真实存在」。第 7 课表设计会展开。
- 框架不取代 SQL:MyBatis / JPA 只是帮你把 Java 对象和 SQL 互转,SQL 本身还是你写的。
- 环境隔离是纪律:练习在
sql_practice,本站真实库叫codeforge。对真实库,只读 SELECT 可以放心跑,任何写操作先想三遍。
你可能会懵的地方
-
「装了 MySQL」和「建了库」是什么关系? 安装的是 MySQL 服务(一个进程),里面可以同时有很多个 DATABASE。
sql_practice和codeforge是同一个服务里的两个库,USE xxx决定当前语句落在哪个库。 -
不装 MySQL,用在线工具行吗? 跑前几课可以(db-fiddle、SQL OnLine 都支持贴脚本)。但索引课要看执行计划、事务课要开两个会话对比,本地或 Docker 环境更稳。
-
SQL 关键字要大写吗? 不区分大小写,
select和SELECT等价。惯例是关键字大写、表名列名小写,纯为了人眼好读。本课程遵循这个惯例。 -
为什么表名不叫
order而叫pay_order?ORDER是 SQL 保留字(排序子句ORDER BY里的那个)。用保留字当表名列名,每次都要加反引号`order`,自找麻烦。避开保留字是基本素养。 -
AUTO_INCREMENT 是什么? 自增:插入时不指定 id,数据库自动分配 1、2、3……这是 MySQL 里主键最常用的生成方式,保证唯一且基本有序(第 7、8 课会讲为什么这很重要)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
连上 codeforge 就开始练 DROP | 真实数据没了 | 只在 sql_practice 练手 |
| 背关键字不连库 | 三天全忘 | 每课脚本亲手跑一遍 |
| 把 SQL 当 Excel 公式填 | 语法混乱、语义不明 | 按 SELECT … FROM … WHERE 骨架写 |
表名列名用保留字(order、desc) | 到处加反引号还容易报错 | 换名:pay_order、sort_order |
| 混用 PostgreSQL 语法 | MySQL 直接报错 | 本课程统一 MySQL 8 写法 |
| 建库不指定 utf8mb4 | 中文/emoji 乱码 | 建库语句带 DEFAULT CHARSET utf8mb4 |
卡点急救
mysql命令找不到:MySQL 的 bin 目录没进 PATH。用安装目录下mysql.exe的全路径,或直接用 Workbench / DBeaver / DataGrip 图形客户端。Access denied for user 'root'@'localhost':密码错。Docker 方式按上面给的-p123456;本机忘了密码就重置或新建一个学习用账号。- 脚本中途报错:看第一条报错所在行(多半是表已存在)。先执行开头三条
DROP/CREATE/USE,再从CREATE TABLE起重跑。 - 中文变问号:连接没走 utf8mb4。命令行加
--default-character-set=utf8mb4;JDBC 连接串加characterEncoding=utf8。
面试常问
-
关系型数据库和非关系型数据库的区别? 关系型:表结构固定、强约束、支持事务和复杂 JOIN,适合订单、账务这类强一致数据。非关系型(Redis、MongoDB):结构灵活、水平扩展容易,适合缓存、日志、文档。本站两者都用了:业务数据在 MySQL,热点缓存用 Redis。
-
说说 DDL、DML、DQL 的区别? DDL 改结构(CREATE/ALTER/DROP),DML 改数据(INSERT/UPDATE/DELETE),DQL 查数据(SELECT)。注意 DDL 通常隐式提交、无法回滚,生产上变更要格外谨慎。
-
为什么选 InnoDB 而不是 MyISAM? InnoDB 支持事务、行级锁、外键和崩溃恢复,MyISAM 都不支持。MySQL 5.5 起 InnoDB 是默认引擎,现代项目没有理由选 MyISAM。
-
utf8和utf8mb4的区别? MySQL 的utf8实际最多 3 字节,存不了 emoji 和部分生僻字;utf8mb4才是完整的 4 字节 UTF-8。新项目一律utf8mb4。
自测三问
cf_lesson.track_id这一列为什么存在?没有它,页面会发生什么?CREATE TABLE属于哪一类 SQL?UPDATE呢?- 练习库里现在有哪 5 张表?
pay_order为什么不叫order?
小结一句话
关系型数据库用「表 + 主键 + 外键」组织真相,SQL 用一句声明式的话就取到结果;全课程 10 课都建立在本课这套 sql_practice 练习数据上。
延伸
- MySQL 8.0 参考手册(当字典查,别整本啃)
- 下一课:SELECT 基本查询——学会「要哪几列、叫什么名、去掉重复」。