把 MySQL 的小机关慢慢理顺
复习 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 | CHAR |
NUMERIC 不是字符串,它是数值类型,和 DECIMAL 更接近,适合放精确数字。
二、主键和外键:表之间的小规矩
主键负责让一行数据有唯一身份。
1 | PRIMARY KEY |
它有两个特点:
- 不能重复。
- 不能是
NULL。
一张表只能有一个主键,但这个主键可以由多列一起组成,也就是复合主键。
外键负责维护表和表之间的关系:
1 | FOREIGN KEY (dept_id) REFERENCES department(id) |
它的意思是:当前表里的 dept_id,应该能在 department 表的 id 中找到对应记录。
外键这里有几个容易混的小点:
| 说法 | 实际情况 |
|---|---|
| 一张表只能有一个外键 | 不对,一张表可以有多个外键 |
外键值一定不能为 NULL |
不一定,除非字段本身设置了 NOT NULL |
| 外键值不能重复 | 不对,多个子表记录可以指向同一个父表记录 |
| 外键用来维护引用关系 | 对,它主要保护关联数据的一致性 |
主键像身份证,外键像通讯录里的联系人编号。一个负责“我是谁”,一个负责“我和谁有关”。
三、查询语句:SQL 最常用的那条路
一条比较完整的查询通常长这样:
1 | SELECT 列名, 聚合函数(列名) AS 别名 |
别急着背,先看它的顺序感:
1 | 先从表里拿数据 |
WHERE 和 HAVING
WHERE 是分组前过滤行,HAVING 是分组后过滤结果。
| 对比 | WHERE |
HAVING |
|---|---|---|
| 过滤时机 | 分组前 | 分组后 |
| 能不能直接写聚合函数 | 通常不能 | 可以 |
| 常见搭配 | 普通条件 | GROUP BY 后的统计条件 |
比如,想先筛出有效订单,再统计每个用户的订单数:
1 | SELECT user_id, COUNT(*) AS order_count |
WHERE status = 'paid' 先筛订单,HAVING COUNT(*) >= 3 再筛统计后的用户。
ORDER BY
排序这里记住两个词:
1 | ASC 升序,从小到大,默认可以省略 |
SELECT * 和 SELECT ALL
平时写:
1 | SELECT * FROM table_name; |
就已经表示查询所有列。
ALL 是 SELECT 的默认行为,表示保留重复行。完整写法可以是:
1 | SELECT ALL * FROM table_name; |
但实际写 SQL 时,直接用 SELECT * 就够了,清楚又省事。
JOIN 连接
连接查询就是把两张表按某个条件拼起来。
1 | SELECT e.name, d.name AS dept_name |
INNER JOIN 只保留两边都匹配上的记录。
LEFT JOIN 会保留左表全部记录,如果右表没有匹配,右表字段就显示为 NULL。
1 | SELECT e.name, d.name AS dept_name |
小口诀:
1 | INNER 看交集,LEFT 保左边。 |
子查询不只住在 WHERE 里
子查询可以出现在很多地方,比如:
SELECT后面,作为查询列。FROM后面,作为临时表。WHERE后面,作为过滤条件。HAVING后面,作为分组后的条件。
例如:
1 | SELECT user_id, total_amount |
这里的子查询就是先算平均值,再拿外层订单金额去比较。
LIKE 和索引
LIKE 做模糊匹配时,百分号的位置很重要:
1 | LIKE 'abc%' |
这种前缀匹配通常可以利用 B-Tree 索引。
1 | LIKE '%abc' |
这种一开头就是 % 的写法,MySQL 很难从索引开头定位,只能更辛苦地扫数据。
INSERT 可以省略字段名吗
可以,但不太推荐。
1 | INSERT INTO employee |
只有当值的数量和顺序完全符合表结构时,它才不会出错。更稳的写法是把字段名写出来:
1 | INSERT INTO employee (id, name, dept_name, salary, hire_date) |
多写一点点,少踩很多坑,划算。
四、事务:要么一起成功,要么一起撤回
事务适合处理那些必须一起完成的操作,比如转账、库存扣减、订单创建。
1 | START TRANSACTION; |
如果中途出错,就用:
1 | ROLLBACK; |
事务最核心的感觉就是:别让数据停在半路。
ACID
| 特性 | 英文 | 意思 |
|---|---|---|
| 原子性 | Atomicity | 要么全部成功,要么全部回滚 |
| 一致性 | Consistency | 事务前后都要保持合法状态 |
| 隔离性 | Isolation | 并发事务之间尽量互不打扰 |
| 持久性 | Durability | 提交后的数据要可靠保存 |
可以把 COMMIT 理解成“盖章确认”,一旦提交成功,数据就正式生效了。
DELETE 和 TRUNCATE
它们都能清数据,但性格不一样:
| 对比 | DELETE |
TRUNCATE |
|---|---|---|
| 类型 | DML | DDL |
| 删除方式 | 可以带 WHERE,逐行删除 |
直接清空整张表 |
| 事务里能否回滚 | 可以 | MySQL 中通常会隐式提交,不能靠事务回滚 |
| 速度 | 相对慢 | 通常更快 |
如果只是删一部分数据,用 DELETE。
如果确定整张表都不要了,再考虑 TRUNCATE。这个操作要谨慎,别手滑。
五、视图:给复杂查询套一层温柔外壳
视图是虚拟表。
它本身不真正存一份完整数据,而是把一段查询保存起来。每次查视图时,MySQL 会根据底层表动态生成结果。
1 | CREATE VIEW active_employee AS |
之后就可以像查表一样查它:
1 | SELECT * FROM active_employee; |
视图的好处有两个:
- 把复杂查询封装起来,使用时更轻松。
- 隐藏不想暴露的字段,比如薪资、手机号等敏感字段。
不过也要注意:有些可更新视图被修改时,会影响到底层基础表。删除视图本身不会删除基础表数据,但通过可更新视图执行 UPDATE、DELETE 时,就不是“只改了一个影子”那么简单了。
常用语法:
1 | CREATE VIEW view_name AS |
带有复杂聚合、GROUP BY、DISTINCT 的视图,通常不能直接更新。它们更适合查询,不适合拿来改数据。
六、存储过程和存储函数
存储过程和存储函数都可以把一段 SQL 逻辑封装起来,但它们的使用方式不同。
| 对比 | 存储过程 PROCEDURE |
存储函数 FUNCTION |
|---|---|---|
| 返回值 | 可以没有返回值,也可以用 OUT 参数传出 |
必须声明返回类型,并且有 RETURN |
| 调用方式 | 用 CALL |
可以放在 SQL 表达式里 |
| 参数 | 支持 IN、OUT、INOUT |
通常只用输入参数 |
| 事务控制 | 可以包含事务控制语句 | 不适合写 COMMIT、ROLLBACK |
存储过程的常见结构:
1 | DELIMITER // |
调用它:
1 | CALL proc_name(1, @result); |
DELIMITER 是为了临时修改语句结束符。因为过程体里面有很多分号,如果不换结束符,MySQL 会提前以为定义结束了。
七、触发器:数据变化时自动响一下
触发器会在 INSERT、UPDATE、DELETE 发生时自动执行。
它不能用 CALL 手动调用,也不会因为普通 SELECT 被触发。
触发时机有两种:
1 | BEFORE 操作之前 |
触发器里经常会看到 OLD 和 NEW:
| 关键字 | 意思 | 常用场景 |
|---|---|---|
OLD.column_name |
操作前的旧值 | DELETE、UPDATE |
NEW.column_name |
操作后的新值 | INSERT、UPDATE |
比如删除员工记录前,顺手删除关联考勤记录:
1 | DELIMITER // |
这里用的是 OLD.id,因为正在删除的那一行,删除后就不存在了,要在删除前拿到它的旧值。
触发器很好用,但不要滥用。逻辑藏得太深,以后排查问题会比较累。能用清楚的业务代码解决时,就别把所有东西都塞进触发器里。
八、大小写问题:跨系统时别太自信
MySQL 的库名、表名是否区分大小写,和操作系统以及配置有关。
在 Windows 上,默认通常不区分大小写;在 Linux 上,通常区分大小写。
最稳的习惯是:表名、字段名统一用小写和下划线。
1 | user_order |
别写一会儿 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 | -- 1. 建库 |
存储过程和触发器可以等主表、关联表、查询逻辑都稳定后再加。先把普通 SQL 写清楚,再封装,思路会顺很多。
最后
MySQL 看起来零件很多,但其实它们各有各的位置。
字段类型负责“放什么”,主键外键负责“怎么关联”,查询语句负责“怎么拿出来”,事务负责“出事时怎么撤回”,视图、存储过程和触发器则负责把常用逻辑包装得更顺手。
不用一口气把所有概念背成一堵墙。先记住每个东西的性格,再慢慢把它们放回 SQL 里。
一步一步来,数据库这间小房间就会越来越亮啦。✨