我有个随手记下来的“坑”,就是数据库里那点白给的空存。 那会儿写 SQL 的时候,总认定 `UPDATE` 要么 `DELETE` 都在一个地方跑,把数据删完了再改,要么先改再删中间人尴尬,最终人傻,不清理干净利落,报错。

后来在 `T-SQL` 里试了半天,发现真正的难题不在代码逻辑,而在执行盘算。 大量时候,你写的那个 `UPDATE` 实际上根本没动过数据。查执行盘算,一看 `id = 0`,目标表里的数据还在原地,只是没更新那一行,要么根本没执行那条语句。

这时候你认定自己在干活,结局数据比原来还旧,最终还得重跑,这操作简直就是自寻死路。 记得有一次,我在改合同表。

本来想把几个订单状态从“已审核”改成“已归档”,结局发现执行盘算里,只有写 `DELETE` 的那块代码跑了一遍,而更新状态的那几行,居然连检查是否更新都没干。

这是出于数据库自动生成了冗余的、索引覆盖的优化路径,别看快,但根本没用数据。 这难题如何解决?实际上挺好办的,就是把语句结构化,别让它去猜如何干。 最常见的是用 `MERGE INTO` 要么 `WITH` 子句,就连好办的 `SELECT` 配合 `UPDATE`。

比如我想批量把一批用户状态改成“灰度”,直接发代码,要是黄了了,直接回滚要么重跑,绝不依赖数据库那种“既然能删就删了”的乐观主义。 还有一种情况,就是你打算用存过程,本来想写个 `DECLARE @count INT = 0; UPDATE table SET ... WHERE count = 0; SELECT @count = @count + 1`,用个计数器来保证原子性。但这玩意儿对于 `UPDATE` 语句来说,往往就是个死循环要么花哨的装饰。

只要业务数据量大,这种逻辑挺好办被优化器骗过,害得它去走 `SELECT ... FOR UPDATE` 要么走索引扫描,而不是走你写的 `UPDATE` 语句。 这就回到了最核心的难题:为啥优化器不选你的语句? 这往往是出于你的语句忒“笨”了。

比如你在写个耗时操作,然后又顺便去删个旧数据,要么反过来。优化器有时候根本看不出来这两者之间是同步的,它可能认定你在操作表 A,然后操作表 B,结局表 B 的数据已经被污染了。 举个例子,我在处理一份超大的导出任务。

本来只想导出特定几个部门的记录,结局写了个死循环:先更新所有部门状态,然后导出,再更新,再导出。为了保险,我加了个计数器,要是计数器没归零,就说明重复操作了,便重新跑。但这根本没用。出于数据库优化器在分析执行盘算的时候,看到了“更新所有部门”这一步,它直接把这个步骤作为“删除所有部门”的优化方案来执行了。 你就连不需求手动改代码,优化器有时候会自动把 `UPDATE ... WHERE status = ?` 变成 `SELECT ... FOR UPDATE ... DELETE FROM`。别看性能提升挺可观,但逻辑上这就变成“先删后改”了。

要是你是在 `T-SQL` 里的 `MERGE` 语句里,那就更费事了。 真正的解决方案是削减数据库的推测。

不要让数据库认定自己能省事搞定,也不要让它认定你的逻辑忒复杂。 这就引出了存过程的一个特殊之处:它本身就是一个独立的逻辑单元。我们能够把它封装起来,让它只负责逻辑,不负责“数据整个性”要么“执行顺序”。 比如,我写一个存过程 `UpdateBatch`,它接收一个输入参数,里面放好所有需求更新的 `WHERE` 条件。我把这个参数直接传给代码,让代码自己去执行更新。

这样我就避免了那个“先更新再更新”的陷阱。 再比如,在处理数据清洗时,我能够写个过程,它先读取一批数据,然后一次性更新,最终扔出结局集。中间人我让它自己背锅,黄了了,要么更新了,都不影响主逻辑。 还有,大量时候我们幻想用 `INSERT INTO OUTPUT` 然后 `SELECT INTO` 来做一点操作,当作这样能做复杂事务。但这玩意儿在 `UPDATE` 上彻底是个笑话。它只能处理 `INSERT` 要么 `SELECT` 的一局部逻辑,对于复杂的 `UPDATE` 操作,数据库的 `OUTPUT` 机制根本不赞成那种精确的、可回滚的、基于行的更新逻辑。 故此,别去折腾那些听起来挺高级的语法了。 最好的做法,就是把所有需求逻辑、需求判断、需求同步的地方,都强调整个语句的原子性。 比如,我有个脚本,要把一批 `sysadmin` 用户改成一般/平平用户。原逻辑可能是个循环,每次查一个,更新一个,查一个。最终查出来发现,有一行没更新,说明中间人卡住了。 目前的做法是:直接写一个存过程 `ModifyUsers`。它的逻辑是:`INSERT` 临时表,`MERGE` 到目标表。

这就锁定了整个批次,要么全成功,要么全黄了,中间没有任何数据处于“脏”状态。 要么,更好办的,直接用 `SELECT ... FOR UPDATE` 语句本身。

这个语法本身就是一个 `UPDATE` 操作,它会自动查询数据,然后更新,然后释放锁。

要是你只是想改数据,就别搞那些复杂的计数器要么死循环。让这条语句自己当“执行者”,它不会去猜你的意图,它只会做它该做的事。 再举个具体的场景,想批量把产品库存从 `100` 降到 `0`。 那会儿: ```sql DECLARE @list NVARCHAR(MAX) = ...; -- 所有 SKU 的字符串 -- 这里有个坑:如何把 NVARCHAR 转成可为 `IN` 关键字用的单行字符串? -- 要是转成多行,执行盘算可能就不一样了。 -- 要是转成单行,优化器可能根本认不出来这是批量更新,而是一次性查询。 -- 便它可能直接查出所有 SKU,全量更新,要么只查了一局部。 ``` 后来: ```sql -- 直接把 SQL 写清楚 UPDATE Products SET Inventory = 0 WHERE SKU IN ('SKU001', 'SKU002', 'SKU003'); ``` 这就行了。

不用去造啥 `IF EXISTS` 要么复杂的 `SELECT ... INTO` 混合逻辑。数据库最精通的就是“好办”,只要逻辑本身不复杂,它就能给你最干净利落的路径。 有时候,我们总认定代码写得歪歪扭扭,数据库优化器就能把歪扭扭的代码变直。

实际上不然。

要是逻辑本身就挺乱,那优化器就变不直。 故此,存过程的本质,不是为了让你去绕弯子,而是为了让你明确告诉数据库:“这件事我负责到底,别让我去猜。” 要是你用存过程,就把它当成一个黑盒函数。输入参数,回结局,要么啥都不说。

不要往里塞那些能帮你省事的 SQL 技巧,比如用 `OUTPUT` 写条件判断,用 `MERGE` 写死循环。 你就老老实实写 SQL:`SELECT ... FROM`,要么 `UPDATE ... WHERE`。让数据库自己去拍板如何跑。 要是你非要搞个过程,就让它只改数据逻辑,别在逻辑里埋坑。

比方说,我想记录一个操作日志,顺便把数据更新。

那就 `INSERT` 日志表,再 `UPDATE` 主表。别搞啥 `SELECT INTO` 去读取日志再写回主表,那是做数据同步的,不是做数据更新的。 最终总结一下: 别试图用复杂的语法去骗优化器。 别在 `UPDATE` 里搞那个死循环计数器。 别指望 `OUTPUT` 能帮你解决“中间人毛病”。 把每一段逻辑都包裹在存过程里,要么干脆直接用一行 `UPDATE` 语句。 让数据库自己走它自己的路,别让用户代码去猜数据库的脑子。 实际上,大量时候我们写的代码,越是越复杂,越依赖那些“高级技巧”,就越是好办出错。最好办的 `UPDATE` 要么 `DELETE`,才是真正可靠的。 下次再写脚本,先回想一下,这段逻辑要是是别人看完直接执行,有没有可能出难题?要是有,那就用存过程把它包起来,要么干脆拆开成几段独立的语句。 数据干净利落了,逻辑才干净利落。别让用户去猜,让数据库自己把数据修得整规整齐。