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

CHAR 和 VARCHAR 像一对双胞胎,WHERE 和 HAVING 总在相邻位置出现,DELETE 和 TRUNCATE 都像是在删数据,结果脾气完全不一样。

所以这篇就不把它写成硬邦邦的清单了。我们把 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

它只适合整型字段,比如 INT、BIGINT。VARCHAR、FLOAT 这类字段就不要交给它啦。

如果手动插入了一个更大的 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;

就已经表示查询所有列。

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

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;

视图的好处有两个:

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

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

常用语法:

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 BY、DISTINCT 的视图,通常不能直接更新。它们更适合查询,不适合拿来改数据。


六、存储过程和存储函数

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

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

存储过程的常见结构:

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 会提前以为定义结束了。


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

触发器会在 INSERT、UPDATE、DELETE 发生时自动执行。

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

触发时机有两种:

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

触发器里经常会看到 OLD 和 NEW:

关键字 意思 常用场景
OLD.column_name 操作前的旧值 DELETE、UPDATE
NEW.column_name 操作后的新值 INSERT、UPDATE

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

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 按需
DELETE 和 TRUNCATE 都能回滚吗 不一样,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 里。

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