1. 为什么我说SQL分类是MySQL的第一课
做了这么多年数据库相关的工作,面试过不少人,也带过不少新人,我发现一个很有意思的现象:很多写了几年业务的开发,让他写一条复杂的查询SQL没问题,但你要是突然问一句“SQL分为哪几类”,他反而会愣一下。不是不会,是从来没刻意去梳理过。
这个现象其实挺危险的。SQL分类不是一个死记硬背的考点,它是一张地图。你想对一张表做操作,首先要问自己:这个操作属于DDL还是DML?它会不会隐式提交事务?它能不能回滚?这些判断全都建立在对SQL分类的清晰认知上。
MySQL的SQL语言按照功能可以划分为五个大类:DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)、DCL(数据控制语言)和TCL(事务控制语言)。这五类语言覆盖了数据库操作的全部场景:建表、删表、增删改查、权限分配、事务提交回滚,所有你能对数据库做的事情,都跑不出这五个分类。
这篇内容不是抄文档,我会结合自己的实际使用经验,把这五个分类讲清楚:每个分类管什么、常用语句有哪些、容易踩哪些坑,以及在实际项目中怎么用才规范。不管你是刚入行的新人,还是写了好几年SQL的老手,花几分钟把这套体系梳理一遍,都不会亏。
一个很实用的类比:你可以把数据库想象成一栋大楼,DDL是盖楼和拆楼,DML是楼里住人的日常活动,DQL是物业查房、统计住户信息,DCL是门禁系统的指纹录入和权限取消,TCL则相当于每件事做完之后的“确认签字”或者“反悔作废”。这样一想,整个体系就清晰多了。
2. 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 | 管理事务 | COMMIT、ROLLBACK、SAVEPOINT | 本身管理的就是回滚 |
这张表里有几个关键信息需要特别强调。
第一,DDL和DCL都是隐式提交的。什么意思?就是你在MySQL里执行一条CREATE TABLE或者GRANT语句,它立即生效,没有后悔药可吃。后面的内容里我会详细说这个坑。
第二,DML本身不是自动提交的,但在MySQL默认的autocommit模式下,每一条DML语句会被当作一个独立事务自动提交。很多新手以为DELETE删错了能立刻ROLLBACK找回,结果发现数据已经没了,就是因为没搞懂autocommit这个参数。
第三,TRUNCATE虽然看起来和DELETE一样都是清数据,但它属于DDL而不是DML,这个分类归属直接决定了它能不能按条件删除、能不能回滚。这是面试高频考点,也是实际开发中的关键区别。
3. DDL数据定义语言:建表删表的底层逻辑
3.1 CREATE TABLE:建表的核心要素要一次想清楚
DDL是数据库操作中最先接触的一类语言。你装好MySQL,做的第一件事肯定是CREATE DATABASE建库、CREATE TABLE建表。这是所有业务数据的载体,也是后面所有操作的前提。
先看一个最基础的建表语句:
CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT COMMENT '主键ID', name VARCHAR(50) NOT NULL COMMENT '姓名', age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄', enroll_date DATE COMMENT '入学日期', score DECIMAL(5,2) COMMENT '平均成绩', status TINYINT DEFAULT 1 COMMENT '状态:1正常 0停用', PRIMARY KEY (id), KEY idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';这个语句里包含了几层信息。ENGINE=InnoDB指定存储引擎,这是MySQL默认引擎,支持事务和行级锁,业务表基本都用它。CHARSET=utf8mb4指定字符集,utf8mb4是utf8的超集,支持emoji和生僻字,能最大程度避免中文乱码。
字段类型的选择也有讲究。比如age用TINYINT就够了,范围是-128到127,用INT纯属浪费空间;score用DECIMAL(5,2)表示最大999.99分,正好覆盖百分制场景;id用INT UNSIGNED,去掉负数,把上限从21亿翻倍到42亿。
COMMENT注释一定要写。我见过不少表,字段叫status,谁也不清楚这个字段什么意思,是状态还是余额还是别的,全靠猜。MySQL的COMMENT是写在表结构里的,查information_schema表就能看到,写清楚注释,三个月后回头看自己的表也不至于抓瞎。
3.2 ALTER TABLE:线上改表的三种常见操作
ALTER TABLE用于修改表结构。实际项目里最常用的三个场景:加字段、改字段、加索引。
-- 加字段 ALTER TABLE student ADD COLUMN phone VARCHAR(20) DEFAULT '' COMMENT '手机号' AFTER name; -- 修改字段类型 ALTER TABLE student MODIFY COLUMN age SMALLINT UNSIGNED DEFAULT 0 COMMENT '年龄'; -- 重命名字段 ALTER TABLE student CHANGE COLUMN enroll_date admission_date DATE COMMENT '入学日期'; -- 加索引 ALTER TABLE student ADD INDEX idx_class_id (class_id);ALTER TABLE在开发环境怎么玩都行,但在生产环境要格外小心。加字段如果表数据量很大,会锁表。MySQL 5.6之前,ALTER TABLE会锁全表,几百万行的表改个字段可能卡几分钟甚至更久。5.6之后有了Online DDL,大部分操作可以在线执行,但依然有锁的细节要关注。
我在生产环境改表有个习惯:数据量超过百万行,绝对不用一条ALTER直接上,而是先看表的写入量,评估峰值,再选低峰期操作。如果实在没法停机,可以借助临时表的方式,把旧表数据迁到新表再原子切换,比直接ALTER稳妥得多。
3.3 DROP与TRUNCATE:两个不可回滚的“危险分子”
DDL里最需要警惕的就是DROP和TRUNCATE。这俩有一个共同特点:一旦执行,立即生效,不可回滚。
-- 删除整张表 DROP TABLE student; -- 清空表数据,保留表结构 TRUNCATE TABLE student;DROP是删掉整张表,表结构、数据、索引全没了,彻底消失。TRUNCATE是清空数据,但保留表结构,你可以立即重新INSERT新数据。两者都不用WHERE条件,因为它们的定义就是针对整张表。
这两个语句在面试中经常被拿来和DELETE做对比。我整理了一下三者的关键差异:
| 对比项 | DELETE | TRUNCATE | DROP |
|---|---|---|---|
| 属于分类 | DML | DDL | DDL |
| 可以带WHERE | 可以 | 不可以 | 不可以 |
| 删除范围 | 按条件删行 | 清空全部行 | 删除整表 |
| 保留表结构 | 保留 | 保留 | 不保留 |
| 可回滚性 | 配合事务可回滚 | 不可回滚 | 不可回滚 |
| 性能 | 逐行删,慢 | 直接释放数据页,快 | 直接删文件,最快 |
| 自增ID重置 | 不重置 | 重置 | 表不存在 |
实际项目中,清空一张表用TRUNCATE还是DELETE,取决于业务场景。如果是临时表、日志表定期清理,TRUNCATE没问题,速度快且自增ID从头开始;如果是业务核心表要做清空操作,哪怕需求明确说清空,我也会多问一句:要不要保留自增ID的连续性?确认清楚再执行。
生产环境操作DROP和TRUNCATE,我的铁律是:先在测试库跑一遍,确认表名和库名无误;操作前备份;操作时开一个单独会话,避免复制粘贴时错行执行。这些都是用血换来的教训。
4. DML数据操作语言:每天写最多的SQL
4.1 INSERT:三种写法,效率和场景各不相同
DML是增删改操作的总称,是日常开发中写SQL最多的一类。INSERT负责把数据写入表中,它的写法有三种,每种都有对应的适用场景。
第一种,单条插入,最简单直接:
INSERT INTO student (name, age, enroll_date) VALUES ('张三', 18, '2024-09-01');第二种,批量插入,一次插入多条数据:
INSERT INTO student (name, age, enroll_date) VALUES ('张三', 18, '2024-09-01'), ('李四', 19, '2024-09-01'), ('王五', 20, '2024-09-01');第三种,INSERT INTO SELECT,把查询结果直接写入表:
INSERT INTO student_archive (name, age, enroll_date) SELECT name, age, enroll_date FROM student WHERE age > 20;批量插入的性能优势非常明显。MySQL执行一条INSERT语句,只需要一次网络往返、一次SQL解析、一次事务处理。如果一条一条插入,这些开销要重复N次。用Python操作MySQL插入10万行数据,逐条插入可能要几十秒,而拼成一条大SQL批量插入只需要两三秒。
INSERT还有一个高频变体:INSERT INTO ... ON DUPLICATE KEY UPDATE,也就是“有则更新,无则插入”。这个语法在实际项目中非常实用,比如同步数据时,主键或唯一索引已存在就更新,不存在就插入。
INSERT INTO student (id, name, age) VALUES (1, '张三', 18) ON DUPLICATE KEY UPDATE age = VALUES(age);注意,新版MySQL对VALUES(age)这种写法标了废弃,推荐用别名方式,依次类推两边是同一行数据的高效处理。具体在新版中建议写成:
INSERT INTO student (id, name, age) VALUES (1, '张三', 18) AS new ON DUPLICATE KEY UPDATE age = new.age;4.2 UPDATE:忘写WHERE,你就是在全表犯罪
UPDATE语句用于修改数据,它的语法本身很简单,但危险程度极高。
UPDATE student SET age = 20 WHERE id = 1;问题往往出在漏写WHERE子句上。一条不带WHERE的UPDATE会把整张表的所有行都改掉:
UPDATE student SET age = 20; -- 所有学生的年龄都被改成20了我见过不止一次这种事故。有的发生在测试环境,影响不大;有的发生在生产环境,后果就很严重了。有一次同事在生产库执行清洗脚本,WHERE条件因为变量拼写问题没生效,一条UPDATE把整张表的金额字段全部置零,最后靠备份恢复数据,还花了大半天核对差异。
UPDATE的SET子句可以同时更新多个字段,字段之间用逗号分隔:
UPDATE student SET age = 20, status = 0 WHERE id = 1;关于UPDATE,有几个实际经验想说一下:
大批量更新数据时,不要一次性更新几十万行。MySQL会加锁,其他业务操作会受影响。我通常会分批更新,每次只更新几千行,通过LIMIT控制:
UPDATE student SET status = 0 WHERE status = 1 LIMIT 5000;但是,MySQL对UPDATE的LIMIT有个限制:它不能和ORDER BY同时使用。如果需要按顺序分批处理,可以先用SELECT查出主键ID列表,再通过IN或者逐个ID更新。
4.3 DELETE:三种“删除”的语义差别要分清
DELETE是DML家族里和业务关联最复杂的一个,因为它牵涉到“软删除”和“硬删除”两种设计思想。
软删除是给表加一个delete_flag字段,删除操作实际是UPDATE,把标记位置为1:
UPDATE student SET delete_flag = 1 WHERE id = 10;硬删除是物理执行DELETE,从表中彻底移除记录:
DELETE FROM student WHERE id = 10;在真实项目中,核心业务表建议用软删除。刚删除的数据还有可能被恢复,运营同事误操作删了用户,你一个UPDATE就能救回来,而物理删除的恢复成本要高得多。在公司的某个人中心业务中,用户注销逻辑就是软删除:注销后冻结账号,保留用户数据和操作记录,随时可以申诉恢复。
DELETE还有一个需要警惕的场景:大批量清理数据。如果你执行:
DELETE FROM logs WHERE create_time < '2023-01-01';数据量大时,这条语句会逐行删除并记录二进制日志,可能持续几分钟甚至几十分钟,期间对表长时间加锁,还会拖累主从同步。我会优先推荐用TRUNCATE分区分段清理,或者分批DELETE,每次删除1万行并短暂休眠,降低对线上业务的影响。
5. DQL数据查询语言:面试考点,也是拉开差距的地方
5.1 SELECT基础语法和子句执行顺序
DQL是整个SQL体系里最庞大、最灵活、也最能体现功力的部分。面试考它,工作用它,性能调优也绕不开它。
标准SELECT语句的结构是固定的:
SELECT DISTINCT 字段列表 FROM 表名 JOIN 其他表 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后过滤 ORDER BY 排序字段 LIMIT 偏移量, 行数;很多人写SQL容易搞混各子句的执行顺序。记住MySQL实际执行顺序是这样的:
- FROM:确定要查询的表
- JOIN:关联其他表
- WHERE:对每行数据过滤
- GROUP BY:分组
- HAVING:对分组后的数据进行过滤
- SELECT:计算和投影需要的字段
- ORDER BY:排序
- LIMIT:取指定行数
这个顺序决定了两个很常见的坑:
WHERE不能在聚合函数上过滤,因为它在GROUP BY之前执行,此时聚合结果还没算出来。想在分组后过滤,只能用HAVING。
SELECT中定义的别名不能在WHERE中引用,但可以在ORDER BY和HAVING中引用。因为SELECT执行在WHERE之后,别名是这一步才生成的。
5.2 WHERE条件:索引生效与否决定查询快慢
查询性能的关键在于WHERE条件能否用上索引。这里说几条实际经验。
等值查询、范围查询、最左前缀匹配,是用索引的典型场景:
SELECT * FROM student WHERE class_id = 5; -- 等值,可走索引 SELECT * FROM student WHERE age BETWEEN 18 AND 20; -- 范围,可走索引 SELECT * FROM student WHERE class_id = 5 AND name = '张三'; -- 最左前缀,前提是索引里class_id在前而对字段做函数处理或隐式类型转换,会让索引失效:
SELECT * FROM student WHERE YEAR(enroll_date) = 2024; -- 对字段用函数,索引失效 SELECT * FROM student WHERE phone = 13800138000; -- 字段是VARCHAR,传入数值,索引失效在数据量小的表上,索引失效没什么感觉,但表到百万级以后,全表扫描和索引查询的差距可能是几百倍。排查慢SQL时,EXPLAIN是第一个要看的工具:
EXPLAIN SELECT * FROM student WHERE class_id = 5;EXPLAIN结果里的type字段是关键,从好到差依次是system、const、eq_ref、ref、range、index、ALL。看到ALL就意味着全表扫描,必须引起注意。
5.3 聚合分组与排序分页
GROUP BY用来分组聚合,配合聚合函数使用。最常见的聚合函数就是COUNT、SUM、AVG、MAX、MIN。
SELECT class_id, COUNT(*) AS student_count, AVG(score) AS avg_score FROM student GROUP BY class_id HAVING COUNT(*) > 10;这里要注意,统计行数时COUNT()和COUNT(字段)的语义不同。COUNT()统计所有行,COUNT(column)只统计该字段非NULL的行。如果字段允许NULL,两者的结果会不一样。
ORDER BY默认是升序ASC,降序要用DESC:
SELECT * FROM student ORDER BY score DESC LIMIT 10;LIMIT分页在数据量大的时候有个性能问题:深分页会导致延迟变大。比如LIMIT 100000, 20,MySQL需要先读取前10万行再丢弃,这是对资源的浪费。优化方式是记录上次查询的最大ID,用范围条件替代偏移量:
SELECT * FROM student WHERE id > 100000 ORDER BY id ASC LIMIT 20;5.4 JOIN多表查询:内连接和外连接的区别
实际业务中数据很少只存在一张表里,JOIN是高频操作。JOIN的类型主要有三种。
INNER JOIN内连接,只返回两张表中匹配成功的行:
SELECT s.name, c.class_name FROM student s INNER JOIN class c ON s.class_id = c.id;LEFT JOIN左连接,返回左表的全部行,右表没有匹配则补NULL:
SELECT s.name, c.class_name FROM student s LEFT JOIN class c ON s.class_id = c.id;RIGHT JOIN使用场景相对少一些,和LEFT JOIN对称,返回右表的全部行。因为RIGHT JOIN完全可以通过交换表顺序用LEFT JOIN实现,很多团队的规范里直接禁止RIGHT JOIN。
JOIN时有个性能原则要记住:小表驱动大表。MySQL优化器会根据表和索引信息选择驱动顺序,但通过left join左边表尽量放小表,会让执行计划更可控。
5.5 子查询与EXISTS的取舍
子查询就是把一个SELECT的结果作为另一个SELECT的条件或数据源:
SELECT name FROM student WHERE class_id IN (SELECT id FROM class WHERE teacher_id = 20);这种写法可读性好,但MySQL对IN子查询的优化有时不如JOIN,特别是子查询结果集很大时。很多情况下,改用JOIN或EXISTS能拿到更好的性能:
-- 用JOIN改写 SELECT s.name FROM student s JOIN class c ON s.class_id = c.id WHERE c.teacher_id = 20;EXISTS适合判断“存在性”,它找到一条匹配就停止,不完全执行子查询:
SELECT name FROM student s WHERE EXISTS (SELECT 1 FROM class c WHERE c.id = s.class_id AND c.teacher_id = 20);但也要说,现代MySQL 8.0的优化器对子查询的处理已经比早期版本强很多,大部分场景下执行计划会自动重写。我自己的原则是:先按可读性写,遇到慢查询再用EXPLAIN分析,需要优化才改写,不要为了理论性能牺牲日常维护体验。
6. DCL数据控制语言:权限管理,别给多了再后悔
6.1 GRANT授权:权限粒度要克制
DCL在单机开发环境里用得少,但在多环境、多团队协作时非常重要。它主要解决两个问题:谁能连数据库,能做什么操作。
创建一个新用户并授权,典型操作如下:
-- 创建用户 CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'StrongP@ss123'; -- 授权查询、插入、更新、删除 GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'192.168.1.%'; -- 刷新权限 FLUSH PRIVILEGES;这里有几个细节值得展开。
权限粒度可以从三个层级控制:全局、数据库、表。最小粒度到列,不过实际项目里用到列级权限的场景很少,大多是给某个库的所有表授权,或者单独授权几张表。
授权时指定主机范围是个好习惯。上面例子中192.168.1.%限制了只有这个网段的机器能连接,比直接用%允许任意IP安全得多。
CREATE USER和GRANT都可以创建用户,区别在于GRANT在MySQL 8.0之前隐式创建用户,8.0之后取消了这种隐式行为,必须先用CREATE USER。
6.2 REVOKE回收权限:删权限和删数据一样要谨慎
回收权限用REVOKE,语法和GRANT对称:
REVOKE DELETE ON mydb.* FROM 'app_user'@'192.168.1.%'; REVOKE ALL PRIVILEGES ON mydb.* FROM 'app_user'@'192.168.1.%'; FLUSH PRIVILEGES;权限管理最常见的坑是授权过宽。有一次排查一个线上问题,发现某个内部系统的账号有所有库的DROP权限,检查后发现是之前做临时数据迁移时顺手授权的,迁移完了权限忘了回收,这个账号就顶着一个过宽的权限在线上挂了半年。
我的习惯是建立一套权限变动记录:每次授权都写清楚原因、时间、申请人、预期回收日期。至少每季度梳理一次数据库账号,把长期不用的账号或权限收回。GRANT和REVOKE都属于DDL族中不可回滚的操作,执行前想清楚,执行后及时记录。
7. TCL事务控制语言:数据安全最后一道防线
7.1 事务的ACID四个特性
TCL的所有操作都围绕事务展开。事务是一组SQL操作的逻辑单元,这组操作要么全部成功,要么全部失败。它必须满足ACID四个特性。
原子性:事务中的操作不可分割,要么全做,要么全不做。一个事务里插入10条数据,第5条失败,前面4条也要回滚。InnoDB通过undo log实现原子性。
一致性:事务执行前后,数据库从一个一致状态变换到另一个一致状态。比如转账场景,A扣100元,B加100元,不管中间发生什么,总金额不变。
隔离性:并发事务之间互不干扰。一个事务正在写入的数据,另一个事务不应该看到中间状态。隔离性由锁和MVCC实现。
持久性:事务提交后,对数据的修改是永久的,即使系统崩溃也不会丢失。InnoDB通过redo log保证持久性。
7.2 COMMIT和ROLLBACK的正确使用姿势
MySQL默认开启autocommit,每一条DML语句自动提交。如果需要多个操作组成一个事务,必须显式关闭autocommit或用BEGIN/START TRANSACTION开启:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; UPDATE account SET balance = balance + 100 WHERE user_id = 2; -- 检查两个UPDATE都成功 COMMIT;如果执行过程中发现异常或者业务判断不通过,就ROLLBACK:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; -- 发现余额不足或业务校验失败 ROLLBACK;编程语言中,事务的正确写法是配合异常处理:
try: conn.begin() cursor.execute("UPDATE account SET balance = balance - 100 WHERE user_id = 1") cursor.execute("UPDATE account SET balance = balance + 100 WHERE user_id = 2") conn.commit() except Exception: conn.rollback() raise finally: cursor.close() conn.close()如果只在执行完所有语句后统一commit,那么任何一步出错都需要手动rollback来避免留下半个事务的数据。
7.3 SAVEPOINT:事务内的分支回滚
SAVEPOINT允许在事务内设置保存点,回滚时可以选择回到某个保存点,而不是全部回滚:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; SAVEPOINT after_deduct; UPDATE account SET balance = balance + 100 WHERE user_id = 2; -- 这里发现用户2的账户异常,只回滚到这里,保留第一步的扣款 ROLLBACK TO SAVEPOINT after_deduct; -- 继续处理或最终提交 COMMIT;这种写法适合处理批处理中某一小段失败的场景,可以保留已执行成功的部分操作,只撤销出错的那一段。
SAVEPOINT有个细节:事务中如果大量使用SAVEPOINT,会占用更多undo log空间,影响性能。常规开发中,一个事务里设置三五个以内的保存点即可,不要过度依赖。
7.4 隔离级别:事务与并发矛盾的平衡点
MySQL的InnoDB支持四种隔离级别,这是TCL相关的另一个重要概念,面试常考。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB通过MVCC和间隙锁实际避免) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
MySQL默认隔离级别是REPEATABLE READ。不同隔离级别最大区别在于能读到什么版本的数据。因为InnoDB在REPEATABLE READ下引入了间隙锁机制,能够有效规避常规幻读问题,所以这个级别在实际项目中用得最普遍。
修改隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;高并发转账场景最常见的问题是更新丢失。解决方式不止一种:数据库层面的“乐观锁”机制也很实用,关键是理解事务隔离级别决定你会看到哪些数据,然后再设计并发控制方案。
8. 实际项目中的SQL分类实战心得
8.1 SQL执行常见问题排查对照表
日常工作中,SQL报错是不可避免的,多数错误其实只要理解了分类归属就能很快定位。我把高频问题整理成了一张速查表。
| 报错或现象 | 涉及分类 | 原因 | 处理方式 |
|---|---|---|---|
| Table already exists | DDL | 表已存在,CREATE TABLE重复执行 | 加IF NOT EXISTS,或先检查information_schema |
| Unknown column | DML | 字段名写错或表结构变更未同步 | 先DESC表查看结构 |
| Duplicate entry for key | DML | 唯一索引冲突 | 用ON DUPLICATE KEY UPDATE或提前查重 |
| 数据被误删或误改 | DML | WHERE条件写错或漏写 | 立刻用备份或binlog恢复,前提是之前有备份 |
| 大表ALTER卡死 | DDL | 表数据量大,锁竞争 | 低峰期操作或使用在线DDL工具 |
| 慢查询 | DQL | 索引失效或深分页 | EXPLAIN分析,优化索引或改写SQL |
| Access denied | DCL | 权限不足 | 检查当前用户权限,用SHOW GRANTS |
| 事务提交后无法回滚 | TCL | autocommit已开启 | 检查sql_mode和autocommit设置 |
8.2 写SQL的几个规范化建议
规范不是约束,是保护。以下是我在团队里推行的几条SQL编写规范,踩了足够多的坑之后总结出来的。
表名、字段名一律小写,多个单词用下划线分隔。MySQL在Linux下对表名区分大小写,而Windows下不区分,统一小写能避免环境差异导致的空格或拼写完全被忽略的问题,很多人踩过这个坑才发现环境一致性的重要性。
每一条DML语句都要有WHERE条件,除非你能明确说出“不需要条件”的理由。这是一个很简单的纪律,但能挡住大部分误操作。
代码块里的SQL要格式化,关键字大写或小写要统一。缩进和换行要清晰,长SQL拆成多行,方便review和排查。
事务里不要混入DDL语句。DDL会触发隐式提交,导致事务提前生效,后面想回滚就回滚不了了。比如:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; CREATE INDEX idx_balance ON account(balance); -- 这里隐式提交了事务 ROLLBACK; -- 此时第一步的UPDATE已经无法回滚批量数据的UPDATE和DELETE一定要分批执行。单批影响行数控制在5000以内,通过循环或应用层控制节奏,能显著降低锁冲突和主从延迟。
所有表必须有主键,推荐用自增INT或BIGINT。没有主键的InnoDB表在复制和恢复时会有很多麻烦。UPDATE...WHERE条件尽量走主键或索引,避免扫全表。
养成良好的复盘习惯,每一次线上SQL事故都要记录原因、处理过程和改进措施,沉淀成团队的SQL规范文档。
8.3 关于SQL分类,我的几点个人体会
我在实际工作中发现一个规律:对SQL分类理解越深的人,处理数据库问题的速度越快。因为大部分问题在定位阶段,就能根据“这是什么类型的SQL操作”快速缩小排查范围。
比如一条DELETE执行报错,如果你理解DELETE属于DML、受事务控制、受外键约束影响、需要权限支撑,排查思路就非常清晰:先看权限,再看事务状态,再看外键关系,最后看数据本身。而如果不理解分类,只能无头苍蝇一样乱试。
优化SQL时也有类似体会。同样是查询慢,如果问题出在DQL,重点看索引和JOIN方式;如果问题出在DML,重点看锁竞争和批量效率;如果问题出在DDL,重点看表结构和存储引擎选择。分类不同,优化方向完全不同。
最后分享一个实用技巧:建表前先在纸上列出这张表的所有查询场景和更新场景,再决定索引怎么建、字段怎么设计。新手习惯先把表建出来再说,但后面加索引、改字段的成本,往往比一开始想清楚高得多。SQL分类的本质是让你在动手之前,先知道自己正在做哪一类操作、会产生什么影响、需要注意什么风险。这个意识建立起来,处理数据库问题会顺手很多。