复习 MySQL 的时候,最容易烦的地方不是 SQL 有多难,而是很多概念长得太像了。

CHARVARCHAR 像一对双胞胎,WHEREHAVING 总在相邻位置出现,DELETETRUNCATE 都像是在删数据,结果脾气完全不一样。

所以这篇就不把它写成硬邦邦的清单了。我们把 MySQL 当成一间小小的数据房间,一格一格看清楚:哪些东西负责存放,哪些东西负责约束,哪些东西负责查询,哪些东西负责在关键时刻兜底。


一、先从数据类型开始

数据库里的字段类型,其实就是在告诉 MySQL:这个格子里准备放什么东西。

CHAR 和 VARCHAR

CHAR(n) 是固定长度,像提前画好的格子。不管实际内容有多短,它都会按 n 个字符的位置来占空间,不够的地方用空格补上。

VARCHAR(n) 是可变长度,像会伸缩的小盒子。实际写了多少,它就尽量按多少来存,只是额外用一点空间记录长度。

类型 长度 更适合的场景
CHAR(n) 固定长度 编号、状态码这类长度稳定的数据
VARCHAR(n) 可变长度 姓名、标题、备注这类长度不固定的数据

可以简单记成:

1
CHAR 更固定,VARCHAR 更灵活。

AUTO_INCREMENT

AUTO_INCREMENT 是自增属性,常用在主键 id 上:

1
id INT PRIMARY KEY AUTO_INCREMENT

它只适合整型字段,比如 INTBIGINTVARCHARFLOAT 这类字段就不要交给它啦。

如果手动插入了一个更大的 id,后面的自增值通常会从已有最大值继续往后走。所以手动改自增列时要小心,不然很容易和后续数据撞车。

字符串类型

常见字符串类型有:

1
2
3
CHAR
VARCHAR
TEXT

NUMERIC 不是字符串,它是数值类型,和 DECIMAL 更接近,适合放精确数字。


二、主键和外键:表之间的小规矩

主键负责让一行数据有唯一身份。

1
PRIMARY KEY

它有两个特点:

  • 不能重复。
  • 不能是 NULL

一张表只能有一个主键,但这个主键可以由多列一起组成,也就是复合主键。

外键负责维护表和表之间的关系:

1
FOREIGN KEY (dept_id) REFERENCES department(id)

它的意思是:当前表里的 dept_id,应该能在 department 表的 id 中找到对应记录。

外键这里有几个容易混的小点:

说法 实际情况
一张表只能有一个外键 不对,一张表可以有多个外键
外键值一定不能为 NULL 不一定,除非字段本身设置了 NOT NULL
外键值不能重复 不对,多个子表记录可以指向同一个父表记录
外键用来维护引用关系 对,它主要保护关联数据的一致性

主键像身份证,外键像通讯录里的联系人编号。一个负责“我是谁”,一个负责“我和谁有关”。


三、查询语句:SQL 最常用的那条路

一条比较完整的查询通常长这样:

1
2
3
4
5
6
SELECT 列名, 聚合函数(列名) AS 别名
FROM 表名
WHERE 行级过滤条件
GROUP BY 分组列
HAVING 聚合结果过滤条件
ORDER BY 排序列 DESC;

别急着背,先看它的顺序感:

1
2
3
4
5
先从表里拿数据
再用 WHERE 筛掉不需要的行
然后 GROUP BY 分组
再用 HAVING 筛分组结果
最后 ORDER BY 排序

WHERE 和 HAVING

WHERE 是分组前过滤行,HAVING 是分组后过滤结果。

对比 WHERE HAVING
过滤时机 分组前 分组后
能不能直接写聚合函数 通常不能 可以
常见搭配 普通条件 GROUP BY 后的统计条件

比如,想先筛出有效订单,再统计每个用户的订单数:

1
2
3
4
5
SELECT user_id, COUNT(*) AS order_count
FROM orders
WHERE status = 'paid'
GROUP BY user_id
HAVING COUNT(*) >= 3;

WHERE status = 'paid' 先筛订单,HAVING COUNT(*) >= 3 再筛统计后的用户。

ORDER BY

排序这里记住两个词:

1
2
ASC  升序,从小到大,默认可以省略
DESC 降序,从大到小

SELECT * 和 SELECT ALL

平时写:

1
SELECT * FROM table_name;

就已经表示查询所有列。

ALLSELECT 的默认行为,表示保留重复行。完整写法可以是:

1
SELECT ALL * FROM table_name;

但实际写 SQL 时,直接用 SELECT * 就够了,清楚又省事。

JOIN 连接

连接查询就是把两张表按某个条件拼起来。

1
2
3
SELECT e.name, d.name AS dept_name
FROM employee e
INNER JOIN department d ON e.dept_id = d.id;

INNER JOIN 只保留两边都匹配上的记录。

LEFT JOIN 会保留左表全部记录,如果右表没有匹配,右表字段就显示为 NULL

1
2
3
SELECT e.name, d.name AS dept_name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id;

小口诀:

1
INNER 看交集,LEFT 保左边。

子查询不只住在 WHERE 里

子查询可以出现在很多地方,比如:

  • SELECT 后面,作为查询列。
  • FROM 后面,作为临时表。
  • WHERE 后面,作为过滤条件。
  • HAVING 后面,作为分组后的条件。

例如:

1
2
3
4
5
SELECT user_id, total_amount
FROM orders
WHERE total_amount > (
SELECT AVG(total_amount) FROM orders
);

这里的子查询就是先算平均值,再拿外层订单金额去比较。

LIKE 和索引

LIKE 做模糊匹配时,百分号的位置很重要:

1
LIKE 'abc%'

这种前缀匹配通常可以利用 B-Tree 索引。

1
LIKE '%abc'

这种一开头就是 % 的写法,MySQL 很难从索引开头定位,只能更辛苦地扫数据。

INSERT 可以省略字段名吗

可以,但不太推荐。

1
2
INSERT INTO employee
VALUES (1, 'NAME_PLACEHOLDER', 'DEPT_PLACEHOLDER', 8000, '2026-01-10');

只有当值的数量和顺序完全符合表结构时,它才不会出错。更稳的写法是把字段名写出来:

1
2
INSERT INTO employee (id, name, dept_name, salary, hire_date)
VALUES (1, 'NAME_PLACEHOLDER', 'DEPT_PLACEHOLDER', 8000, '2026-01-10');

多写一点点,少踩很多坑,划算。


四、事务:要么一起成功,要么一起撤回

事务适合处理那些必须一起完成的操作,比如转账、库存扣减、订单创建。

1
2
3
4
5
6
7
8
9
10
11
START TRANSACTION;

UPDATE account
SET balance = balance - 100
WHERE id = 1;

UPDATE account
SET balance = balance + 100
WHERE id = 2;

COMMIT;

如果中途出错,就用:

1
ROLLBACK;

事务最核心的感觉就是:别让数据停在半路。

ACID

特性 英文 意思
原子性 Atomicity 要么全部成功,要么全部回滚
一致性 Consistency 事务前后都要保持合法状态
隔离性 Isolation 并发事务之间尽量互不打扰
持久性 Durability 提交后的数据要可靠保存

可以把 COMMIT 理解成“盖章确认”,一旦提交成功,数据就正式生效了。

DELETE 和 TRUNCATE

它们都能清数据,但性格不一样:

对比 DELETE TRUNCATE
类型 DML DDL
删除方式 可以带 WHERE,逐行删除 直接清空整张表
事务里能否回滚 可以 MySQL 中通常会隐式提交,不能靠事务回滚
速度 相对慢 通常更快

如果只是删一部分数据,用 DELETE

如果确定整张表都不要了,再考虑 TRUNCATE。这个操作要谨慎,别手滑。


五、视图:给复杂查询套一层温柔外壳

视图是虚拟表。

它本身不真正存一份完整数据,而是把一段查询保存起来。每次查视图时,MySQL 会根据底层表动态生成结果。

1
2
3
4
CREATE VIEW active_employee AS
SELECT id, name, dept_id
FROM employee
WHERE status = 'active';

之后就可以像查表一样查它:

1
SELECT * FROM active_employee;

视图的好处有两个:

  • 把复杂查询封装起来,使用时更轻松。
  • 隐藏不想暴露的字段,比如薪资、手机号等敏感字段。

不过也要注意:有些可更新视图被修改时,会影响到底层基础表。删除视图本身不会删除基础表数据,但通过可更新视图执行 UPDATEDELETE 时,就不是“只改了一个影子”那么简单了。

常用语法:

1
2
3
4
5
6
7
CREATE VIEW view_name AS
SELECT column_name FROM table_name WHERE condition;

ALTER VIEW view_name AS
SELECT column_name FROM table_name WHERE new_condition;

DROP VIEW view_name;

带有复杂聚合、GROUP BYDISTINCT 的视图,通常不能直接更新。它们更适合查询,不适合拿来改数据。


六、存储过程和存储函数

存储过程和存储函数都可以把一段 SQL 逻辑封装起来,但它们的使用方式不同。

对比 存储过程 PROCEDURE 存储函数 FUNCTION
返回值 可以没有返回值,也可以用 OUT 参数传出 必须声明返回类型,并且有 RETURN
调用方式 CALL 可以放在 SQL 表达式里
参数 支持 INOUTINOUT 通常只用输入参数
事务控制 可以包含事务控制语句 不适合写 COMMITROLLBACK

存储过程的常见结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DELIMITER //

CREATE PROCEDURE proc_name(
IN p_id INT,
OUT p_result VARCHAR(50)
)
BEGIN
DECLARE v_count INT DEFAULT 0;

SELECT COUNT(*) INTO v_count
FROM employee
WHERE id = p_id;

SET p_result = IF(v_count > 0, 'exists', 'missing');
END //

DELIMITER ;

调用它:

1
2
CALL proc_name(1, @result);
SELECT @result;

DELIMITER 是为了临时修改语句结束符。因为过程体里面有很多分号,如果不换结束符,MySQL 会提前以为定义结束了。


七、触发器:数据变化时自动响一下

触发器会在 INSERTUPDATEDELETE 发生时自动执行。

它不能用 CALL 手动调用,也不会因为普通 SELECT 被触发。

触发时机有两种:

1
2
BEFORE  操作之前
AFTER 操作之后

触发器里经常会看到 OLDNEW

关键字 意思 常用场景
OLD.column_name 操作前的旧值 DELETEUPDATE
NEW.column_name 操作后的新值 INSERTUPDATE

比如删除员工记录前,顺手删除关联考勤记录:

1
2
3
4
5
6
7
8
9
10
11
DELIMITER //

CREATE TRIGGER trg_delete_employee_attendance
BEFORE DELETE ON employee
FOR EACH ROW
BEGIN
DELETE FROM attendance
WHERE emp_id = OLD.id;
END //

DELIMITER ;

这里用的是 OLD.id,因为正在删除的那一行,删除后就不存在了,要在删除前拿到它的旧值。

触发器很好用,但不要滥用。逻辑藏得太深,以后排查问题会比较累。能用清楚的业务代码解决时,就别把所有东西都塞进触发器里。


八、大小写问题:跨系统时别太自信

MySQL 的库名、表名是否区分大小写,和操作系统以及配置有关。

在 Windows 上,默认通常不区分大小写;在 Linux 上,通常区分大小写。

最稳的习惯是:表名、字段名统一用小写和下划线。

1
2
3
user_order
order_item
created_at

别写一会儿 UserOrder,一会儿 userorder。自己看着累,数据库也可能看着迷糊。


九、容易记混的小卡片

小问题 记法
CHAR(10)VARCHAR(10) 一样占空间吗 不一样,CHAR 固定,VARCHAR 按需
DELETETRUNCATE 都能回滚吗 不一样,TRUNCATE 在 MySQL 中通常会隐式提交
一张表只能有一个外键吗 不是,可以有多个
视图会独立存储完整数据吗 不会,它是虚拟表
GROUP BY 必须配聚合函数吗 不必须,单独用时有点像分组去重
FUNCTION 必须有返回值吗 必须
PROCEDURE 必须有返回值吗 不必须
LEFT JOIN 会保留哪边 左表全部保留
LIKE '%abc' 容易用上普通 B-Tree 索引吗 不容易,因为开头就是通配符
子查询只能写在 WHERE 里吗 不是,很多位置都可以
DESC 是升序吗 不是,DESC 是降序
AUTO_INCREMENT 能给字符串用吗 不适合,它用于整型自增
触发器需要手动调用吗 不需要,它会自动触发

十、写综合 SQL 时,可以按这个顺序走

如果要从零写一套比较完整的数据库逻辑,可以先按这个路线来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-- 1. 建库
CREATE DATABASE IF NOT EXISTS db_name
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE db_name;

-- 2. 建主表
CREATE TABLE table_a (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
);

-- 3. 建关联表
CREATE TABLE table_b (
id INT PRIMARY KEY AUTO_INCREMENT,
a_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (a_id) REFERENCES table_a(id)
);

-- 4. 插入数据
INSERT INTO table_a (name)
VALUES ('NAME_PLACEHOLDER');

-- 5. 多表查询
SELECT a.name, b.created_at
FROM table_a a
JOIN table_b b ON a.id = b.a_id;

-- 6. 分组统计
SELECT a_id, COUNT(*) AS total_count
FROM table_b
GROUP BY a_id
HAVING COUNT(*) >= 1;

存储过程和触发器可以等主表、关联表、查询逻辑都稳定后再加。先把普通 SQL 写清楚,再封装,思路会顺很多。


最后

MySQL 看起来零件很多,但其实它们各有各的位置。

字段类型负责“放什么”,主键外键负责“怎么关联”,查询语句负责“怎么拿出来”,事务负责“出事时怎么撤回”,视图、存储过程和触发器则负责把常用逻辑包装得更顺手。

不用一口气把所有概念背成一堵墙。先记住每个东西的性格,再慢慢把它们放回 SQL 里。

一步一步来,数据库这间小房间就会越来越亮啦。✨