讲一个我观察很久的现象:很多干了两三年数据库开发的人,SQL的增删改查写得飞起,表连接、子查询、窗口函数也门儿清,但你问他“数据控制”层面有哪些动词,他一愣,然后说“不就是GRANT和REVOKE吗”。这其实是很多人的知识盲区——SQL语言并不是只有查询和修改,它还有一套专门负责“谁能动数据”和“动了怎么算数”的动词体系。
这套体系在不同教材里被归纳成“数据控制9动词”,简单点说就是:权限控制里的DCL(Data Control Language)加上事务控制里的TCL(Transaction Control Language),一共九个核心动词。这九个动词在真实项目中出现的频率极高,小到给报表账号开查询权限,大到处理线上数据回滚、修复半夜的数据事故,全都要靠它们兜底。
这篇文章就打算把这九个动词彻底拆开讲一遍,包含它们各自的作用、在SQL Server/MySQL/Oracle里的差异、常见的坑,以及我在实际运维和开发中踩过的雷。适合数据库管理员、后端开发、数据运维和正在准备SQL面试的同学,看完基本能把这套体系串起来。
1. 先搞懂9动词的分类逻辑:为什么是“9”不是“3”也不是“12”
1.1 SQL语言的整体布局与数据控制的定位
SQL这门语言被业界分成几个族类,最经典的分法是DDL、DML、DCL、TCL四类。DDL管结构,CREATE、ALTER、DROP都是它的地盘,相当于给房子画图纸、改承重墙;DML管数据操作,SELECT、INSERT、UPDATE、DELETE是主力,相当于在房子里搬家具、布置房间;而DCL管权限,GRANT和REVOKE是代表,相当于给房子配门禁卡;TCL管事务,COMMIT、ROLLBACK是代表,相当于记账时确认账目还是作废重来。
数据控制9动词,正好是把DCL和TCL合在一起看的结果:
- DCL三个动词:GRANT(授权)、REVOKE(撤销)、DENY(拒绝)。
- TCL六个动词:BEGIN/START TRANSACTION(开启事务)、COMMIT(提交)、ROLLBACK(回滚)、SAVEPOINT(保存点)、SET TRANSACTION(设置事务属性)、LOCK TABLE(锁表)。
这里我得说清楚一个事儿——“9动词”这个提法并不是SQL标准里明文规定的分类,而是工程界在教学和实践中形成的归纳方式。SQL标准的文档不会特意告诉你“数据控制一共9个动词”,但你在真正的生产环境里摸爬滚打一圈就会发现,日常工作里和数据安全、数据一致性相关的操作基本全被这九个词覆盖了。把GRANT和COMMIT放在同一个“数据控制”维度下看,并不奇怪:权限控制管的是“谁有资格碰数据”,事务控制管的是“碰了之后数据能不能算数”,本质都是在给数据“上保险”。
1.2 主流数据库在这个框架下的口径差异
我们做工程不能只看理论分类,还要看落地实现。这九个动词在不同数据库里的语法和默认行为差异极大,如果不注意,在这家写好的脚本搬到另一家直接报错。
以事务控制为例,我列几个最典型的差异:
- SQL Server:默认自动提交模式,每条语句执行完自动提交。但你用BEGIN TRANSACTION手动开启事务后,必须由COMMIT或ROLLBACK收尾,否则事务会一直挂着。
- MySQL:InnoDB引擎默认autocommit=1,单条语句即时提交。用START TRANSACTION可以显式开始一个事务,注意DDL语句在MySQL里会隐式提交,这个后面细讲。
- Oracle:默认没有自动提交的概念,执行完DML后必须手动COMMIT,否则退出客户端时数据会回滚。很多从MySQL转到Oracle的人第一周就栽在这里。
- PostgreSQL:默认也是每条语句自动提交,但如果你在一个事务块里出了错,整个事务块会进入“aborted”状态,必须ROLLBACK才能继续。
权限控制也一样。SQL Server有DENY,直接一票否决;MySQL、Oracle、PostgreSQL都没有DENY,想拒绝权限只能不授权或者用REVOKE回收。所以如果你在SQL Server里养成了“GRANT之后发现不对就用DENY补一刀”的习惯,换个数据库就要重新适应。
2. 权限控制三剑客:GRANT、REVOKE、DENY的完整实操
2.1 GRANT授权的正确姿势与最小权限原则
GRANT的职责是给用户或角色开通权限。它的基础语法在不同数据库里大同小异,SQL Server写法:
-- 给用户分配订单表的查询和插入权限 GRANT SELECT, INSERT ON dbo.Orders TO user_dev; -- 给用户分配存储过程的执行权限 GRANT EXECUTE ON dbo.sp_CalcDailyReport TO user_etl; -- 给整个Schema的查询权限 GRANT SELECT ON SCHEMA::dbo TO user_readonly;MySQL的写法:
-- 给应用账号分配company库的所有表查询权限 GRANT SELECT ON company.* TO 'app_read'@'192.168.1.%'; -- 分配带刷新权限的账号 GRANT SELECT, INSERT, UPDATE ON company.orders TO 'app_write'@'192.168.1.%'; FLUSH PRIVILEGES;Oracle的写法:
-- 给用户分配hr模式下employees表的查询和更新权限 GRANT SELECT, UPDATE ON hr.employees TO scott;这里最核心的实践原则只有一个:最小权限原则。我见过太多事故,都是因为图省事直接给应用账号开了db_owner或者给了ALL PRIVILEGES,结果应用被SQL注入后,攻击者拿着这个高权限账号为所欲为。跑批脚本只需要INSERT和UPDATE,就绝对不给DELETE;报表账号只需要SELECT,就绝对不给写权限;DBA的账号和应用的账号必须物理分开。
同时要特别提防WITH GRANT OPTION这个选项。它表示允许用户把自己拥有的权限再转授给其他人。看起来方便,实际上会让权限关系变成一张失控的网。A授权给B并带了这个选项,B又授权给C,等你想收回A的权限时,还要考虑B和C的权限是否要级联回收。我在实际项目里的建议是:除非你有非常明确的委派场景,否则别用WITH GRANT OPTION,权限统一由DBA入口下发,这是最稳妥的安全基线。
2.2 REVOKE与DENY:一个撤销,一个拒绝,优先级完全不同
REVOKE是GRANT的逆操作,把之前授出去的权限再撤回来:
-- SQL Server REVOKE SELECT, INSERT ON dbo.Orders FROM user_dev; -- MySQL REVOKE SELECT ON company.* FROM 'app_read'@'192.168.1.%'; -- Oracle REVOKE SELECT, UPDATE ON hr.employees FROM scott;注意REVOKE有一个坑:在SQL Server里,REVOKE默认不仅撤销GRANT授予的权限,还会撤销DENY拒绝的权限。我用一句话帮助你记:REVOKE是在“权限开关”上恢复默认状态,清除之前的所有设置,而DENY则是把开关拧到“强制关闭”,而且是焊死的。
DENY是SQL Server特有的语法。它会拒绝一个权限,并且它的优先级高于GRANT。什么意思?假设某个用户同时属于两个角色,角色A被GRANT了查询权限,角色B被DENY了查询权限,最终这个用户是查不了的,因为DENY会“一票否决”。这个特性必须时刻记住,否则排障时会被绕晕。
三者关系我用一个场景帮你串起来:
| 操作序列 | 最终效果 |
|---|---|
| 只执行GRANT | 允许访问 |
| 只执行DENY | 拒绝访问 |
| 先GRANT后REVOKE | 回到默认,拒绝访问 |
| 先GRANT后DENY | 拒绝访问,DENY优先级压过GRANT |
| 先DENY后REVOKE | 回到默认,拒绝访问 |
我自己曾经在给一个项目迁移账号权限时,旧环境用的是“GRANT后DENY补漏”的方式,新环境用脚本统一REVOKE再GRANT,结果因为忘了REVOKE会把DENY也清掉,导致有些本来不该放开的数据在新环境里直接裸露了。后来我给自己定了一条规矩:权限脚本里必须显式写清楚GRANT和DENY的状态,绝不依赖“REVOKE之后应该是什么状态”这种隐式逻辑。
3. 事务控制六虎将:从BEGIN到COMMIT的完整链路
3.1 为什么事务控制也属于数据控制
很多人理解事务,知道它能保证ACID,但不太理解它和数据控制的关系。你可以这样想:权限控制决定“谁能进这个房间”,事务控制决定“房间里的账本能不能被改花眼”。不管是权限还是事务,最终目标都是让数据保持正确、可信、可追溯。
举个例子,你做一个订单系统,用户下单后要同时扣库存、写订单表、加支付流水,这三步必须“同生共死”。如果扣了库存但订单没写进去,库存就无端少了;如果订单写了但支付流水没生成,财务对账就永远不平。这些场景靠单条SQL是解决不了的,必须靠事务把所有操作包成一个不可分割的原子单元。
这里就出来了一个很重要的概念:事务边界。什么时候开始?什么时候结束?结束是成功提交还是失败回滚?这三件事决定了一个事务的完整生命周期。而六个动词里,BEGIN管开始,COMMIT管成功结束,ROLLBACK管失败结束,SAVEPOINT管局部回滚,SET TRANSACTION管事务的隔离属性,LOCK TABLE管并发控制。六兄弟各司其职,拼在一起才是一个完整的控制体系。
3.2 BEGIN、COMMIT、ROLLBACK、SAVEPOINT的实战拆解
先看最基础的一组。SQL Server里是这样:
BEGIN TRANSACTION; UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 1001; INSERT INTO order_table (order_id, product_id, user_id) VALUES (50001, 1001, 20001); COMMIT TRANSACTION;如果中间任何一步出错,你可以选择回滚:
BEGIN TRANSACTION; UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 1001; -- 这里假设插入订单表失败了 INSERT INTO order_table (order_id, product_id, user_id) VALUES (50001, 1001, 20001); -- 发现错误,回滚整个事务 ROLLBACK TRANSACTION;这个例子很简单,但实际工作中的事务往往更长。长事务里出问题,如果只能整体回滚,前面几十条正确的操作也得全部作废,这在批处理场景里非常浪费。于是就有了SAVEPOINT。它相当于在事务中间插了一个个“存档点”,出错时不用从头再来,只回滚到最近的存档点就行。
SQL Server的写法是SAVE TRANSACTION,MySQL和Oracle的写法是SAVEPOINT:
-- MySQL语法 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; SAVEPOINT after_deduction; UPDATE account SET balance = balance + 100 WHERE user_id = 2; -- 假设这里发现给用户2加钱加错了,只回滚到保存点 ROLLBACK TO SAVEPOINT after_deduction; COMMIT;执行完这段代码后,用户1扣掉的100元不会回滚,只有用户2加钱的这步被撤销了。这个能力在复杂的资金处理、状态机流转场景下特别实用。
这里我得单独强调一下事务开启的入口差异。MySQL里除了START TRANSACTION,也可以用BEGIN关键字,但更建议用START TRANSACTION,因为MySQL的BEGIN和BEGIN...END(存储过程里的代码块)容易让人产生混淆。SQL Server则是BEGIN TRANSACTION。Oracle最特殊,它没有显式的“开启事务”语句,当你执行第一条DML语句时事务其实已经自动开始了,后面只需要在合适的地方写COMMIT或ROLLBACK就行。
3.3 SET TRANSACTION与LOCK TABLE:并发控制的最后两道闸门
SET TRANSACTION的作用是设置当前事务的隔离级别或访问模式。隔离级别决定了事务在并发环境下能看到多少别人的“脏数据”。MySQL语法:
-- 把当前事务隔离级别设置为读已提交 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置为可重复读 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 设置为串行化 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;SQL Server的写法略有不同,它把ISOLATION LEVEL的语法直接挂在SET后面:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;隔离级别由低到高分别是READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。级别越低,并发性能越好,但越容易读到别人没提交的数据(脏读)、越容易出现同一查询前后结果不一致(不可重复读)、越容易在查询范围内出现新的行(幻读)。级别越高,数据越稳,但并发度越低、锁竞争越激烈。
我在实际调优时给过一个建议:大部分业务系统用READ COMMITTED就够了,MySQL默认的REPEATABLE READ在绝大多数场景其实也不会成为瓶颈,但不建议普通业务去碰READ UNCOMMITTED,除非是纯报表、能容忍行数据短暂不一致的场景。没事别为了“性能提升”去调隔离级别,很多线上问题就是这么调出来的。
LOCK TABLE则是直接对表加锁,它的使用场景其实非常有限。MySQL支持显式的表锁:
-- 对orders表加写锁,事务结束自动释放 LOCK TABLE orders WRITE; -- 对orders表加读锁 LOCK TABLE orders READ;在ORM框架和数据库连接池大行其道的今天,我不太建议应用层主动去LOCK TABLE,因为在连接池模式下,锁的持有时间很容易失控。如果一个锁因为事务未提交而长时间不释放,整个表的所有写入都会被堵住,几分钟就能把线上业务打挂。大多数场景下,你应该依赖数据库引擎自己的行锁和MVCC机制,让引擎去管并发,而不是人工去锁表。
4. 高频翻车现场与排查速查表
4.1 权限相关的典型翻车现场
我在带新人和做数据库巡检时,反复见过几个权限类的坑,这里集中列一下。
第一个坑是“授了权还是查不了”。排查思路要从下往上捋:先看用户是否真的登录到了对的实例或库,再看用户所属角色是否有冲突,再看数据库层面是否有DENY覆盖了GRANT,再看是否有Schema级别的默认拒绝。SQL Server里经常出现用户名对但default schema不对,导致查表时解析不到对象的情况,此时用ALTER USER user_dev WITH DEFAULT_SCHEMA=dbo改一下就好。MySQL里则要注意,授权时IP段如果写成了'192.168.1.%',实际连接是从别的网段来的,权限怎么都不生效。
第二个坑是“REVOKE之后反而更危险”。前面说过,在SQL Server里REVOKE会把之前设置的DENY也清掉。很多人写权限回收脚本时,下意识认为REVOKE=“收走权限”,结果它把“拒绝”这道防线也拆了,原本被DENY的敏感对象反而能被访问了。正确做法是:如果既要收走授权又要保持拒绝状态,必须显式执行DENY,而不是只用REVOKE。
第三个坑是“存储过程越权”。这个比较隐蔽,但也比较重要。存储过程默认以调用者权限执行,如果你的存储过程内部访问了调用者本不该访问的表,那么用户只要执行存储过程,就能间接读到敏感数据。排查时务必检查存储过程是不是该加WITH EXECUTE AS OWNER,或者反过来确认这个存储过程是否暴露了超出预期的数据通道。
第四个坑和SQL注入相关。如果应用层的数据库账号拥有过大的权限,比如拥有DROP、TRUNCATE权限,一旦代码存在SQL注入点,攻击者就能直接删库。权限最小化不只是一句安全口号,它是真实能救命的防护层。权限控制层面做得越细,SQL注入的实际危害性就越小。
4.2 事务相关的典型翻车现场
事务类的坑,很多比权限类更隐蔽。
第一个坑是“开了事务忘记提交”。代码里写了BEGIN TRANSACTION,但因为异常分支提前return了,没走到COMMIT,连接也没关,事务就一直挂着。数据库端看到的现象是什么?session一直Sleep,但锁不释放,其他会话全堵在某个表上。这时候排查SQL Server可以用这条语句:
SELECT session_id, transaction_id, elapsed_time_seconds FROM sys.dm_tran_session_transactions ORDER BY elapsed_time_seconds DESC;MySQL可以用这条:
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started;看到超长事务后,如果确认业务已经不需要它了,直接KILL掉对应会话,但要提醒自己查一下应用代码为什么没提交。这类问题治标不治本,改代码、加超时、加try-finally才是正路。
第二个坑是“DDL隐式提交”。MySQL和SQL Server对DDL的处理不同,MySQL里执行一条ALTER TABLE,隐式提交当前事务,前面步骤做的DML全被固化,后面想ROLLBACK已经来不及了。所以在写批量脚本时,尽量保证设计是先做完所有DML操作,再统一做DDL变更;如果需要DDL先执行,就要提前把事务拆开。
第三个坑是“ROLLBACK把并发和日志一起拖垮”。超长事务的回滚本身也是一次极其耗时的操作,回滚期间产生大量Undo日志,其他事务的读可能因为版本链过长而变慢。我处理过一个极端案例:一个跑了近40分钟的事务回滚时产生了十几GB的Undo日志,主库IO被打满。后来我们引入了分批提交的改造思路,把大事务拆成多个小事务,每批几百条就提交一次,虽然牺牲了一些原子性(可以通过补偿逻辑弥补),但整体稳定性大幅提升。
第四个坑是“隔离级别引发的死锁和延迟”。READ COMMITTED和REPEATABLE READ在加锁策略上有本质区别,前者在读数据时加的是共享锁但读完即释放,后者在读数据时加间隙锁或next-key lock直到事务结束。同一个事务里先查再改,在REPEATABLE READ下锁的范围可能比预想大很多。改成READ COMMITTED之后,很多死锁往往不治而愈。但这不意味着REPEATABLE READ有错,关键还是看业务能不能容忍不可重复读。
另外说一下排查工具的配合。不要等出了问题才被动去查,平时就要把监控跑起来。我一般会在测试环境准备一份SQL模板,定期拉一下当前长时间运行的事务、被锁阻塞的会话、权限变更记录。SQL Server里查阻塞可以看sys.dm_exec_requests的blocking_session_id,MySQL可以直接看performance_schema里的data_lock_waits。提前储备这些排查手段,事到临头才不会慌。
5. 围绕9动词的学习路径与个人经验
如果你现在对数据控制这块还比较陌生,我的建议是先别急着背语法,而是把九个动词按“管权限”和“管事务”两条线在脑子里分好层。线一:GRANT、REVOKE、DENY,解决的是“谁能动数据”;线二:BEGIN/START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT、SET TRANSACTION、LOCK TABLE,解决的是“动了之后怎么保证数据安全”。两条线交织在一起,就是完整的SQL数据控制体系。
学的时候一定要环境实操。纸上谈兵只会让你对语法有印象,但不会让你理解“为什么提交失败后数据没变”“为什么另一个会话能看到这个会话的数据”。装一个SQL Server或者MySQL,开两个查询窗口,一边开事务一边查询,反复试几遍,你很快就能建立肌肉记忆。
最后分享一个我觉得很有价值的习惯:在项目里建立一个数据库权限和事务操作的checklist,把常见风险点列进去。比如“授权前确认是否满足最小权限”“事务代码中是否所有异常路径都覆盖了ROLLBACK”“是否确认没有DDL隐式提交风险”“是否在主从环境里误在从库上执行了写事务”等等。每次上线前对照检查一遍,长期坚持下去,你会发现很多线上的幺蛾子早就被挡在了发布之前。