入行快十年,我面试过不少自称“会 SQL”的候选人,第一轮大概率会听到一个标准答案:不就是增删改查嘛。这句话不能说错,但最多只答对了四分之一。SQL 的基本组成远不止 SELECT、INSERT、UPDATE、DELETE 这四个动词,它还包括建表、改权限、管事务等一系列动作。如果这个分类概念没理清楚,后面看建库脚本、权限脚本、批量数据脚本的时候,很容易把“语句类型”和“数据库对象”搅成一锅粥。
所以这篇文章我想把 SQL 的基本组成拆开讲透。不打算讲一堆用不上的理论知识,而是用我平时写脚本、看别人脚本、排查线上问题时最常用的那套框架,把 DDL、DML、DQL、DCL、TCL 五类语言分别是什么、各自负责什么、容易栽在哪里一次性说清楚。无论你是刚学 SQL 的新手,还是已经写了两三年 SQL 但没系统梳理过的人,这五类划分都值得在脑子里刻牢。
1. SQL 的基本组成不只是“增删改查”:先把五类语言分清楚
很多人把 SQL 理解成“查询语言”,其实全称 Structured Query Language 里的 Query 是历史命名习惯。今天我们在数据库里干的活,早就远远超出了“查询”的范围。你要建表、改表、删表,这不是查询,而是定义;你要给某个账号开放读写权限,这不是查询,而是控制;你要让一批操作要么全部成功、要么全部失败,这不是查询,而是事务。
所以正统的分类法,一般把 SQL 划分为五个组成部分。这个分类不是某个公司发明的,而是 T-SQL、各关系型数据库官方文档、以及 DBA 日常沟通里共同采用的基本框架。
1.1 一张速查表解决“都有什么”
我建议你把这张表存下来,比背十遍定义都管用。
| 分类 | 核心动词 | 一句话职责 | 生活类比 |
|---|---|---|---|
| DQL(数据查询语言) | SELECT | 从表里查数据 | 看仓库里有什么 |
| DML(数据操纵语言) | INSERT、UPDATE、DELETE、MERGE | 改表里的数据 | 往仓库搬货、挪货、扔货 |
| DDL(数据定义语言) | CREATE、ALTER、DROP、TRUNCATE、RENAME | 建库建表、改表结构、删表 | 盖仓库、改仓门、拆仓库 |
| DCL(数据控制语言) | GRANT、REVOKE | 谁能干什么 | 发门禁卡、收回门禁卡 |
| TCL(事务控制语言) | COMMIT、ROLLBACK、SAVEPOINT | 让一批操作有始有终 | 签收确认、退回重来 |
这张表里最容易被忽略的是 TCL。很多新人写 SQL 只关注前三类,等到线上误删数据、想反悔却找不到退路的时候,才知道 COMMIT 和 ROLLBACK 的价值。控制权限的 DCL 也经常被抠掉,因为自己开发环境里的账号普遍都是管理员权限,根本体会不到“收权限”的必要性。
1.2 为什么 DQL 被单独拎出来
你可能会问:SELECT 不也是操纵数据吗?为什么单独叫 DQL?
我的理解是这样的:DML 的本质是改变数据库里的数据状态,而 DQL 只负责读取和计算,理论上不产生持久化变化。虽然查出来的结果里可以带各种函数计算、排序、分组,但这些结果不会写回表。因此,DQL 在权限模型里经常被单独对待,比如一个报表账号可能只有 SELECT 权限,连 INSERT 和 UPDATE 都没有。这种拆分,在生产环境里直接决定了你能不能给下游同学开“只读权限”。
所以我跟新人讲 SQL 的基本组成时,一定会把“SQL 不是增删改查”这句话拆开,让它们理解 DQL 和 DML 的分界线。分清楚了,后面看 GRANT 语句、看事务日志、排查锁等待时,脑子的路径会清晰很多。
2. DDL:不是“删除数据的意思”,是数据库对象的施工队
第一次听 DDL 的人很容易望文生义,以为和 DELETE 有关。其实 DDL 全称 Data Definition Language,里面那个 Definition 指的是对数据库对象的定义。什么是数据库对象?表、视图、索引、库、触发器、存储过程,这些都属于对象。DDL 负责的对象生命周期管理,基本就是创建、修改、删除三条线。
2.1 建表、改表、删表:DDL 的基本动作
一个最日常的例子,建一张用户表:
CREATE TABLE user_info ( id BIGINT PRIMARY KEY, user_name VARCHAR(50) NOT NULL, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这张表建完之后,如果发现漏了一个字段,用 ALTER 补上:
ALTER TABLE user_info ADD COLUMN age INT;如果觉得某个字段长度不够,再改结构:
ALTER TABLE user_info MODIFY COLUMN user_name VARCHAR(100);有一天不需要这张表了,直接 DROP 掉:
DROP TABLE user_info;这几个动作能很直观地说明 DDL 的职责:表的出生、成长、死亡都由 DDL 管理。CREATE 是出生,ALTER 是成长,DROP 是死亡。TRUNCATE 稍微特殊一点,它不删表结构,只清空表里的全部数据,但因为它直接操作表存储的物理空间,通常也被归入 DDL,而不是 DML。
2.2 DDL 的实际影响:结构变更为什么比数据变更更“重”
我在刚工作的时候,觉得 ALTER TABLE 不就是加个字段吗?跟 UPDATE 一样简单。后来被线上故障教育过一次才明白,DDL 的“重”在于它改动的是表的结构定义,影响的是将来所有写入和查询的模式。
举个例子,一张几亿行的表要加一个新字段,不同数据库的处理方式差异巨大。有些会锁表,期间所有查询和写入都被堵住;有些底层是通过新建表、拷贝数据、再切换的方式实现“在线变更”,依然有额外的空间和时间成本。所以生产环境里执行 DDL 之前,DBA 通常要求先评估表数据量、索引量、业务低峰期窗口,而不是顺手敲一条命令就完事。
这里特别提醒第一次接触生产库的人:DDL 操作大多不是简单的“增删改查”,执行前一定要确认目标表是哪个环境、哪个库、哪个 schema。我在测试环境就见过有人把 DROP 和 DELETE 弄混,直接把一张配置表的结构给删了。配置表没有独立的备份文件,最后只能靠订单日志慢慢回补,那个下午过得极其痛苦。
3. DML 和 DQL:日常工作里最常碰到的“增删改查”到底怎么分工
如果把数据库比作一栋公寓,DDL 决定房间怎么隔、门开在哪,而 DML 决定屋里家具怎么摆,DQL 则是站在门口往里看,看看家具摆成了什么样。日常开发里,90% 的 SQL 语句都集中在这两类上面。
3.1 DML 动手改数据,DQL 只负责看
先看一组最简单的 DML:
INSERT INTO user_info (id, user_name, email) VALUES (1, '张三', 'zhangsan@example.com'); UPDATE user_info SET email = 'zhangsan_new@example.com' WHERE id = 1; DELETE FROM user_info WHERE id = 1;INSERT 是新增一行,UPDATE 是修改已有行的字段,DELETE 是删除指定行。三个操作都会改变表里的内容,这就是 DML 的共性。
再看 DQL:
SELECT user_name, email FROM user_info WHERE id = 1;这条语句不会改变 user_info 表,它只负责把符合条件的行捞出来。很多人写 SELECT 时可以一口气写上十几行 JOIN、子查询,思维非常活跃,但一碰到 UPDATE 或 DELETE 就无比谨慎,因为一旦条件写错,后果是实际的数据被改动或删除。这个谨慎是对的,而且应该延伸到 INSERT 和 DDL 上。
3.2 事务配合 DML:一次改多行别裸奔
DML 单独使用其实很危险。比如要批量把一批订单的状态从“待支付”改成“已取消”,理论上应该是一整个原子操作。如果中间某一行更新失败,最好让所有行都保持原样,否则就会出现同一批订单一半改了、一半没改的诡异状态。
处理这种场景,需要把 DML 包在事务里:
BEGIN; UPDATE orders SET status = 'CANCELLED' WHERE order_date < '2024-01-01' AND status = 'PENDING'; -- 核对更新行数 SELECT ROW_COUNT(); COMMIT;如果执行到一半发现条件写错了,或者更新行数远超预期,就不要 COMMIT,直接 ROLLBACK:
ROLLBACK;事务的意义就在于给 DML 一个“后悔按钮”。现在很多工具默认开启自动提交,每一条 DML 执行完就自动 COMMIT,这在生产环境跑批量脚本时是个隐患。我现在的习惯是:只要是在命令行里手工修改生产数据,一定先开一个显式事务,先少查几条,确认影响范围,再决定 COMMIT 还是 ROLLBACK。
3.3 SELECT 的执行顺序:理解 DQL 的钥匙
SELECT 看着是从表里拿数据,但数据库内部执行顺序和书写顺序完全不一样。这也是新手最容易困惑的地方。你写:
SELECT department, COUNT(*) FROM employee WHERE status = 'ACTIVE' GROUP BY department HAVING COUNT(*) > 10 ORDER BY department;从词法上看,SELECT 在最前面,但数据库实际执行时,第一步是从 FROM 读取表,接着 WHERE 过滤,然后 GROUP BY 分组,再去分组结果上 HAVING 过滤,之后才轮到 SELECT 计算列,最后才是 ORDER BY 排序。
这个执行顺序不是我编的,而是几乎所有关系型数据库优化器都会遵守的逻辑阶段。理解它最大的价值在于,你写 WHERE 条件时不能用 SELECT 里的别名,但 HAVING 或 ORDER BY 有时可以用别名,原因就和执行顺序有关。很多莫名其妙报错,最后都是卡在这条执行顺序上。
4. DCL 和 TCL:容易被忽视,但生产环境全靠它们兜底
前面两类每天写,后面这两类不一定每天写,但缺了它们,大型系统根本转不起来。DCL 管人,TCL 管事。
4.1 DCL:角色、授权、回收
DCL 全称 Data Control Language,核心动作是 GRANT 和 REVOKE。
GRANT SELECT ON orders TO report_user; REVOKE SELECT ON orders FROM report_user;第一句给报表账号开放 orders 表的查询权限,第二句把权限收回来。很多业务系统会有多个数据库账号,有的账号只读,有的账号可写,有的账号能建表,有的账号只能执行存储过程。这些账号能力的边界,全部靠 DCL 来划定。
权限管理这块,我最想强调一个词:最小授权。很多公司为了省事,直接给所有开发账号开一个超级管理员权限,刚开始确实省心,但出了事没人说得清是哪台机器、哪个操作、哪个账号干的。权限粒度越粗,事故定位越难。给账号 GRANT 的时候,只给当前任务必需的那部分权限,是成本最低的风险控制。
4.2 TCL:让“改错了”这件事可以反悔
TCL 全称 Transaction Control Language,核心命令包括 COMMIT、ROLLBACK、SAVEPOINT。前面在 DML 里已经接触过 COMMIT 和 ROLLBACK,这里把它们归到 TCL 更系统。
SAVEPOINT 是一个更细粒度的回滚点。有时候一个事务里要做好几件事,前面的没问题,后面的发现错了,但我只需要回退到中间某个点,而不是整个事务全部回滚:
BEGIN; INSERT INTO order_log(id, info) VALUES (1, 'start'); SAVEPOINT sp1; UPDATE orders SET status = 'PAID' WHERE order_id = 100; ROLLBACK TO SAVEPOINT sp1;这个技术的意义在于:大事务里保留中间存档,操作到后半段出了问题,不需要把前面成功的工作全部作废。虽然普通开发场景用得不多,但一旦你开始写复杂的批量存储过程,SAVEPOINT 就是一个救命工具。
对 TCL,还要知道一个基础事实:不是所有操作都能回滚。很多人以为 ROLLBACK 是万能后悔药,其实它只对 DML 生效。对 DDL,很多数据库在执行前或执行后会自动提交,或者直接不支持事务回滚。这一点特别重要,我在第六部分会专门展开。
5. 实操案例:用一段逻辑把 SQL 的基本组成串起来
说了这么多概念,不如来一段可以落地的完整例子。下面这个例子不依赖任何特定数据库,语法尽量通用,目的是让你看到五类 SQL 是怎么在一个真实过程中配合的。
5.1 设计一个小目标
假设我们要维护一个商品促销活动:先建一张商品表,再往里放两条商品数据,然后查询价格在某个区间的商品,接着更新其中一条的库存,最后演示删除一条记录、清理权限。
第一步,DDL 建表:
CREATE TABLE product ( id INT PRIMARY KEY, product_name VARCHAR(50), price DECIMAL(10, 2), stock INT );第二步,DML 插入:
INSERT INTO product(id, product_name, price, stock) VALUES (1, '键盘', 199.00, 500), (2, '鼠标', 99.00, 800);第三步,DQL 查询:
SELECT product_name, price FROM product WHERE price BETWEEN 100 AND 300;第四步,DML 更新库存,把键盘的库存扣掉 10:
UPDATE product SET stock = stock - 10 WHERE id = 1;第五步,DML 删除鼠标:
DELETE FROM product WHERE id = 2;第六步,DCL 给一个只读账号开放查询权限:
GRANT SELECT ON product TO report_user;这六步里,你基本能看到 DDL、DML、DQL、DCL 各自的身影。TCL 没有直接出现,是因为表面上是单条语句;但如果是真实业务,第二步到第五步往往应该在一个事务里,确保库存变更和删除操作要么成批成功,要么成批失败。
5.2 把语句按“基本组成”归类
做完上面的例子,可以再做一个训练:把一段陌生脚本按五类语言拆开。我平时看别人的部署脚本时,习惯先扫描所有语句,把它们分堆。
| 语句类型 | 判定方法 | 归入分类 |
|---|---|---|
| CREATE、ALTER、DROP、TRUNCATE | 是否在定义对象结构 | DDL |
| INSERT、UPDATE、DELETE、MERGE | 是否改变表里的数据内容 | DML |
| SELECT | 是否只读不改状态 | DQL |
| GRANT、REVOKE | 是否在控制账号权限 | DCL |
| BEGIN、COMMIT、ROLLBACK、SAVEPOINT | 是否在控制事务边界 | TCL |
这个归类练习看起来幼稚,但它能帮你在看一条语句时快速判断影响范围。比如线上突然出现锁等待,你第一反应就不会是去查 SELECT 权限,而是去看长事务里有没有 DML 没提交。
5.3 把脚本按“组成”合并成一份可维护的清单
另一个很实用的习惯是给脚本文件按组成部分命名或分区。我一般在一个部署目录下放四个文件:schema.sql 放 DDL,data.sql 放初始化 DML,query_examples.sql 放常用查询,security.sql 放 DCL。每个文件顶部写清楚哪一部分是干嘛的。团队协作用这种划分方式,出问题的时候定位非常快。
有人会问,TCL 放哪?答案是放进 data.sql。因为它和 DML 的关系太紧密,批量数据变更脚本里几乎离不开事务控制。把 COMMIT、ROLLBACK 跟对应的 DML 放一起,语义上才完整。
6. 我踩过的坑:SQL 分类在真实工作中最容易翻车的几个点
前面把五类 SQL 的基本组成讲完了,最后这部分是我最想写的。这些坑不是教材上会明确警告你的,而是我亲手踩过、或者在别人代码里见到过的真实教训。
6.1 DELETE 和 TRUNCATE 用混了,恢复难度完全不一样
DELETE 是 DML,删除的是符合条件的数据行,如果不小心删多了,只要事务没过、备份还在,还有机会回滚或恢复。TRUNCATE 是 DDL,直接清空整个表的数据,而且很多数据库里 TRUNCATE 执行后不会逐行记录日志,恢复难度比 DELETE 高好几个量级。
我记得有一次同事在测试环境清空一张配置表,为了图快,用 TRUNCATE,结果发现脚本连错了环境,把另一张线上维表给清了。那张表没有实时备份,后来是导了前一天凌晨的备份再手工补白天的增量,折腾了一整天。所以我现在对 TRUNCATE 特别敏感,执行之前必须反复确认表名和环境名。
6.2 DDL 在多数生产环境里不能随便回滚
DML 可以用事务回滚,但 DDL 在很多数据库里并不像 DML 那样可以轻松反悔。有的数据库执行 ALTER TABLE 时会隐式提交,也就是说你的 BEGIN 和 ROLLBACK 对它根本不起作用。有的数据库虽然支持在线 DDL,也不会自动把建表、改列的每一步都放进一个可回滚的事务里。
所以,改动数据库结构前一定要先做好备份和变更方案。我通常的做法是:把所有 DDL 语句写在变更单里,先在预发环境执行一遍,再准备一个“反向变更”脚本。比如新增字段后,如果发现不对,反向脚本就是删除这个字段。这一步看起来多写了一堆代码,但真到要恢复的时候,比临时想方案可靠得多。
6.3 GRANT 权限要按最小化给,REVOKE 要及时收敛
我自己早期犯过一个错:给一个关联方数据库账号授了整库的 SELECT,又顺手授了 UPDATE。后来对方反馈,本来只是做数据校验,但因为有 UPDATE 权限,他们的运维脚本误改了几行业务字段。责任虽然不完全在我,但权限给太宽确实提供了出事的土壤。
现在我的权限申请单里,一定会明确写清楚:哪个库、哪个表、哪个动作、有效期多久。如果只是临时排查问题,周期到了就主动 REVOKE。权限这种东西,宁可在需要时再申请,也不要一开始就铺一大片。
6.4 视图、存储过程、索引不属于第四类 SQL
经常有人在分类时把视图、存储过程、索引当成本身就是一种 SQL 类型。这是个典型的误解。视图是数据库对象,不是 SQL 分类;存储过程也是一组 SQL 逻辑的封装,它可以包含 DDL、DML、DQL、TCL;索引更不是独立语言,它通常由 CREATE INDEX 创建,本质属于 DDL 的范畴。
为什么这个误解值得纠正?因为当你被别人问“这个存储过程是 DDL 还是 DML”时,你会意识到对象和语句是两个维度的概念。对象是“数据库里存的什么东西”,语句是“对对象执行什么操作”。分类时一定要看动词类型,而不是看它出现在什么位置。
6.5 SQL 标准与“方言”的差异
SQL 有国际标准,但各大数据库都实现了自己的方言。也就是说,同一套五类 SQL 的基本组成框架下,具体语法可能不完全一致。比如某些数据库用 LIMIT 控制返回行数,另一些用 TOP;分页方式也不一样;事务的隔离级别名称也有差异。
我个人的处理方法是:基础的五类分类用标准概念去理解,具体语法永远以当时连接的数据库文档为准。不要因为在一个库里写熟了,就笃定所有库都这么写。正因为有这种差异,把“组成”和“实现细节”分开看才更有意义。理解通用的组成框架,换数据库时只需要改语法细节,而不需要重新理解业务逻辑。
最后再分享一个小习惯:每次拿到一份陌生 SQL 脚本,我第一件事不是读里面的具体业务,而是先扫描动词,看它里面涉及哪些分类。只要把 DDL、DML、DQL、DCL、TCL 的边界划清楚,再复杂的脚本也会变得一目了然。这个习惯我用了很多年,确实比从第一行顺着读下去高效得多。