1. 别急着敲代码,先搞懂SQL到底在分什么
很多人学MySQL,上来就背CREATE TABLE、SELECT这些单词,背完就忘,忘了再背。我当初带新人的时候,最常见的问题不是语法不会写,而是压根不知道自己写的每一条语句属于哪个分类、解决什么问题。等到真正做项目、调权限、查性能的时候,一堆问题就冒出来了——为什么这条语句能回滚,那条不能?为什么删表要用DROP,清空数据要用TRUNCATE,这俩看起来不是一样的吗?
实际上,SQL全称叫Structured Query Language,结构化查询语言,它不是一门"编程语言",而是一套专门用来和关系型数据库打交道的标准语言。MySQL只是实现了这套标准的其中一种数据库产品。而SQL本身,按功能可以拆成五大类:数据定义语言(DDL)、数据操作语言(DML)、数据查询语言(DQL)、数据控制语言(DCL)、事务控制语言(TCL)。
这五种分类不是考试用的死概念,它们对应了你在数据库上完全不同层次的操作权限和底层行为。我打个比方你就懂了:DDL是"改楼房的户型结构",DML是"往房间里搬家具"、"调整家具位置"、"搬走几件家具",DQL是"进房间参观、数数有几件家具",DCL是"管门禁卡,决定谁能进这栋楼",TCL是"装修搬家的每一道工序,要么全部完工,要么全部退回原样,别搞到一半烂尾"。
所以这篇我不打算给你背概念,而是把每一类SQL在MySQL里最常用、最坑、最容易踩雷的地方逐个拆开讲,结合我实际写SQL、调优、带新人的经验,帮你把这五类的边界彻底理顺。不管是刚入门的,还是准备面试的,这篇都值得你认真看一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DDL:表结构是地基,地基改错了比数据错了更麻烦
2.1 常用DDL语句全家桶
DDL全称Data Definition Language,数据定义语言。核心操作对象是数据库、表、视图、索引、存储过程这些"结构",不涉及具体某一行数据。MySQL里最常见的DDL语句就这几个:CREATE、ALTER、DROP、TRUNCATE、RENAME。
sql复制-- 创建数据库
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 创建表
CREATE TABLE user (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 修改表结构
ALTER TABLE user ADD COLUMN email VARCHAR(100) NULL AFTER username;
ALTER TABLE user MODIFY COLUMN age SMALLINT UNSIGNED NOT NULL DEFAULT 18 COMMENT '年龄';
ALTER TABLE user DROP COLUMN email;
-- 删除表
DROP TABLE IF EXISTS user;
-- 清空表(保留表结构)
TRUNCATE TABLE user;
这里有个基础但关键的细节:为什么建表要显式加上ENGINE=InnoDB和DEFAULT CHARSET=utf8mb4?因为很多老版本MySQL(5.5之前)默认引擎是MyISAM,不支持事务和外键,而字符集默认是latin1,存中文直接乱码。即便你用的是5.7或8.0,显式声明也是一种好习惯,避免后面迁移环境的时候因为默认配置差异踩坑。
2.2 DDL的真正特征:隐式提交
DDL最容易被人忽略的坑,就是它会在执行的时候隐式提交当前事务。意思是,你如果在一个事务里先UPDATE了一条记录,还没提交,然后执行了一条CREATE TABLE或ALTER TABLE,之前那个UPDATE会被自动提交掉。此时你想ROLLBACK,发现回滚无效,数据已经落盘了。
这个特性在生产环境非常坑。我遇到过不止一次:同事在一个事务里改了线上订单状态,然后顺手加了个索引,结果事务中途报错回滚,但订单状态已经被提交了,线上数据直接错乱,最后只能靠备份恢复。
所以我的建议是:事务里不要混入DDL语句。如果你确实需要"先改数据再改结构",那就分开两个批次执行,先确认数据变更没问题,再执行结构变更。DDL操作之前务必要备份,特别是ALTER TABLE在大表上执行时可能会锁表很久,这个后面单独讲。
2.3 TRUNCATE和DELETE:真的不只是"一个保留结构一个删数据"这么简单
很多教程告诉你:TRUNCATE是清空表,DELETE是删除行,仅此而已。但实际工作中这俩的差异能决定你怎么办故障。
| 对比项 | TRUNCATE | DELETE |
|---|---|---|
| 删除方式 | 直接释放整个表的数据页 | 逐行删除,写入binlog和undo log |
| 回滚 | 不可以(隐式提交) | 在事务内可以回滚 |
| 自增ID | 重置为初始值 | 不重置(继续累加) |
| 触发器 | 不会触发 | 会触发 |
| 锁范围 | 表级锁 | 行级锁(InnoDB) |
| 速度 | 极快 | 慢(尤其大表) |
| 磁盘空间 | 基本完全释放 | 不会立刻释放,有碎片 |
实际工作中,清空一张大表,如果你业务明确要"全部清掉,保留表结构,id从1重新开始",用TRUNCATE。但如果你只是"清掉脏数据,可能还要回滚",那必须用DELETE并且包在事务里。另外,在MySQL 8.0里TRUNCATE依然不支持表分区单独清空,如果只需要清某个分区,得用ALTER TABLE ... TRUNCATE PARTITION。
关于ALTER TABLE的坑,我再多说一句:在千万级大表上执行ALTER TABLE ADD COLUMN,在MySQL 5.6之前的版本会锁全表,业务直接卡死。5.6以后InnoDB支持了Online DDL,但也不是所有操作都能原地完成——比如MODIFY COLUMN改数据类型,很多时候还是要重建表。所以大表变更,建议用工具或者低峰期操作,先EXPLAIN分析一下。
3. DML:增删改里那几个让你半夜爬起来修数据的细节
3.1 INSERT的进阶玩法
DML全称Data Manipulation Language,数据操作语言。核心就是增(INSERT)、删(DELETE)、改(UPDATE)。看起来最简单,但实际出的幺蛾子最多,因为数据一旦被改了,恢复的成本远高于查询错误的成本。
先说INSERT。最基础的是单条插入:
sql复制INSERT INTO user (username, password_hash, age)
VALUES ('zhangsan', 'hashed_value', 25);
但实际项目里我强烈推荐批量插入,一次插入几百到几千条,而不是一条一条循环插。原因是每条INSERT内部都有事务开销和日志写入,循环插一万条可能要几十秒,批量插只需要几百毫秒。批量插入的写法也要注意别一次塞太多,建议每批500到1000条比较稳。
MySQL还有一个INSERT IGNORE和ON DUPLICATE KEY UPDATE的语法,这两个是在主键或唯一键冲突时的处理策略:
sql复制-- 冲突时忽略,不报错
INSERT IGNORE INTO user (id, username) VALUES (1, 'lisi');
-- 冲突时更新指定字段
INSERT INTO user (id, username, age) VALUES (1, 'lisi', 30)
ON DUPLICATE KEY UPDATE
username = 'lisi',
age = 30;
注意ON DUPLICATE KEY UPDATE在批量导入场景极其好用,比如每天同步用户信息,有就更新,没有就插入,一条SQL搞定。但要注意它有一个隐藏坑:affected rows的返回值,如果插入是1,更新是2,什么没干是0,有些ORM框架(比如MyBatis的insert返回值)拿到这个数字容易误判,所以用来判断"到底插入了没有"时要小心。
3.2 UPDATE没带WHERE,就是一场事故
这是我自己实习时犯过的错,也是我带新人后强调的第一条红线:UPDATE和DELETE没有WHERE就是灾难。
sql复制-- 危险写法:把整张表的工资都改成10000
UPDATE employee SET salary = 10000;
-- 危险写法:把整张表都删了
DELETE FROM employee;
这一类语句MySQL默认是允许执行的(除非你开了sql_safe_updates这个参数),所以防呆只能靠自己。我的习惯是:写UPDATE之前,先用相同WHERE条件的SELECT查一遍,确认影响行数和预期一致。比如:
sql复制-- 先查
SELECT id, salary FROM employee WHERE department_id = 3;
-- 再改
UPDATE employee SET salary = salary * 1.1 WHERE department_id = 3;
另外关于热搜词里那个"mysql中int+5",其实很多人问的是UPDATE里字段自增的写法。正确的语法是:
sql复制UPDATE product SET stock = stock + 5 WHERE id = 100;
而不是先SELECT出来在代码里加完再UPDATE回去,那样会有并发覆盖问题。直接用SQL表达式stock = stock + 5配合InnoDB的行锁,可以保证并发下不会丢更新。
3.3 DELETE、软删除和binlog,一个都不能少
DELETE是DML里的删除,它会逐行删除并记录binlog,所以在事务里可以回滚。但实际生产环境,我很少直接物理删除业务表的数据。
原因有三:
- 删了就没了,审计、追溯、恢复都很难。
- DELETE不会释放磁盘空间,表会越来越大,碎片越来越多。
- 万一误删,找回数据的成本极高。
所以现在的主流做法是软删除:给表加一个deleted字段,查询时默认过滤,删除时只更新状态。
sql复制-- 软删除:标记为已删除
UPDATE employee SET deleted = 1 WHERE id = 100;
至于真正需要物理清理的场景,比如清理日志表,那是一整个独立的归档流程,要用DELETE ... WHERE create_time < ...分批删,或者直接DROP/TRUNCATE整表重建,避免一次性巨量DELETE把binlog撑爆、把主从延迟拉大。
4. DQL:SELECT的骨架,WHERE、GROUP BY、HAVING、ORDER BY、LIMIT的执行顺序
4.1 SELECT各子句的真实执行顺序
DQL(Data Query Language)严格来说只有一条语句,就是SELECT,但它绝对是SQL里最核心、最复杂、最值得花时间学的部分。很多人写SELECT靠拼凑,能跑就行,但性能一塌糊涂。
我强调先记住SELECT各子句的逻辑执行顺序,因为一旦顺序错了,你就会写出很多"感觉没问题但结果不对"的SQL。
标准顺序是:
FROM:确定数据源WHERE:对源数据逐行过滤GROUP BY:分组HAVING:对分组后的结果过滤SELECT:投影,计算要返回的列ORDER BY:排序LIMIT:分页
举个实际例子,如果你想查"2024年每个部门平均工资超过8000的部门,按平均工资从高到低排列,取前3":
sql复制SELECT department_id, AVG(salary) AS avg_salary
FROM employee
WHERE hire_date >= '2024-01-01'
GROUP BY department_id
HAVING avg_salary > 8000
ORDER BY avg_salary DESC
LIMIT 3;
这里有个很多人写错的点:WHERE里不能使用SELECT里定义的别名,比如WHERE avg_salary > 8000直接报错,因为WHERE在SELECT之前执行,平均工资还没算出来。但HAVING和ORDER BY可以用别名,因为它们发生在SELECT之后。
4.2 排序和去重,别被"ORDER BY是不是DQL"这种问题带偏
热搜词里出现了"mysql排序"和"mysql的or能去重吗",我统一说清楚。
排序用ORDER BY,是DQL的一部分,支持单列和多列:
sql复制SELECT name, age, salary FROM employee
ORDER BY salary DESC, age ASC;
执行顺序是优先按salary降序,如果salary相同再按age升序。很多人误以为ORDER BY是最后执行的,其实它确实在LIMIT之前执行,但逻辑上在SELECT之后。所以可以在ORDER BY里使用SELECT中的别名。
去重用DISTINCT,它是SELECT里的一种修饰,不是独立的语句:
sql复制SELECT DISTINCT department_id FROM employee;
至于"or能去重吗"——这完全是个误解。OR是在WHERE里做条件连接的,比如WHERE age > 30 OR department_id = 3,它和"去重"没有任何关系。能去重的只有DISTINCT和GROUP BY,两者虽然都能去重,但有本质区别:DISTINCT是直接对查询结果的行去重,GROUP BY是先分组再聚合,通常配合COUNT、SUM、AVG使用。
4.3 WHERE后面那些容易踩的隐式转换坑
WHERE是DQL的过滤核心,但实际写起来坑也不少。最典型的是隐式类型转换。比如表里某个字段phone是VARCHAR,你查询时写成WHERE phone = 13800138000,MySQL会尝试把字符串列转成数字,一旦列里有非纯数字的值,就可能匹配不到,或者索引失效。
再比如日期过滤:
sql复制-- 这样写,create_time列的索引有可能失效
SELECT * FROM order WHERE create_time >= '2024-01-01 00:00:00' AND create_time <= '2024-12-31 23:59:59';
-- 推荐闭区间写法
SELECT * FROM order
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2025-01-01 00:00:00';
这不是SQL分类本身的问题,是实际写SQL时最常见的性能杀手。因为DQL的核心价值不只是"查出结果",而是"高效查出任然结果"。面试喜欢问的"为什么数据量一大就慢",十有八九就出在WHERE条件没有正确使用索引上。
5. DCL和TCL:权限和事务,平时不起眼出事就背锅
5.1 DCL:GRANT和REVOKE的权限模型
DCL全称Data Control Language,数据控制语言,管的是用户权限。命令不多,主要就是GRANT和REVOKE。
sql复制-- 创建用户并授权
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'StrongP@ssw0rd';
GRANT SELECT ON shop.* TO 'readonly_user'@'%';
-- 授权增删改
GRANT INSERT, UPDATE, DELETE ON shop.* TO 'write_user'@'%';
-- 回收权限
REVOKE DELETE ON shop.* FROM 'write_user'@'%';
-- 查看权限
SHOW GRANTS FOR 'readonly_user'@'%';
这里我说几个值得注意的细节:
第一,'%'这个host通配符不代表"所有人都能连",它只是表示"任何IP"。如果只想让内网某个网段连,最好指定IP,比如'192.168.1.%',减少暴露面。
第二,GRANT之后要执行FLUSH PRIVILEGES吗?在MySQL 8.0里,只要通过CREATE USER和GRANT方式修改,权限会自动生效,不需要FLUSH。但如果是直接操作mysql.user表改数据,那就必须FLUSH。
第三,最小权限原则。给应用账号只分配它需要的权限,别图省事给个ALL PRIVILEGES。万一被SQL注入,攻击者拿到的就是数据库的全部家当。我见过不少公司,一个普通业务库账号带GRANT OPTION,直接能把权限再授给别人,属于相当危险的一类配置。
5.2 TCL:事务的COMMIT、ROLLBACK和SAVEPOINT
TCL全称Transaction Control Language,事务控制语言。MySQL里主要就是START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT。
sql复制START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 如果两条UPDATE都成功,提交
COMMIT;
-- 如果第二条失败,回滚
ROLLBACK;
事务最核心的价值是保证原子性:一组操作要么全部成功,要么全部失败,不能存在"转出成功但转入失败"的中间状态。
SAVEPOINT是事务内的存档点,适合长事务里想保留部分操作的情况:
sql复制START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
SAVEPOINT after_deduct;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 发现写入失败,回滚到存档点,保留第一条更新
ROLLBACK TO SAVEPOINT after_deduct;
COMMIT;
但这里有一个很多教程没提的细节:MySQL默认是自动提交模式,每一条单独的SQL执行完就自动COMMIT了。所以你要用事务,必须显式写START TRANSACTION或者BEGIN,否则你以为自己在事务里,其实每条语句都已经落盘了,回滚根本没用。
另外,热词里出现"数据库死锁"和"访问数据库时发生错误",这里我也提醒一句:死锁常见的场景就是两个事务以不同顺序更新同一批记录。比如事务A先更新id=1再更新id=2,事务B先更新id=2再更新id=1,两边互相等锁,InnoDB检测到死锁会选择回滚其中一方。处理死锁的方式不是靠调参数,而是保证多行更新时都以相同的顺序执行,比如都按id从小到大更新。还有一个实践技巧:事务不要开太大,避免持锁时间过长。一个事务里包含几千条UPDATE的,不仅容易死锁,还会拖垮主从同步。
6. 面试和日常归类:一张表把五类SQL收拾得明明白白
6.1 五类SQL的完整对照
这是很多培训机构不会帮你整理的归类表,也是你应付面试和日常工作最该长期贴在工位旁边的对照表:
| 分类 | 全称 | 核心命令 | 操作对象 | 能否回滚 |
|---|---|---|---|---|
| DDL | Data Definition Language | CREATE、ALTER、DROP、TRUNCATE、RENAME | 库、表、索引、视图、存储过程等结构 | 不能(隐式提交) |
| DML | Data Manipulation Language | INSERT、UPDATE、DELETE | 表中的数据行 | 事务内可以 |
| DQL | Data Query Language | SELECT | 查询并返回数据 | 不涉及修改 |
| DCL | Data Control Language | GRANT、REVOKE | 用户权限、角色 | 不能 |
| TCL | Transaction Control Language | START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT | 事务 | — |
面试高频题里,几乎必考的变体是:"DELETE、TRUNCATE、DROP三者有什么区别?"我把答案也给你理一遍:
DELETE是DML,逐行删除,可以加WHERE,事务内可回滚,不释放磁盘空间,自增ID不重置。TRUNCATE是DDL,清空整表,不能回滚,重置自增ID,释放表空间,速度快。DROP是DDL,删除整张表的结构和数据,表都不存在了,想恢复只能靠备份。
再延伸一个面试题:"MySQL的幻读是什么?和脏读、不可重复读有什么不同?"这其实挂在TCL+隔离级别话题下,但很多人学SQL分类时没关联上。简单说:
- 脏读:读到另一个事务未提交的数据。
- 不可重复读:同一事务内两次读同一行,结果不同(因为别的事务修改并提交了)。
- 幻读:同一事务内两次范围查询,结果行数不同(因为别的事务插入并提交了新行)。
InnoDB默认隔离级别是REPEATABLE READ,它通过Next-Key Lock很大程度上解决了幻读问题,但也不是绝对。这个属于TCL和锁机制的结合考点,建议深入理解一下。
6.2 日常写SQL时我给自己定的几条规矩
经验这东西,说多了像说教,但有几条确实是血泪教训换来的,我在这里分享一下。
第一,SELECT里永远不要用SELECT *,尤其是生产环境。你要哪些列就写哪些列。好处有:减少网络传输、让索引覆盖成为可能、避免表结构变更导致程序报错。
第二,UPDATE和DELETE先查后改。哪怕是在测试环境,也养成这个习惯。上生产前把SELECT换成UPDATE或DELETE,条件不要变。
第三,大批量数据操作永远拆批。比如你要删一张日志表里半年前的数据,不要一条DELETE FROM log WHERE create_time < '2024-01-01'一把梭。可以在存储过程或脚本里按主键范围循环,每次删1000条并sleep一小段,避免产生巨大binlog和长时间锁表。
第四,权限和事务边界搞清楚。连接数据库的账号分只读和读写,读写账号也不给ALL。事务里混DDL这种操作,代码评审时直接打回。
第五,写完SQL顺手EXPLAIN一下。看是不是全表扫,看type是不是ALL,看rows预估是不是离谱。这个习惯能帮你提前挡掉90%的慢查询。
sql复制EXPLAIN SELECT department_id, AVG(salary)
FROM employee
WHERE hire_date >= '2024-01-01'
GROUP BY department_id;
重点看possible_keys、key、rows三列。如果key是NULL,说明这个查询没用到索引,数据量大就危险了。
7. 从SQL分类到实战:把学习路径串起来
很多人学完分类就散了,其实是没把知识串成一条线。我建议按下面这个路径去巩固:
先练DQL,因为日常工作中查询占八成以上。把SELECT、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT、JOIN全部写熟。再练DML的INSERT、UPDATE、DELETE,配合事务练回滚。然后练DDL,自己设计几张有主外键、有索引的表,通过ALTER TABLE反复改结构,理解隐式提交。最后再学DCL,给自己建两个不同权限的账号,实际感受一下权限隔离的意义。
我在带人的时候发现一个规律:凡是能把五类SQL边界讲清楚的人,遇到问题时的排查速度明显更快。因为数据库报错时,你第一反应就应该是"这是哪一层的问题"——是结构问题(DDL)?是数据问题(DML)?是查询性能问题(DQL)?是权限问题(DCL)?还是事务一致性问题(TCL)?分类清楚,方向就对了。
比如你遇到UPDATE卡住不动,你的第一反应应该是查SHOW PROCESSLIST,看是不是有别的事务锁住了行,这是TCL和锁的问题,不是SQL语法问题。遇到权限报错Access denied,你应该直接看DCL的授权。分类是你排错的第一把钥匙。
最后再说一个很多人忽略的细节:MySQL 8.0之后,GRANT语句不再支持隐式创建用户,必须先CREATE USER再GRANT。另外8.0默认字符集是utf8mb4,默认认证插件是caching_sha2_password,有些老客户端连不上,需要调整客户端的认证插件版本。这些都是我在实际项目中踩过的兼容坑,写在这里提醒一下,免得你到时候查半天资料还找不到原因。
