前置准备:统一案例表
classes 班级表
| id | name |
|---|---|
| 1 | 一班 |
| 2 | 二班 |
| 3 | 三班 |
| 4 | 四班 |
students 学生表
| id | class_id | name | gender | score |
|---|---|---|---|---|
| 1 | 1 | 小明 | M | 90 |
| 2 | 1 | 小红 | F | 95 |
| 3 | 1 | 小军 | M | 88 |
| 4 | 1 | 小米 | F | 73 |
| 5 | 2 | 小白 | F | 81 |
| 6 | 2 | 小兵 | M | 55 |
| 7 | 2 | 小林 | M | 85 |
| 8 | 3 | 小新 | F | 91 |
| 9 | 3 | 小王 | M | 89 |
| 10 | 3 | 小丽 | F | 85 |
第一部分:关系模型核心三要素
关系数据库的本质是用二维表格存储数据,表和表之间通过主键、外键建立关联,靠索引提升查询效率。
1.1 主键
什么是主键
主键是一张表里唯一标识一条记录的字段,相当于数据的「身份证号」。
- 一张表只能有一个主键,值绝对不重复
- 主键一旦插入,最好永远不要修改
- 主键字段不允许为 NULL
设计原则
不要用业务字段当主键 (比如身份证号、手机号,业务规则可能变更)。推荐用自增整数 id 作为主键,和业务完全无关,不会因为业务变化受影响。
代码示例
建表时定义主键(MySQL 语法):
sql
CREATE TABLE students ( id BIGINT NOT NULL AUTO_INCREMENT, -- 自增主键
class_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
gender VARCHAR(1) NOT NULL,
score INT NOT NULL,
PRIMARY KEY (id) -- 声明id为主键
);
补充:也支持「联合主键」------ 用多个字段共同作为主键,只要所有主键列组合起来不重复即可。但日常开发尽量不用,会增加表的复杂度。
1.2 外键
什么是外键
外键用来在两张表之间建立一对多 关联关系。比如一个班级对应多个学生,我们就在学生表加一个 class_id 字段,指向班级表的主键 id,这个 class_id 就是外键。
外键约束
我们可以给字段添加外键约束,让数据库强制保证数据有效性 ------ 比如班级表里没有 id=99 的班级,学生表里就插不进去 class_id=99 的学生。
创建外键约束:
sql
ALTER TABLE students ADD CONSTRAINT fk_class_id -- 外键约束名,可自定义
FOREIGN KEY (class_id) -- 当前表的外键列
REFERENCES classes (id); -- 关联到目标表的主键
删除外键约束:
sql
ALTER TABLE students
DROP FOREIGN KEY fk_class_id;
延伸的三种表关系
- 一对多:一个班级 → 多个学生(最常见)
- 多对多:通过中间表实现,比如学生和课程,一个学生选多门课,一门课有多个学生
- 一对一:一张表的一条记录对应另一张表的一条记录,常用来拆分冷热字段(比如用户基本信息表 + 用户详情表)
1.3 索引
什么是索引
索引是对表中一列或多列的值预排序的数据结构,作用就像书的目录 ------ 不用一页页翻完整本书,直接通过目录快速定位内容,大幅提升查询速度。
创建普通索引
如果经常按分数查询学生,就给 score 列创建索引:
sql
ALTER TABLE students
ADD INDEX idx_score (score); -- idx_score是索引名,可自定义
也可以创建多列联合索引:
sql
ALTER TABLE students
ADD INDEX idx_name_score (name, score);
索引的优缺点
- 优点:大幅提升查询速度
- 缺点:插入、更新、删除数据时,需要同步修改索引,写入速度会变慢;索引越多,写入性能越低
唯一索引
如果某一列业务上要求唯一(比如邮箱、身份证号),但又不适合当主键,可以加唯一索引:
sql
ALTER TABLE students
ADD UNIQUE INDEX uni_name (name); -- 保证学生姓名不重复
也可以只加唯一约束、不创建索引:
sql
ALTER TABLE students
ADD CONSTRAINT uni_name UNIQUE (name);
小技巧:主键会自动创建主键索引,而且查询效率是最高的,因为主键保证绝对唯一。
第二部分:查询数据(开发最常用)
查询是 SQL 的核心,也是日常开发最高频的操作,我们从最简单到复杂逐层讲解。
2.1 基本查询
查询整张表的所有行、所有列:
sql
SELECT * FROM students;
执行后会返回 students 表全部 10 条学生数据。
小知识:SELECT 后面可以跟表达式,比如 SELECT 100+200;,常用来测试数据库连接是否正常。
2.2 条件查询
用 WHERE 子句筛选符合条件的记录,比如只查 80 分以上的学生。
语法:
sql
SELECT * FROM 表名 WHERE 条件表达式;
三种逻辑条件
- AND(并且) :同时满足两个条件示例:查询分数≥80 且是男生的学生
sql
SELECT * FROM students WHERE score >= 80 AND gender = 'M';
- OR(或者) :满足任意一个条件即可示例:查询分数≥80 或者是男生的学生
sql
SELECT * FROM students WHERE score >= 80 OR gender = 'M';
- NOT(取反) :不符合条件的记录示例:查询不是 2 班的学生
sql
SELECT * FROM students WHERE NOT class_id = 2; -- 等价于 SELECT * FROM students WHERE class_id <> 2;
常用条件运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
= |
相等 | name = '小明' |
> / >= |
大于 / 大于等于 | score > 80 |
< / <= |
小于 / 小于等于 | score < 60 |
<> |
不等于 | gender <> 'M' |
LIKE |
模糊匹配 | name LIKE '小%'(匹配所有姓「小」的学生) |
优先级:NOT > AND > OR,拿不准的时候直接加小括号,不会出错。
2.3 投影查询
如果不需要所有列,只返回指定字段,就用投影查询,还可以给列起别名。
示例 1:只查 id、姓名、分数三列
sql
SELECT id, name, score FROM students;
示例 2:给列起别名把 score 重命名为 points 返回:
sql
SELECT id, name, score points FROM students;
示例 3:投影 + 条件组合只查询男生的 id、姓名和分数:
sql
SELECT id, name, score FROM students WHERE gender = 'M';
2.4 排序
查询结果默认按主键排序,用 ORDER BY 可以指定按某一列排序。
示例 1:按分数从低到高升序
sql
SELECT id, name, score FROM students ORDER BY score;
-- ASC是升序,默认可以省略,等价于 ORDER BY score ASC
示例 2:按分数从高到低降序
sql
SELECT id, name, score FROM students ORDER BY score DESC;
示例 3:多列排序先按分数倒序,分数相同的再按性别排序:
sql
SELECT id, name, gender, score
FROM students
ORDER BY score DESC, gender;
示例 4:带条件的排序查询一班的学生,按分数倒序:
sql
SELECT id, name, score
FROM students
WHERE class_id = 1 ORDER BY score DESC;
2.5 分页查询
数据量大的时候,一次返回所有数据太耗性能,分页每次只返回一页的数据。
语法:
sql
LIMIT 每页条数 OFFSET 起始位置
- 起始位置从 0 开始计算
- 公式:第 N 页的 OFFSET = 每页条数 × (N - 1)
示例:每页 3 条,查询第 1、2、3 页
第 1 页:
sql
SELECT id, name, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 0;
第 2 页:
sql
SELECT id, name, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 3;
第 3 页:
sql
SELECT id, name, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 6;
MySQL 简写:LIMIT 3 OFFSET 3 可以简写成 LIMIT 3, 3
注意:OFFSET 越靠后,查询效率越低,大数据量分页需要专门优化。
2.6 聚合查询
用来做统计计算,比如统计总人数、平均分、最高分,不用把数据全查出来自己算。
常用聚合函数
| 函数 | 作用 |
|---|---|
COUNT(*) |
统计行数 |
SUM(列名) |
计算某一列的数值总和 |
AVG(列名) |
计算某一列的平均值 |
MAX(列名) |
求某一列的最大值 |
MIN(列名) |
求某一列的最小值 |
示例 1:统计学生总人数
sql
SELECT COUNT(*) num FROM students;
-- 给结果列起个别名num,方便程序读取
返回结果:num = 10
示例 2:统计男生的平均成绩
sql
SELECT AVG(score) average FROM students WHERE gender = 'M';
分组聚合
按某一列分组统计,比如统计每个班有多少学生:
sql
SELECT class_id, COUNT(*) num
FROM students
GROUP BY class_id;
结果会返回 3 行,分别对应 1 班、2 班、3 班的人数。
进阶:按班级 + 性别分组,统计各班男女生人数
sql
SELECT class_id, gender, COUNT(*) num
FROM students
GROUP BY class_id, gender;
2.7 多表查询
同时查询多张表,返回的是两张表的笛卡尔积------ 第一张表的每一行和第二张表的每一行两两组合。
基础示例:
sql
SELECT * FROM students, classes;
学生表 10 行,班级表 4 行,结果会返回 10×4=40 行数据,大部分组合是没有意义的,实际开发中一定会加条件筛选,并且给表起别名,避免列名重复。
带条件和别名的多表查询:
sql
SELECT
s.id sid, s.name, s.score,
c.id cid, c.name cname
FROM students s, classes c
WHERE s.class_id = c.id; -- 只保留班级id匹配的有效行
2.8 连接查询
连接查询是更规范的多表查询方式:先确定一张主表,再把另一张表的行按条件「拼接」上来。
INNER JOIN(内连接,最常用)
只返回两张表都匹配的行。
示例:查询学生信息,同时返回对应的班级名称
sql
SELECT
s.id, s.name, s.class_id,
c.name class_name, s.score
FROM students s
INNER JOIN classes c
ON s.class_id = c.id; -- 连接条件
结果:四班没有学生,所以不会出现在结果里。
LEFT JOIN(左连接)
左表的所有行都会返回,右表没有匹配的字段就填 NULL。
sql
SELECT
s.id, s.name, c.name class_name
FROM students s
LEFT JOIN classes c
ON s.class_id = c.id;
如果有学生的class_id在班级表不存在,class_name会显示 NULL。
RIGHT JOIN(右连接)
右表的所有行都会返回,左表没有匹配的字段就填 NULL。
sql
SELECT
s.id, s.name, c.name class_name
FROM students s
RIGHT JOIN classes c
ON s.class_id = c.id;
结果里会出现「四班」这一行,学生相关的列都是 NULL。
FULL JOIN(全连接)
左右表的所有行都返回,两边不匹配的都填 NULL,MySQL 原生不直接支持。
记忆口诀:
- INNER JOIN:两边都有才返回
- LEFT JOIN:左表全返回,右表匹配不上就空着
- RIGHT JOIN:右表全返回,左表匹配不上就空着
第三部分:修改数据(增删改)
数据库的增删改查简称 CRUD,对应 Create、Retrieve、Update、Delete。
3.1 插入数据
语法:
sql
INSERT INTO 表名 (字段1, 字段2, ...) VALUES (值1, 值2, ...);
示例:插入一条学生记录
sql
INSERT INTO students (class_id, name, gender, score) VALUES (2, '大牛', 'M', 80);
- 字段顺序不用和表结构完全一致,但值的顺序必须和字段一一对应
- 自增主键
id不用写,数据库会自动生成
批量插入
一条语句插入多条记录,用逗号分隔:
sql
INSERT INTO students (class_id, name, gender, score)
VALUES
(1, '大宝', 'M', 87),
(2, '二宝', 'M', 81),
(3, '三宝', 'M', 83);
3.2 更新数据
语法:
sql
UPDATE 表名 SET 字段1=值1, 字段2=值2 WHERE 条件;
示例 1:更新指定学生的信息
sql
UPDATE students SET name='大牛', score=66 WHERE id=1;
示例 2:批量更新多条记录给所有 80 分以下的同学加 10 分:
sql
UPDATE students SET score = score + 10 WHERE score < 80;
⚠️ 高危提醒 UPDATE 如果不带 WHERE 条件,会更新整张表的所有数据:
sql
UPDATE students SET score = 60; -- 危险操作!所有学生分数都变成60
执行前一定要先用 SELECT 测试 WHERE 条件是否正确。
3.3 删除数据
语法:
sql
DELETE FROM 表名 WHERE 条件;
示例 1:删除指定学生
sql
DELETE FROM students WHERE id=1;
示例 2:批量删除删除所有不及格的学生:
sql
DELETE FROM students WHERE score < 60;
⚠️ 高危提醒 不带 WHERE 的 DELETE 会清空整张表:
sql
DELETE FROM students; -- 危险操作!清空学生表所有数据
同样建议先 SELECT 确认条件再执行。
第四部分:实用 SQL 技巧与事务
4.1 实用 SQL 语句
这几个是开发中非常好用的语法糖,能帮你少写很多业务代码。
1. 插入或替换
如果记录存在就先删再插,不存在就直接插入,不用先查一遍:
sql
REPLACE INTO students (id, class_id, name, gender, score)
VALUES (1, 1, '小明', 'F', 99);
2. 插入或更新
如果记录存在就更新指定字段,不存在就插入:
sql
INSERT INTO students (id, class_id, name, gender, score)
VALUES (1, 1, '小明', 'F', 99)
ON DUPLICATE KEY UPDATE name='小明', gender='F', score=99;
3. 插入或忽略
如果记录存在就啥也不做,不存在才插入:
sql
INSERT IGNORE INTO students (id, class_id, name, gender, score)
VALUES (1, 1, '小明', 'F', 99);
4. 复制表快照
把一张表的部分数据复制成一张新表,不用提前建表:
sql
-- 把1班的学生复制成新表 students_of_class1
CREATE TABLE students_of_class1
SELECT * FROM students WHERE class_id = 1;
5. 查询结果直接写入表
统计结果直接存入统计表,不用程序中转:
sql
-- 先建统计表
CREATE TABLE statistics (
id BIGINT NOT NULL AUTO_INCREMENT,
class_id BIGINT NOT NULL,
average DOUBLE NOT NULL,
PRIMARY KEY (id) );
-- 把各班平均分直接写入
INSERT INTO statistics (class_id, average)
SELECT class_id, AVG(score)
FROM students
GROUP BY class_id;
6. 强制使用指定索引
数据库查询优化器选错索引时,可以强制它使用指定索引:
sql
SELECT * FROM students
FORCE INDEX (idx_class_id)
WHERE class_id = 1
ORDER BY id DESC;
7. 什么是事务
事务是把多条 SQL 语句绑成一个整体,要么全部执行成功,要么全部失败,不会出现一半成功一半失败的情况。
最经典的场景是转账:A 账户扣 100 元,B 账户加 100 元,这两条必须同时成功,或者同时回滚。
事务四大特性(ACID)
- A 原子性:所有 SQL 是一个原子单元,要么全执行,要么全不执行
- C 一致性:事务结束后,数据状态保持一致(扣的钱一定到对方账户)
- I 隔离性:多个事务并发执行,互相之间数据隔离
- D 持久性:事务提交后,修改永久保存,断电不丢失
事务基础语法
正常提交:
sql
BEGIN; -- 开启事务
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 提交事务,所有修改生效
出错回滚:
sql
BEGIN; -- 执行SQL...
ROLLBACK; -- 回滚,所有修改都撤销
8. 事务隔离级别
多个事务并发运行时,可能会出现脏读、不可重复读、幻读三类问题,数据库提供了 4 种隔离级别来平衡安全性和性能。
先明确三个概念:
- 脏读 :一个事务读到了另一个事务还没提交的修改
- 不可重复读:同一个事务内,两次读同一条记录,结果不一样
- 幻读:同一个事务内,第一次查某条记录不存在,更新却成功了,再查又出现了
| 四种隔离级别对比:隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 会 | 会 | 会 |
| Read Committed | 不会 | 会 | 会 |
| Repeatable Read | 不会 | 不会 | 会 |
| Serializable | 不会 | 不会 | 不会 |
8.1 Read Uncommitted(读未提交)
最低隔离级别,会出现脏读。
- 事务 A 修改了数据但未提交
- 事务 B 读到了这个未提交的修改
- 事务 A 回滚,事务 B 刚才读到的就是无效的「脏数据」
实际开发基本不用,安全性太差。
8.2 Read Committed(读已提交)
只能读到其他事务已经提交的数据,解决了脏读,但仍存在不可重复读。
- 事务 B 第一次读数据,值是 A
- 事务 A 修改数据并提交
- 事务 B 第二次读同一条数据,值变成了 B
- 同一个事务内,两次读取结果不一致
很多商业数据库的默认隔离级别是这个。
8.3 Repeatable Read(可重复读)
解决了脏读和不可重复读:同一个事务内,多次读取同一条记录,结果始终一致,哪怕别的事务修改并提交了,它也读不到。
但仍存在幻读:
- 事务 B 查询
id=99的学生,结果为空 - 事务 A 插入了
id=99的学生并提交 - 事务 B 再查还是空,但更新这条记录却成功了
- 再次查询记录又出现了,像幻觉一样
MySQL InnoDB 引擎的默认隔离级别就是 Repeatable Read。
8.4 Serializable(串行化)
最高隔离级别,所有事务排队串行执行,脏读、不可重复读、幻读都不会出现。缺点是并发性能极差,只有对数据一致性要求极高的场景才会使用。
文末总结
- 关系模型的核心是主键、外键、索引,分别负责唯一标识、表关联、提速查询。
- 查询是 SQL 的重中之重,从基础查询到连接查询,层层递进,日常开发覆盖 90% 场景。
- 增删改操作一定要注意带 WHERE 条件,避免全表误操作。
- 事务保证数据一致性,四个隔离级别各有取舍,默认级别能满足绝大多数业务需求。