我前阵子被一个转岗过来的同事问:DELETE删完数据自增ID却断了一截,TRUNCATE清空表之后又没法回滚,为什么?我说,你先把MySQL里的SQL分类搞明白,就全通了。这个分类不是考试时候拿来背的,而是你面对一个数据库问题时用来定位“危险等级”的底层地图。SQL分类可以大致拆成DDL、DML、DQL、DCL四类,有人会把事务控制TCL也单独拎出来,本质上还是围绕数据操纵展开。这篇文章我就用实际开发、review别人SQL、处理线上故障的视角,把这几类讲透。适合刚学MySQL想系统梳理一遍的同学,也适合已经写了几年SQL但对“执行顺序”“能不能回滚”“为什么锁表”这些事还模糊的开发者。
1. SQL分类的底层逻辑:为什么很多故障都是“我以为是A其实是B”
1.1 四类语句的分工比你以为的更清晰
很多数据库事故,锅不在操作人粗心,而是操作人没意识到自己用的语句属于哪个类别。比如DELETE和DROP看起来都是“删数据”,实际一个属于DML、一个属于DDL,性质完全不一样。前者改的是数据行,后者动的是表结构;前者在事务里可能回滚,后者执行完基本没有回头路。
我常用一个类比:一个数据库实例就像一套房子。DDL是砸墙、改格局、装楼梯,动的是建筑设计;DML是搬家具、扔杂物,动的是屋内摆设;DQL是你站在屋里打开手机拍照记录;DCL是给谁配钥匙、允许谁进哪个房间。买房子的人不会天天砸承重墙,但很多人写数据库操作时,却随手执行TRUNCATE和DROP,根本没意识到自己在砸墙。
下面这张表是我给团队新人培训时用的,直接建一个简单印象:
| 分类 | 核心动词 | 作用对象 | 典型风险 |
|---|---|---|---|
| DDL | CREATE、ALTER、DROP、TRUNCATE、RENAME | 库、表、索引、视图等结构 | 隐式提交、可能锁表、不可回滚 |
| DML | INSERT、UPDATE、DELETE | 表中的数据行 | 条件写错会全表更新/删除 |
| DQL | SELECT | 查询数据 | 本身不改数据,但慢查询会拖垮数据库 |
| DCL | GRANT、REVOKE | 用户权限 | 授权不当等于裸奔 |
| TCL | COMMIT、ROLLBACK、SAVEPOINT | 事务边界 | 不会用就丢数据 |
单看这张表还比较抽象,下面每类我都配合真实场景展开。
1.2 分类的本质是给“错误代价”分级
我review过上千条SQL,发现很多人写SQL时最大的问题是:不管什么语句,拿到就执行。想清空表数据就TRUNCATE,想删几行垃圾数据就DELETE,想删表就DROP。等出了事才发现DELETE删不掉是因为语句写错条件漏了,TRUNCATE一执行整表数据没了还回滚不了,DROP更不用说,整个表结构直接消失。
如果脑子里有分类意识,动手前就会多问一句:这条语句是改结构、改数据,还是查数据?改操作不可怕,可怕的是在错误分类下用了错误的回滚策略。DML可以用事务包住,出问题ROLLBACK;DDL不行,MySQL里的DDL会隐式提交当前事务,执行完再想反悔就晚了。
另一个常见分类混淆是TRUNCATE。很多人以为TRUNCATE是DELETE的升级版,其实MySQL把TRUNCATE归为DDL,它采用 drop table + recreate table 的方式清空数据,过程中还会隐式提交。这意味着你没法用事务包住TRUNCATE然后回滚。在我们实际项目里,清空一张表之前必须确认这张表是不是需要保留结构、是不是有外键引用、是不是生产库,任何一个环节没确认都可能酿成事故。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DDL:结构定义类语句的“高危”真相与线上避坑方法
2.1 一个完整的建表语句应该注意什么
DDL的第一步不是写CREATE,而是想清楚表结构。我见过太多表里全是varchar(255)、int字段甚至用float存金额。这里我给一个比较标准的订单表建表语句,也是后文反复会用到的表:
sql复制CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) NOT NULL COMMENT '订单号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付,1已支付,2已取消',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
字段类型的选择有几个容易被忽略的点。
主键用bigint而不是int,尤其业务量稍微大一点,int的21亿上限没有想象中那么远。订单号用varchar(64)并加唯一索引,因为订单号是业务唯一键,不允许重复。金额必须用decimal而不是float或double,二进制浮点数没法精确
