1. 从一条 Update 语句说起为什么值得整理一份语法合集做 .NET 开发的同学尤其是用 SqlSugar 做过几个真实项目之后大概率会有这样一个感受查询语法花样多更新语法反而容易被忽略。但实际开发里更新操作的复杂度一点不比查询低——单表更新、条件更新、批量更新、联表更新、更新返回列表、更新时忽略某些字段、更新时自动填充时间和业务字段这些都是真实业务里天天要碰的东西。我最初接触 SqlSugar 的 Update 功能时也是靠翻文档、翻社区帖、甚至看源码才把各种写法捋清楚。后来项目越做越多代码里到处是Updateable的各种花式调用遇到别人问“SqlSugar 更新怎么写”我发现自己脑子里其实有一套“按场景查用法的索引”这套索引比去看一遍官方文档更实用因为它是从真实业务反推出来的。这篇文章就是我整理的 SqlSugar Update 更新语法合集覆盖从最基本的db.Updateable单表更新到条件更新、批量更新、联表更新、更新指定列、更新忽略列、更新时执行 SQL 函数或实体函数、更新返回影响行数和数据列表、大数据分批更新、枚举与 JSON 字段更新、多租户下的更新注意事项等十几类场景。适合正在用 SqlSugar 做项目、想系统掌握更新写法的朋友参考也适合刚从 Dapper 或其他 ORM 转过来的新手对照着“抄作业”。先说清楚一件事SqlSugar 的版本更新频率不高但每个小版本都在修正细节。这篇文章基于常见稳定版本的写法整理如果你用的版本比较老或者恰好某个 API 在新版里有变化以你对项目实际依赖的版本来验证为准。提示SqlSugar 的实体类一般继承自基类的场景比较多本文示例尽量用最简形式实际使用中请把实体类定义补全。另外所有示例代码都省略了SqlSugarClient/Db的初始化部分默认为你已经有一个全局可用的Db实例。2. 基础更新语法Updateable 的两种核心写法2.1 按实体对象更新最简单但坑也最多SqlSugar 最基础的更新方式就是直接传入一个实体对象var student new Student { Id 1, Name 张三, Age 20, CreateTime DateTime.Now }; var result Db.Updateable(student).ExecuteCommand();这段代码执行后SqlSugar 会根据实体的主键Id生成一条 Update 语句把实体里所有有值的字段都更新一遍。这里有个关键点称为“整体更新”只要实体属性有值就会出现在 SET 子句中。实际开发中最常见的坑就在这里要是你只改了Name但student对象是从别的地方查出来的里面Age、CreateTime都有值那么这一次更新会把所有字段都覆盖写一遍。这在很多时候不是你想要的效果但如果你就是想整行覆盖那这个写法是最省事的。另一个很容易踩的坑是“实体属性为 null 或默认值会不会被更新”。默认情况下SqlSugar 会跳过null值字段不会把它更新到数据库。比如var student new Student { Id 1, Name 李四, Age null // 这个属性是 null }; var result Db.Updateable(student).ExecuteCommand();生成的 SQL 不会包含Age null只会更新Name。这是默认行为可以理解为“SqlSugar 帮你把 null 条件过滤掉了”。如果你确实需要把某个字段置空需要用到UpdateColumns显式指定列这个后面讲。2.2 指定条件更新不依赖主键的更新方式有时候你并不知道主键只知道业务条件比如“把所有ClassId 2的学生年龄加一岁”。这时候用实体对象就不合适了得用条件更新的写法Db.UpdateableStudent() .UpdateColumns(it new Student { Age it.Age 1 }) .Where(it it.ClassId 2) .ExecuteCommand();这个写法理解起来很直白你要更新Student表更新的列是Age Age 1条件是ClassId 2。SqlSugar 会把它翻译成UPDATE Student SET Age Age 1 WHERE ClassId 2这种按条件更新的方式比“先查出来再改实体再保存”在性能上高很多因为少了一次 Select直接拼接 SQL 执行。对于批量的、基于规则的更新比如状态流转、计数增减、时间批量修改就应该优先用这个写法而不是先查后改。2.3ExecuteCommand与ExecuteCommandAsync返回值到底代表什么ExecuteCommand返回的是影响行数affected rows这个数字在大多数情况下就是更新了几行数据。新版 SqlSugar 也支持ExecuteCommandAsync用法几乎一样只是多了个await。有一个细节值得注意影响行数为 0不代表“更新失败”也可能是“条件匹配了但数据没变化”。尤其当你更新时把某个字段的新值和旧值设成一样有些数据库驱动不会把这个计入受影响行数。所以项目里不要用影响行数去判断“值是否真的变了”要用它去判断“是否有行被更新操作匹配到”。3. 指定列更新的几种姿势UpdateColumns 的精髓3.1 只更新指定的几个字段业务需求最常见的场景是一个表单只允许用户改“姓名”和“手机号”其它字段要保持原样。实体整体更新会把所有字段覆盖掉这时就必须用UpdateColumns。var student new Student { Id 1, Name 王五, Phone 13800001111, Age 18, // 不想更新这个字段 CreateTime DateTime.Now // 也不想更新这个字段 }; var result Db.Updateable(student) .UpdateColumns(it new { it.Name, it.Phone }) .ExecuteCommand();注意UpdateColumns里用匿名对象来选择列这个写法很符合直觉。生成的 SQLUPDATE Student SET Name Name, Phone Phone WHERE Id 1这种写法最大的价值就是“防御性”即使实体里有脏数据、有多余字段只要你在UpdateColumns里没指定它就不会被更新到数据库。我个人的习惯是只要不是“整行覆盖”这种明确场景一律用UpdateColumns显式指定字段不要依赖“其它字段正好没值”这种巧合。3.2 忽略指定字段与 UpdateColumns 互补的 IgnoreColumns反过来有时候你想“除了这个字段其它都更新”比如除了CreateTime不更新其它都更新var result Db.Updateable(student) .IgnoreColumns(it new { it.CreateTime }) .ExecuteCommand();这里的语义和UpdateColumns正好相反。两者可以组合使用吗一般来说不建议同时混用因为很容易搞混。而且如果既UpdateColumns又IgnoreColumns代码可读性会突然下降以后的维护者会看着代码发懵。真要控制列就用一种方式要么明确“我需要更新哪几列”要么明确“我排除哪几列”不要两个都用。3.3 更新时执行函数在 SET 里调用 SQL Server 函数或实体函数有些业务需要更新的值不是直接来自实体属性而是来自数据库函数。比如“更新时间直接取数据库当前时间”比从应用服务器取时间更可靠因为应用服务器和数据库服务器可能存在时钟偏差。SqlSugar 支持在UpdateColumns里直接写SqlFunc相关操作或者直接用DateTime.Now。Db.UpdateableStudent() .UpdateColumns(it new Student { LastLoginTime DateTime.Now, LoginCount it.LoginCount 1 }) .Where(it it.Id 1) .ExecuteCommand();这里DateTime.Now会被 SqlSugar 翻译成参数也就是应用服务器时间。如果你更想用数据库时间SqlSugar 提供了SqlFunc.GetDate()之类的函数映射具体函数名看你用的 SqlSugar 版本和数据库类型SQL Server 下一般有对应映射。用数据库时间的好处是如果数据库和应用在不同机器上时间戳更统一坏处是这会让 SQL 不再“参数化”执行计划缓存方面会有细微差别但一般业务完全可以接受。3.4 忽略列的高级用法根据条件动态忽略有些更复杂的场景里“哪些列需要更新”是可变的。比如一个通用的审核接口普通管理员只能更新Status超级管理员可以更新Status和Remark。SqlSugar 的IgnoreColumns支持条件判断吗具体写法取决于版本有的版本支持使用if之类的逻辑在表达式树外面做分支var updater Db.Updateable(entity); if (isSuperAdmin) { updater.IgnoreColumns(it new { it.CreateTime }); } else { updater.IgnoreColumns(it new { it.CreateTime, it.Remark }); } var result updater.ExecuteCommand();这种方式虽然不优雅但可读性极佳把分支逻辑摆在明面上比硬塞进表达式树里强多了。实际项目中我推荐这种朴素写法别为了“炫技”搞复杂抽象。4. 批量更新与带条件复杂更新4.1 同时更新多条记录传入实体集合SqlSugar 的Updateable支持传入集合生成多条 Update 语句或一条多值更新取决于数据库和版本。var list new ListStudent { new Student { Id 1, Name A }, new Student { Id 2, Name B }, new Student { Id 3, Name C } }; var result Db.Updateable(list).ExecuteCommand();这个写法在实体集合数量不大时很顺手。注意它默认还是按主键匹配所有实体必须有主键。集合超过一定量比如几百条时建议不要用这种方式而是使用“根据条件更新”或者使用后面要讲的“大数据分页更新”避免一条一条更新把数据库拖垮。4.2 联表更新Where 里面带 Join 条件联表更新在 SqlSugar 里的实现路径和单表更新不太一样。若你的场景是“更新学生表但条件是关联班级表的某个字段”最直接的方式是笛卡尔积式UpdateableWhere但这种方式在 SqlSugar 里需要借助Updateable的Where支持多表查询吗实际上不同版本实现的便捷度差异较大。粗略讲如果你使用的是较新版本SqlSugar 支持Updateable配合UpdateColumns直接写多表更新Db.UpdateableStudent() .UpdateColumns(it new Student { Status 0 }) .Where(it it.ClassId SqlFunc.SubqueryableClass() .Where(c c.Name 文科班) .Select(c c.Id)) .ExecuteCommand();利用子查询来过渡联表条件是一种稳定、跨版本兼容性好的写法本质上是“在Where里用SqlFunc.Subqueryable把关联关系以子查询表达”。如果你用的数据库很古老或者 SqlSugar 版本对Updateable直接 Join 支持得不好子查询方案是最稳的兜底。另一种更暴力的方式先查出目标 Id 集合再用Where(it list.Contains(it.Id))更新。这个方案简单但在数据量特别大时会有性能风险因为IN集合太大时会生成很长的 SQL。在实际项目里我觉得子查询方案比内存中先查 Id 集合更推荐。它避免了一次额外的数据传输而且数据库优化器处理子查询通常比较聪明。4.3 根据条件批量更新一条 SQL 解决一类数据真正的批量更新不是“循环发更新”而是“按条件一条 SQL 更新多行”。这类操作建议写成“判断条件 赋值表达式”的模式Db.UpdateableOrder() .UpdateColumns(it new Order { Status 3, PayTime DateTime.Now }) .Where(it it.CreateTime DateTime.Now.AddDays(-30) it.Status 1) .ExecuteCommand();这个模式非常实用。比如你做订单超时自动关闭、优惠券过期失效、积分批量过期等等都可以用这种一条更新的方案搞定十几万条数据也只需要一条 SQL效果远好于“查出列表再遍历更新”。数据库层面也会友好得多减少日志量和网络往返。4.4 分页批量更新大数据量下的稳健方案如果数据量真的很大几十万甚至上百万一条 Update 锁定的行太多会让数据库长时间持有锁甚至拖垮线上业务。这时就该考虑“分页更新”策略。核心思路是每次只更新一小批比如 1000 条循环处理直到更新完。循环里如何“选”这一批数据靠主键范围或者游标条件// 伪代码思路 var lastId 0; while (true) { var ids Db.QueryableOrder() .Where(it it.Status 1 it.Id lastId) .OrderBy(it it.Id) .Take(1000) .Select(it it.Id) .ToList(); if (ids.Count 0) break; var count Db.UpdateableOrder() .UpdateColumns(it new Order { Status 2 }) .Where(it ids.Contains(it.Id)) .ExecuteCommand(); lastId ids.Max(); }注意这里用Id lastId做增量推进而不是每月一次Skip/Take这样可以避免深分页偏移导致的全表扫描。每次处理一批数据库的压力可控即使中途出问题重启后也能从lastId继续具备断点续跑能力。这条“批次 游标”的方案是我处理超过 20 万条数据更新的首选。你要是只处理几千条那就没必要上这个写法直接一条更新也行。5. 更新返回数据的特殊场景分析5.1 更新后返回实体列表输出参数与 Output 的取舍SqlSugar 较新版本提供了ExecuteReturnEntity或类似能力用于在更新后把受影响的行返回回来。如果你要从更新后的行里拿到数据库计算的值比如触发器修改的某个字段、数据库默认值、getdate()生成的时间这种能力就很关键。var entity Db.Updateable(student) .UpdateColumns(it new Student { Status 1 }) .Where(it it.Id 1) .ExecuteReturnEntity();这个 API 的底层会根据数据库方言采用不同的实现SQL Server 会利用OUTPUT子句其他数据库可能用事务 查询配合。这个能力很强大但是要注意如果你用的数据库或 SqlSugar 对应版本不支持这种语句可能会退化成“先更新再查询”的模式这样会多一次查询也意味着“非原子”。关键业务要自行评估这种非原子性是否可接受。5.2 更新返回影响行数判断成功的正确姿势很多小伙伴用ExecuteCommand()返回的int来判断“是否更新成功”其实我的经验是判断“有没有行受影响”是可靠的判断“值有没有变”不可靠。var rowCount Db.Updateable(entity) .UpdateColumns(it new { it.Name }) .Where(it it.Id id) .ExecuteCommand(); if (rowCount 0) { // 说明没有匹配到行要提示用户目标可能已被删除 }这种判断在“乐观锁”实现里尤其重要。比如更新文章时带上Version条件var rowCount Db.UpdateableArticle() .UpdateColumns(it new Article { Title 新标题, Version it.Version 1 }) .Where(it it.Id 1 it.Version 5) .ExecuteCommand(); if (rowCount 0) { // 版本冲突让用户重新拉取最新数据 }这是极其经典且实用的乐观锁方案。它依赖的就是“受影响行数”这个精准信号比“先查后改再更新”在高并发下强得多减少了并发覆盖。5.3 更新返回主键某些场景下的便利能力严格来说更新不太需要“返回主键”因为你既然能更新说明你已经知道主键了。但有一种情况例外通过业务条件更新你并不清楚究竟更新了哪些主键。如果后续日志或审计需要记录这些主键可以用返回主键列表的方法如果版本支持var ids Db.UpdateableStudent() .UpdateColumns(it new Student { Status 2 }) .Where(it it.ClassId 5) .ExecuteReturnList(it it.Id);这种能力在“批量标记已读然后记录已读用户列表”之类的场景很像“查出来”的一种替代品。不过要小心返回大量主键列表会产生额外开销最好规模较小、或这些数据本身就是日志所需的否则不如先查出来处理好业务逻辑再更新。6. 表别名、更新锁与特殊字段处理6.1 更新时加锁处理并发更新的关键细节数据库的更新锁是很多人容易忽略的点。默认情况下执行UPDATE时数据库会对涉及的行加排他锁其他事务写操作会被阻塞这个行为是数据库自己保证的。但对于“先查再更新”这种非原子流程锁的粒度就是你业务代码里多出来的那段时间窗口。SqlSugar 里有没有显式支持UPDLOCK/FOR UPDATE之类的能力在你需要锁住一批行防止并发重复领取的时候可以用Db.Ado直接执行原生 SQL 或者使用事务隔离级别配合。SqlSugar 的事务用法很成熟把更新逻辑包在事务里配合合适隔离级别能做到“行锁一直持有到事务提交”。// 伪代码示意实际要包 try-catch Db.Ado.BeginTran(); try { var count Db.UpdateableCoupon() .UpdateColumns(it new Coupon { OwnerId 1, Status 2 }) .Where(it it.Status 1 it.ExpireTime DateTime.Now) .ExecuteCommand(); if (count 0) { Db.Ado.RollbackTran(); return 已被抢完; } Db.Ado.CommitTran(); } catch { Db.Ado.RollbackTran(); throw; }这种“先更新抢名额而不是先查再抢”的方式是并发扣减、领取优惠券、库存预占的标准做法。之所以高效是因为UPDATE本身有行锁保护配合事务能保证“扣成功才提交扣失败就回滚”。用ExecuteCommand返回的行数作为是否抢占成功的信号比“应用层锁”更可靠。6.2 表别名更新解决同一个表关联自身的场景不好的消息是SqlSugar 的Updateable对表别名的支持并不像查询那么方便。如果你遇到“同一张表根据子集更新另一个子集”的场景比如把每个班级里年龄最大的学生标记为班长我的建议是分两步第一步用查询把符合条件的 Id 找出来var ids Db.QueryableStudent() .Where(it it.Age Db.QueryableStudent() .Where(s s.ClassId it.ClassId) .Max(s s.Age)) .Select(it it.Id) .ToList();第二步再使用Updateable按ids.Contains更新Db.UpdateableStudent() .UpdateColumns(it new Student { IsMonitor true }) .Where(it ids.Contains(it.Id)) .ExecuteCommand();这种拆两步的方案虽然不是最优但很好地绕开了同表关联更新在 ORM 表达上复杂的问题。数据量不大时完全够用代码也容易理解。6.3 枚举、JSON 与自动填充字段的更新细节枚举字段更新时SqlSugar 默认会存字符串还是 int取决于实体配置。默认多数情况是把枚举当成 int 存储更新时赋值枚举值即可Db.UpdateableStudent() .UpdateColumns(it new Student { Gender GenderEnum.Female }) .Where(it it.Id 3) .ExecuteCommand();JSON 字段则在较新版本里支持直接更新整个 JSON 字符串或者通过SqlSugar提供的 JSON 函数做局部更新针对特定数据库如 SqlServer 的 JSON 函数或 MySQL 的 JSON_SET。这里我的建议是如果只是整体覆盖某个 JSON 列直接用实体属性赋值最省事如果要做 JSON 内部局部更新最好直接写原生 SQL因为不同数据库的 JSON 函数差异太大ORM 封装得再好也可能陷入“低效表达”的困境。自动填充字段如CreateTime、UpdateTime、CreateUserId等在更新时是否会自动更新取决于你是否使用了 SqlSugar 的DataFilter或实体特性配置。如果配置了“更新时自动填充UpdateTime”那么用Updateable更新时即使你没显式赋值SqlSugar 也可能自动填充。这个行为有时是惊喜有时是惊吓——如果你不想自动填充就要在IgnoreColumns里显式排除它。关键提醒默认情况下我只把CreateTime设成“插入时填充”把UpdateTime设成“插入和更新时填充”。但并非所有项目都是这套规则你要明确了解实体配置否则数据更新后看UpdateTime没变化会一脸茫然。7. 存储过程与原生 SQL 更新ORM 之外的兜底方案7.1 SqlSugar 调用存储过程执行更新部分老系统和复杂业务里更新逻辑写在存储过程里。SqlSugar 调用存储过程的常见写法如下var result Db.Ado.UseStoredProcedure() .ExecuteCommand(proc_UpdateStudentStatus, new SugarParameter(studentId, 1), new SugarParameter(status, 2));这里UseStoredProcedure()告诉 SqlSugar 接下来执行的是存储过程参数用SugarParameter传递。如果你需要拿到存储过程的输出参数var pId new SugarParameter(studentId, 1); var pStatus new SugarParameter(status, 2); var pOut new SugarParameter(result, null, true); // 输出参数 Db.Ado.UseStoredProcedure().ExecuteCommand(proc_UpdateStudentStatus, pId, pStatus, pOut); var outValue pOut.Value;这类写法在“ORM 无法优雅表达复杂更新逻辑”时非常有用。但我的建议是如果存储过程只是包了一层简单的更新 SQL趁早把它改成 ORM 写法的代码可维护性提升一个档次如果存储过程里有大量事务和临时表那保留存储过程是合理的ORM 层就当个搬运工。7.2 原生 SQL 更新与 SqlSugar 的 Ado 能力有些情况下SqlSugar 的更新表达式没办法完全覆盖所有 SQL 方言特性比如 SQL Server 的UPDATE ... FROM ... JOIN语法、 PostgreSQL 的UPDATE ... RETURNING、MySQL 的UPDATE LOW_PRIORITY等。这时可以直接用Db.Ado.ExecuteCommand执行原生 SQLvar sql UPDATE student s INNER JOIN class c ON s.class_id c.id SET s.status 0 WHERE c.name className; var result Db.Ado.ExecuteCommand(sql, new SugarParameter(className, 文科班));原生 SQL 表达力最强但也意味着你会失去 SqlSugar 的实体映射、参数检查和跨数据库兼容能力。能用 ORM 解决就先别用原生 SQL但遇到性能瓶颈或复杂 JOIN 更新时不要硬拗 ORM原生 SQL 是完全合法的选择。8. 多租户、分库分表场景下的 Update 注意事项8.1 多租户按租户过滤更新多租户架构里最担心的是“跨租户更新”。如果你在 SqlSugar 里配置了全局过滤器Updateable在某些版本里会自动带上租户过滤条件。这个设计的本意是防止你手滑更新到别的租户的数据但也可能成为“你明明只更新一个租户结果过滤条件把目标行搞没了”的坑源。例如// 假设全局过滤器 TenantId 1 Db.UpdateableStudent() .UpdateColumns(it new Student { Status 2 }) .Where(it it.Id 1) .ExecuteCommand();如果Id 1的这条记录属于租户 2它根本不会被过滤器放行实际影响行数就可能为 0。出现这种情况时不要立刻认为“程序错了”要检查过滤器和当前租户上下文是否匹配。8.2 分表场景下的更新路由分表按年份分表、按业务 ID 取模分表后ORM 更新能不能自动路由到正确的物理表取决于你的分表策略配置。SqlSugar 的Updateable对分表支持的细节在不同版本差异较大。我实际测试过的方案是如果分表键是查询条件之一SqlSugar 能识别并路由到正确的表如果更新条件里没有分表键ORM 往往会遍历多张物理表这很容易造成性能问题甚至出现“更新多张表”的行为。所以分表场景下的 Update我的实操建议是一定在Where条件中带上分表键。比如订单表按年分表更新时一定要带上CreateTime的年份条件或直接按分表键定位。别指望 ORM 能“智能地”帮你把所有表都找一遍那可能是一场灾难。8.3 更新审计字段大数据量下的性能取舍很多系统有严格的审计需求更新记录时必须记录操作人、操作时间、变更前后值。如果数据结构是“主表 审计日志表”常规思路是更新主表再插入审计日志。这在一个事务里做虽然安全但性能一般。如果用的是“用 Updateable 直接更新主表”然后“用 Insertable 插入审计日志”整体还是两条语句要注意保持在一个事务里。如果批量更新几千上万行审计日志也会等量膨胀此时可以考虑把审计日志的插入放到消息队列异步处理或者只在业务层记录“关键字段变更”而不是所有字段。总之审计是个“横切关注点”别把所有重量都塞进 Update 链路里。9. 实际项目中的踩坑记录与排查思路9.1 更新时出现多余条件导致影响行数为 0我遇到过好几次这样的问题明明数据在更新却返回 0。排查发现实体类里某些属性被配置成了主键或者唯一约束SqlSugar 自动把它们加进了WHERE条件。比如var student new Student { Id 1, IdCard 110xxx }; Db.Updateable(student).ExecuteCommand();如果IdCard在实体配置里被标记成了“更新必须条件”实际生成的 SQL 可能会是UPDATE Student SET ... WHERE Id Id AND IdCard IdCardIdCard一旦不匹配就影响 0 行。遇到这种情况先看一眼生成的 SQL用 SqlSugar 的ToSql()方法或日志把语句打出来就能快速定位。SqlSugar 绝大多数情况下不会自动给你加这种多条件但配置不当的话可能发生所以养成就地看 SQL 的习惯很重要。9.2 更新大字段导致性能明显下降有一次项目里把文章正文大字段几千字的 HTML放在主表里业务上每次修改标题都顺手把整个实体Updateable了结果数据库日志量暴涨更新慢得用户骂娘。后来优化为UpdateColumns只更新标题字段正文不动性能立刻回稳。这个案例说明一个道理更新量最小化是优化更新性能的第一原则。别把实体里所有字段一股脑提交只更新的确有变化的字段。尤其在字段多、大字段多、行数大的情况下差别极为明显。9.3 更新 SQL 拼接错误字符串里包含引号或特殊符号虽然 SqlSugar 参数化做得比较好但偶尔有同学从外部传入字符串比如富文本内容里面带单引号更新时虽然参数化不会打断 SQL但某些特殊字符可能在别的环节出问题。我的建议是更新前对文本长度、空值、非法字符做统一校验。ORM 参数化是兜底不代表你可以完全不做输入验证。9.4 排查更新问题的通用步骤当你遇到“更新没生效、更新报错、更新极慢”三类问题时按这个顺序排查能省一半时间用.ToSql()或日志把 SqlSugar 生成的 SQL 打出来确认 SQL 逻辑是否符合预期。检查WHERE条件尤其是实体主键、全局过滤器、租户过滤器是不是意外参与。检查事务是否在事务里事务隔离级别是否有异常是否回滚了。检查数据库锁如果有长时间锁等待需要看锁等待事件和阻塞源。检查配置实体是否配置了自动填充、忽略列、更新时字段映射等。用这套流程大多数更新问题都能在十分钟内定位。不要一上来就怀疑 SqlSugar 的 bug先怀疑自己的 SQL 和配置这是做 ORM 调试的基本素养。10. 工具选型与版本差异不同数据库的 Update 差异一览10.1 主流数据库的更新语法差异SqlSugar 的一个亮点是“差异化封装”但对更新语句来说不同数据库的 SQL 方言差异依然存在ORM 已经帮你消化了很多但仍有边界。下面是根据我的实际经验整理的差异点SQL Server支持UPDATE ... OUTPUT、UPDATE ... FROM锁机制完善SqlSugar 返回实体列表在 SQL Server 下很舒服。MySQL更新锁默认是行级锁但UPDATE ... WHERE的条件如果走不到索引会退化成表锁或间隙锁这是高并发更新最需要警惕的。Oracle有UPDATE ... RETURNING不支持LIMIT分页更新需用ROWNUM或者借助 UPSERT 思路。PostgreSQL支持UPDATE ... RETURNING锁机制严格但对更新性能影响也直观。SQLite更新是表级写锁并发写性能弱大批量更新要谨慎容易“database is locked”。做项目前最好确认团队选用的数据库再针对性设计更新方案。SqlSugar 虽然做好了适配但你在设计业务逻辑时依然要尊重各数据库的脾气。10.2 SqlSugar 版本选择与更新方式稳定性SqlSugar 的版本迭代中Updateable这个入口一直很稳基本不需要改代码。但有几点值得注意老旧的 4.x / 5.x 版本和较新的 5.1.x 版本ExecuteReturnEntity等 API 可能差异很大。如果你的项目绑定了较老版本宁可基于该版本的源码去确认 API也不要盲目照抄网上的新语法。升级版本时重点看更新日志里的“Breaking Changes”尤其是关于更新列和返回值的调整。实际上我见过不少项目因为 “copy 了别人的代码但没注意版本”在更新语法上踩了软钉子。这也侧面说明整理一份贴合自己项目版本的语法合集是值得的。10.3 从 Dapper 或其他 ORM 转来的迁移要点如果你从 Dapper 转过来最不适应的可能是“表达式树”这一套。Dapper 习惯写 SQL映射靠手写SqlSugar 的Updateable把 SQL 生成藏起来了但你想看 SQL 也可以随时“打破沙锅问到底”。我的建议是在初学阶段所有更新语句写完后先.ToSql()看一眼生成的 SQL慢慢建立“表达式到 SQL”的映射直觉。只要建立起这个直觉以后写更新语法会非常快。11. 更新语法的性能优化建议与最终心得11.1 三条铁律第一条能用一条更新解决的绝不用循环更新。循环更新是数据库性能杀手应用层看不出问题数据库层日志、锁、网络往返都会指数级上升。第二条能更新最小列就不更新整行。尤其在大字段、多字段场景下更新列数量直接影响日志量和执行时间。第三条需要并发安全时用“条件更新 行数判断”代替“先查后更新”。这是乐观锁的核心也是最容易见效的并发优化手法。11.2 用 ToSql 检验设计每次写完Updateable多花十秒调用.ToSql()检查生成的 SQL能发现很多隐藏问题。比如var sql Db.UpdateableStudent() .UpdateColumns(it new Student { Status 2 }) .Where(it it.Id 123) .ToSql(); Console.WriteLine(sql);看到实际生成的 SQL 后你会更清楚 SqlSugar 到底干了什么也更清楚自己的写法是否准确。这个习惯不需要花多少时间但能极大减少“以为更新了其实没更新”的尴尬。11.3 最后的个人建议SqlSugar 的Update语法并不复杂难点在于“根据业务选择正确的更新模式”。我在实际项目中最常使用的组合就是UpdateColumnsWhereExecuteCommand既明确更新列又精确控制范围并且利用返回行数做业务判断。但这不代表其它写法没用联表、子查询、返回实体、存储过程兜底都有它们各自的舞台。希望这份语法合集能成为你手边常备的参考。以后不管是在改订单状态、批量更新积分、还是做乐观锁版本控制都可以直接翻开找到对应的写法照着落地少踩几个坑。