尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C# SqlSugar 批量查询:参数上限、分片与结果拼装实践
1. 先把“批量查询”这件事说清楚上周帮一个做后台系统的朋友看接口请求体里传进来两千多个订单 ID要一次性把这些订单的编号、金额、状态全取出来。他原来的写法很朴素foreach循环一个一个First()去数据库拿。压测时这个接口 P99 直接冲到两秒多日志里两千多条 SQL 刷屏DBA 那边也开始有意见。后来换成一次c# sqlsugar 批量查询同样的数据量四十来毫秒出结果SQL 从两千条变成两条。差距不是调参调出来的是查询方式和调用次数压根不在一个量级。所以先给不太熟的朋友把概念捋清楚批量查询不是某个具体方法的名称而是一类做法指的是用尽量少的数据库往返把一批主键、一批复合条件、一批区间范围对应的数据一次性取回来。它解决的核心痛点有三个网络往返次数太多、SQL 解析和编译次数太多、以及由此带来的接口响应时间不可控。适合谁只要你的代码里出现过“列表里拿着 ID 去库里查详情”“导入时逐行校验是否存在”“报表要按一批条件捞数据”这类场景无论你是刚学 ORM 的新手还是写了好多年仓储的老手都值得把这套东西整理一遍。SqlSugar 在这件事上给的能力其实挺全的In、Contains、Expressionable、ToPageList、Includes、MergeTable、Ado.SqlQuery各有各的适用面。但麻烦也在这儿——能选的太多很多人随手挑一个就上线结果在数据量涨到几千、几万的时候撞上参数上限、内存暴涨、结果顺序对不上这些坑。后面我按“怎么选、怎么写、怎么排错”的顺序把这一串东西拆开讲。1.1 循环单查到底亏在哪里很多人觉得循环查只是“多几条 SQL 而已”其实成本构成完全不是线性的。每条 SQL 从客户端发出到服务端执行中间有 TCP 往返、连接池借还、数据库端语法解析、权限校验、执行计划查找最后才是真正的索引查找。索引查找可能只花 0.05 毫秒但前面那一串开销加起来轻松就 0.5 到 2 毫秒。两千次累加就是一两秒而且这些时间全是串行的等待CPU 空转。换成一条IN查询这些固定开销被摊薄到一次真正的索引查找还是两千次但它发生在数据库内部走的是内存里的 B 树遍历单次成本极低。这就是为什么批量查询的收益常常是几十倍——省掉的不是计算是通信。还有一层容易被忽略的连接池压力。循环查如果每次都是独立上下文、独立连接并发一高池子被占满后面的请求全在排队等连接。批量查询天然把占用时间压缩池子的吞吐能力直接上一个台阶。所以我的判断标准很简单同一个请求里对同一张表查询次数超过 5 次就该考虑改成批量。1.2 什么情况下不该硬上批量批量不是银弹有两种场景我会主动放弃。第一种是一次性要处理几十万条记录、但每条都要走复杂的业务规则、还要回写状态的任务。这种应该走分批拉取加流式处理一次拿两千条处理完再拿下一批而不是一条 SQL 把三十万行全塞进内存——ListT里三十万个对象光是 GC 就够你喝一壶。第二种是入参本身极度稀疏、命中率很低的场景比如用户随手贴了五千个编号实际库里只有三个存在这时候批量查询虽然快但你可能更需要先做格式校验和去重把无效输入挡在数据库外面。还有个细节值得提批量查询对数据库的瞬时压力其实比循环查更大。循环查是涓涓细流批量查是集中冲击一条IN里塞几千个值如果字段没索引或者索引选择性差数据库可能直接走全表扫描把 CPU 打满。所以上批量之前先确认查询条件上有合适的索引这一点比选哪个 API 重要得多。1.3 SqlSugar 里能做批量查询的几个入口我把常用入口按适用场景列一下方便你对照自己的需求挑入口适用场景关键特点In/Contains按主键或唯一键取一批生成IN (...)最常用Expressionable条件动态拼装组合AND/OR支持条件开关ToPageList需要总数或分页展示一次拿数据 总数Includes主表带子表底层会拆成多条 SQLMergeTableIN列表极大转成临时表关联绕开参数上限Ado.SqlQuery需要手写 SQL 或复杂统计完全可控但要自己防注入WithCache变化少、读多写少的字典表走本地缓存减少往返这张表建议存下来后面每一节我都会对应展开。需要提醒的是SqlSugar 各版本之间 API 有过调整比如某些重载的参数类型、MergeTable的可用性在不同 5.x 版本上表现不完全一样。下面给的代码都是我在若干 5.x 版本上实测跑通的你在自己项目里落地前先在测试库上跑一遍确认签名对得上别直接复制到生产。2. 按主键批量取数的写法与参数化真相这是使用频率最高的场景也是坑最集中的地方。表面上一行代码就完事但只要入参数量一上去报错、超时、结果不对全都会冒出来。2.1 In 和 Contains 两种写法的实际差异最常见的两种写法是这样var ids new Listlong { 1001, 1002, 1003 }; // 写法一In var list1 db.QueryableOrder().In(o o.Id, ids).ToList(); // 写法二Contains var list2 db.QueryableOrder().Where(o ids.Contains(o.Id)).ToList();两种写法最终生成的 SQL 都是WHERE Id IN (p0,p1,p2)执行计划一样性能没有区别。差别在工程层面In的重载是 SqlSugar 自己封装的传List或数组都行写起来更直白Contains是 C# 原生语法好处是能嵌在更复杂的表达式里比如Where(o ids.Contains(o.Id) o.Status 1)可读性更好。我个人的习惯是单纯按一批键取数用In条件里还要夹别的东西用Contains。两者混用没问题但同一个仓储里最好统一一下风格否则后面接手的人得来回切换思维。还有个细节In传空列表会怎样不同版本行为不完全一致有的直接返回空集合有的生成IN ()这种非法 SQL 报错。稳妥做法是在方法入口就拦住if (ids null || ids.Count 0) return new ListOrder();这一行能省掉很多线上莫名其妙的异常。2.2 SQL 参数上限是绕不过去的硬墙这是批量查询最经典的坑也是我见得最多的线上事故来源。SQL Server 单条语句的参数上限是 2100 个Oracle 的IN列表上限是 1000 个超过就直接报错不是慢是直接失败。用In或者Contains列表里的每个值都会被参数化成一个独立参数所以你传 3000 个 ID就是 3000 个参数稳稳超限。报错信息通常是“传入的请求参数过多”或者“ORA-01795: 列表中的最大表达式数为 1000”这一类。第一次遇到会很懵代码看着完全正常怎么就报错了。原因就在这儿。解决办法有两个方向。第一个方向是分片把大列表切成若干个不超过安全阈值的小块逐块查询再合并结果。这个方案通用、可靠、不依赖数据库特性是我默认推荐的。第二个方向是换执行方式比如用MergeTable把列表转成临时表做关联或者拼成UNION ALL的子查询绕开参数化。这两个后面单独讲。分片的阈值怎么定我会留余量SQL Server 用 1000 到 1500Oracle 用 500 到 800。为什么不填满因为实际查询里往往还有别的条件也要占参数位比如状态、时间范围、租户 ID加上去就可能顶到天花板。留一半余量是最省心的做法。2.3 分片查询的落地代码先把通用分片方法写出来这个后面很多场景都能复用public static class BatchQueryHelper { /// summary /// 按指定键列批量查询自动分片避免超出数据库参数上限 /// /summary public static ListT QueryByKeysT, TKey( ISqlSugarClient db, ExpressionFuncT, TKey keySelector, IEnumerableTKey keys, int chunkSize 1000, ExpressionFuncT, bool extraWhere null) where T : class, new() { if (keys null) return new ListT(); // 去重能直接减少参数数量很多时候入参里重复项不少 var keyList keys.Where(k k ! null).Distinct().ToList(); if (keyList.Count 0) return new ListT(); var result new ListT(keyList.Count); foreach (var slice in ChunkList(keyList, chunkSize)) { var query db.QueryableT().In(keySelector, slice); if (extraWhere ! null) { query query.Where(extraWhere); } result.AddRange(query.ToList()); } return result; } private static IEnumerableListTKey ChunkListTKey(ListTKey source, int size) { for (var i 0; i source.Count; i size) { yield return source.GetRange(i, Math.Min(size, source.Count - i)); } } }调用起来就一行var orders BatchQueryHelper.QueryByKeysOrder, long( db, o o.Id, request.OrderIds, 1000, o o.IsDeleted false);这里有几个我实际踩过才加上的细节。第一是Distinct()入参里重复 ID 很常见去重后参数数量可能直接腰斩分片次数跟着减少。第二是Where(k k ! null)尤其TKey是string的时候前端传个空字符串或者null进来拼进IN里会生成IN (NULL)结果不可预期。第三是new ListT(keyList.Count)预设容量几千条的时候能少几次扩容拷贝虽然收益不大但白给的为什么不拿。注意Chunk方法在 .NET 6 及以上有原生 LINQ 支持如果你的项目还在 .NET Framework 或者 .NET Core 3.1得用上面那种手写循环的方式别直接写keyList.Chunk(1000)编译不过。3. 结果拼装才是真正的活查询取回来只是第一步真正决定代码质量的是怎么把结果和入参对上。这一块我见过太多写歪的表面能跑一出边界问题就是空指针或者数据错位。3.1 结果顺序丢失是常态必须记住一件事IN查询返回的结果不保证顺序。你传的是[3, 1, 2]数据库返回的可能按主键升序[1, 2, 3]也可能按索引扫描顺序取决于执行计划。如果你后面直接拿list[0]去对应第一个入参妥妥地错位。有人会想用ORDER BY强制排序比如ORDER BY CHARINDEX(...)之类的花活。我劝你别这么干一是索引可能失效二是每个数据库的写法都不一样三是可读性极差。正确做法是在内存里重建顺序用字典做映射var map orders.ToDictionary(o o.Id); var ordered request.OrderIds .Where(id map.ContainsKey(id)) .Select(id map[id]) .ToList();这段代码同时解决了顺序和缺失项两个问题入参里有但库里没有的 ID会被Where过滤掉顺序完全按入参走业务上层不用关心数据库返回什么样。ToDictionary那里要注意如果查询结果里主键有重复理论上主键唯一但联表或者视图场景可能出现会抛“重复键”异常。稳妥一点可以用循环赋值后面遇到再处理。3.2 用字典做 O(1) 匹配批量查询最常见的后续操作是“给列表补字段”。比如你有一个订单列表每个订单要关联商户名称、商品标题、优惠券信息。如果每个订单单独去查又是 N1。正确姿势是先收集所有需要的 ID批量查回来做成字典然后一次遍历填充// 一次查出所有商户 var merchantIds orders.Select(o o.MerchantId).Distinct().ToList(); var merchantMap BatchQueryHelper.QueryByKeysMerchant, long(db, m m.Id, merchantIds) .ToDictionary(m m.Id, m m.Name); // 填充 foreach (var order in orders) { order.MerchantName merchantMap.TryGetValue(order.MerchantId, out var name) ? name : string.Empty; }这里用TryGetValue而不是merchantMap[id]是因为数据可能被物理删除或者有脏数据导致关联不上直接索引会抛KeyNotFoundException一个脏数据能让整个接口 500。这种防御性写法看着啰嗦但线上真能救命。3.3 缺失项和重复项怎么判定业务上经常需要知道“哪些 ID 没查到”。比如批量导入时要告诉用户哪几行数据在库里不存在。这时候字典就派上用场var existIds map.Keys.ToHashSet(); var missing request.OrderIds.Where(id !existIds.Contains(id)).Distinct().ToList(); if (missing.Count 0) { // 组装提示信息注意别一次返回几千个 ID 给前端 return Result.Fail($有 {missing.Count} 条数据不存在示例{string.Join(,, missing.Take(5))}); }ToHashSet比List.Contains快一个数量级数据量上千时差别很明显。另外提示信息里只取前几个做示例别把几千个 ID 全塞进响应体前端渲染会卡死用户也看不懂。提示如果业务要求“顺序严格按入参且缺失项要占位”可以用Dictionarylong, Order配合入参遍历缺失的填null或者占位对象。我做过一个对账功能就是这么处理的前端表格的行数必须和上传文件一致不能少行只能空着。4. 动态条件与复合键的批量查询真实的业务查询很少只有IN一个条件往往是“一批 ID 状态 时间范围 可选的关键词”。这时候条件怎么拼、拼完生成的 SQL 长什么样直接决定查询能不能走索引。4.1 Expressionable 组合条件SqlSugar 的Expressionable是我最常用的工具之一它解决的是“条件可选”这个痛点。传统写法用if-else拼Where嵌套几层之后可读性崩盘。用Expressionable可以平铺下来var exp Expressionable.CreateOrder(); exp.And(o o.IsDeleted false); exp.AndIF(status.HasValue, o o.Status status.Value); exp.AndIF(startTime.HasValue, o o.CreatedAt startTime.Value); exp.AndIF(endTime.HasValue, o o.CreatedAt endTime.Value.AddDays(1)); exp.AndIF(ids ! null ids.Count 0, o ids.Contains(o.Id)); var list db.QueryableOrder() .Where(exp.ToExpression()) .OrderBy(o o.CreatedAt, OrderByType.Desc) .ToList();AndIF的第一个参数是布尔条件为false时这段表达式根本不会被拼进 SQL所以不会产生WHERE 11 AND Status NULL这类垃圾条件。这一点很重要因为多余的条件会影响优化器选择索引。时间范围那里我特意写成 startTime和 endTime.AddDays(1)而不是BETWEEN。BETWEEN是闭区间对于带毫秒的时间字段会漏掉当天最后一秒的数据这个坑我踩过对账时发现少了几条记录查了半天才发现是边界问题。用左闭右开的写法最稳。4.2 复合主键怎么批量匹配有些表没有单一主键比如订单明细表用(OrderId, LineNo)做联合主键。这种批量查询就麻烦一点IN只能作用于单列。有三种解法第一种数量少的时候直接拼ORvar exp Expressionable.CreateOrderItem(); foreach (var key in keys) { var oid key.OrderId; var line key.LineNo; exp.Or(i i.OrderId oid i.LineNo line); } var list db.QueryableOrderItem().Where(exp.ToExpression()).ToList();这种方式简单直白但只适合几十个键多了之后 SQL 会长得离谱解析成本飙升。第二种按第一个维度批量查内存里再过滤。如果keys里的OrderId去重后只有几十个但明细有几百条那就只按OrderId做IN查回来再用HashSet(long, int)过滤var orderIds keys.Select(k k.OrderId).Distinct().ToList(); var allItems BatchQueryHelper.QueryByKeysOrderItem, long(db, i i.OrderId, orderIds); var keySet keys.Select(k (k.OrderId, k.LineNo)).ToHashSet(); var matched allItems.Where(i keySet.Contains((i.OrderId, i.LineNo))).ToList();这是我最推荐的方案一次查询搞定内存过滤成本可以忽略。第三种临时表关联适合几千个复合键的场景后面第七节讲。4.3 只取需要的列别 SELECT *批量查询为了图快很多人直接QueryableT().ToList()把整行拉回来。如果这张表有text、varchar(max)、blob这类大字段几千行的数据量能轻松到几十兆序列化和 GC 的成本远超查询本身。养成习惯只取需要的列var list db.QueryableOrder() .In(o o.Id, ids) .Select(o new OrderBriefDto { Id o.Id, OrderNo o.OrderNo, Amount o.Amount, Status o.Status }) .ToList();Select投影之后生成的 SQL 只查这四列网络传输量可能只有原来的十分之一。我做过对比一张带两个大文本字段的表两万行数据全字段查询耗时 1.8 秒、内存占用 400 多兆投影四列之后 200 毫秒、40 兆。差了一个数量级就这么简单的一个改动。注意用了Select投影之后返回的是 DTO 不是实体后续如果要调实体的方法或者做跟踪更新得换回来。我一般约定查询类接口全部返回 DTO只有编辑页才加载完整实体。5. 大数据量拉取的分页与切片当批量查询的数据量从“几千”涨到“几十万”前面那套就不够用了得考虑分页或者切片拉取。这块的坑比批量IN还要隐蔽因为很多写法在小数据量下完全正常。5.1 offset 分页的深水区ToPageList是 SqlSugar 里最常用的分页方法var total 0; var page db.QueryableOrder() .Where(o o.Status 1) .OrderBy(o o.Id) .ToPageList(pageIndex, pageSize, ref total);小数据量下毫无问题。但当pageIndex翻到几千页时数据库需要先扫描并丢弃前面几十万行才能拿到目标数据耗时随页码线性增长。这就是所谓的深分页问题。SQL Server 的OFFSET ... FETCH、MySQL 的LIMIT offset, size本质都是这样。如果你的场景是“导出全部数据”千万别用分页循环去拉。直接改成下面的 keyset 方式性能差距能到几十倍。5.2 keyset 分页的正确写法keyset 的核心思路是记住上一批的最后一个值下一批从它之后开始不做偏移public static void ExportAll(ISqlSugarClient db, ActionListOrder handler, int batchSize 2000) { long lastId 0; while (true) { var batch db.QueryableOrder() .Where(o o.Id lastId o.Status 1) .OrderBy(o o.Id) .Take(batchSize) .ToList(); if (batch.Count 0) break; handler(batch); lastId batch[batch.Count - 1].Id; if (batch.Count batchSize) break; } }这个写法有几个好处每批都是走主键索引的范围扫描无论翻到第几批单次耗时都是稳定的内存占用恒定一批处理完就能被回收对数据库的瞬时压力也可控。前提条件是排序字段必须唯一且有索引通常用自增主键。如果业务上必须按创建时间排序而创建时间可能重复得用(CreatedAt, Id)组合做游标条件写成WHERE (CreatedAt t) OR (CreatedAt t AND Id id)。写起来啰嗦一点但正确性有保障。5.3 一次 ToList 十万行的真实代价我用一张三十万行的订单表做过测试QueryableOrder().ToList()一次拉全表查询本身耗时约 1.2 秒对象物化加 GC 耗时约 3.5 秒内存峰值 700 多兆也就是说真正的时间花在了对象创建和垃圾回收上不是数据库。如果你的服务是常驻进程这种大对象分配还会导致 GC 代际晋升后续请求跟着变慢形成连锁反应。所以我的经验值是单次批量查询的行数控制在 5000 行以内。超过这个量级改成切片处理哪怕是同一个请求内串行处理总耗时反而更短因为 GC 压力小了。这个结论跟直觉相反但实测下来很稳。6. 联表与导航属性里的批量陷阱主表带子表的批量查询是另一个高发区。SqlSugar 的Includes用起来很舒服但舒服背后是有代价的不理解它的执行方式就容易写出性能灾难。6.1 Includes 背后到底是几条 SQLvar orders db.QueryableOrder() .Includes(o o.Items) .In(o o.Id, orderIds) .ToList();这段代码看起来是一条查询实际上 SqlSugar 会拆成两条先查主表拿到所有主键再用这些主键去查子表最后在内存里组装。这个策略叫“分离查询”好处是避免了联表产生的笛卡尔积放大——一个订单十个明细联表查出来就是十行重复的主表数据。分离查询只查一次主表、一次子表数据量是最优的。但注意如果orderIds有几千个子表那条 SQL 的IN列表也会很长同样面临参数上限问题。而且Includes的分片处理不像手写那么可控。所以大批量场景我倾向于手工查子表自己控制分片var orders BatchQueryHelper.QueryByKeysOrder, long(db, o o.Id, orderIds); var items BatchQueryHelper.QueryByKeysOrderItem, long(db, i i.OrderId, orderIds); var itemMap items.GroupBy(i i.OrderId).ToDictionary(g g.Key, g g.ToList()); foreach (var order in orders) { order.Items itemMap.TryGetValue(order.Id, out var list) ? list : new ListOrderItem(); }多写十几行但分片、去重、超时全都可控。数据量超过一千的时候我一直用这个写法。6.2 多层嵌套关联的批量处理Includes支持嵌套比如订单带明细、明细带商品。但嵌套层数一多生成的 SQL 条数会指数增长而且中间结果集不好控制。我的做法是分层批量查逐层用字典关联// 第一层订单 var orders BatchQueryHelper.QueryByKeysOrder, long(db, o o.Id, orderIds); // 第二层明细 var items BatchQueryHelper.QueryByKeysOrderItem, long(db, i i.OrderId, orderIds); // 第三层商品只查明细里出现的商品 var productIds items.Select(i i.ProductId).Distinct().ToList(); var products BatchQueryHelper.QueryByKeysProduct, long(db, p p.Id, productIds) .ToDictionary(p p.Id); // 组装 foreach (var item in items) { item.Product products.TryGetValue(item.ProductId, out var p) ? p : null; } var itemMap items.GroupBy(i i.OrderId).ToDictionary(g g.Key, g g.ToList()); foreach (var order in orders) { order.Items itemMap.TryGetValue(order.Id, out var list) ? list : new ListOrderItem(); }三层一共三条 SQL每层都是批量没有 N1也没有嵌套放大。这个模式我称之为“逐层批量 字典缝合”任何多层关联都能套用比配置导航属性省心也更容易加缓存。6.3 分组统计一次拿全还有一种批量需求是统计比如“每个订单的明细总金额”。用循环去Sum()就是典型的 N1正确做法是让数据库一次算完var stats db.QueryableOrderItem() .In(i i.OrderId, orderIds) .GroupBy(i i.OrderId) .Select(i new { OrderId i.OrderId, TotalAmount SqlFunc.AggregateSum(i.Amount), ItemCount SqlFunc.AggregateCount(i.Id) }) .ToList(); var statMap stats.ToDictionary(s s.OrderId);一条 SQL 拿到所有订单的汇总然后在内存里查字典。这里SqlFunc.AggregateSum是 SqlSugar 提供的聚合函数封装不能用 C# 的Sum()否则会在客户端计算等于把所有明细拉回内存再求和白折腾。提示分组字段上最好有索引否则大数据量下分组操作会用到临时表或者排序速度会掉得厉害。如果按OrderId分组而OrderId上有索引数据库可以直接走索引扫描速度快很多。7. 原生 SQL 与临时表兜底有些查询用 ORM 的表达能力写不出来或者写出来性能不理想这时候就得下到原生 SQL 层。SqlSugar 的Ado通道和SqlQueryable就是为这个准备的。7.1 Ado 通道的安全写法最直接的方式是自己拼IN的参数占位符配合SugarParameter传值既保证性能又不会注入public static ListOrderDto QueryByIds(ISqlSugarClient db, Listlong ids) { if (ids null || ids.Count 0) return new ListOrderDto(); var paramNames ids.Select((_, i) p i).ToArray(); var sql $SELECT Id, OrderNo, Amount, Status FROM t_order WHERE Id IN ({string.Join(,, paramNames)}); var parameters ids.Select((v, i) new SugarParameter(p i, v)).ToArray(); return db.Ado.SqlQueryOrderDto(sql, parameters); }这里唯一的拼接是参数名值全部走参数化安全上没问题。参数名是自己生成的p0、p1不存在用户输入参与拼接。这种写法比 ORM 多了些代码但遇到需要UNION、窗口函数、WITH子句的时候省事得多。要特别注意别图省事写成WHERE Id IN ({string.Join(,, ids)})这种把值拼进 SQL 的方式。虽然后台系统的入参看着可控但一旦哪天这个 ID 从别的地方流转过来就可能出问题。而且值拼接之后数据库每次都要重新解析 SQL执行计划缓存利用率低性能反而更差。7.2 临时表方案解决超大批量当 ID 数量到了一两万分片查询的往返次数也会变多。这时候可以走临时表把这一批 ID 一次性灌进临时表然后和业务表做JOIN。SqlSugar 较新版本提供了MergeTable思路把IN列表转成UNION ALL的派生表再关联绕开参数上限var list db.QueryableOrder() .Where(o ids.Contains(o.Id)) .MergeTable() .ToList();MergeTable的机制是把列表值拼成SELECT ... UNION ALL SELECT ...的形式然后和主表关联。这样参数化的只有值本身不受IN数量限制。代价是生成的 SQL 会很长几万个值可能撑到几兆解析时间也跟着涨。所以我的判断是一千以内走IN一千到一万走分片一万以上考虑临时表或者导入中间表。如果是几十万的量级那就别在应用层折腾了直接把数据写进一张中间表带批次号然后用JOIN查查完删掉。这个方案对数据库友好也便于排查问题就是需要额外的写入权限和清理逻辑。7.3 什么时候该放弃 ORM 表达式我给自己定的规则是查询涉及超过三张表、或者需要窗口函数、递归CTE、复杂聚合就别硬用Queryable拼了直接写 SQL。用 ORM 拼出来的复杂表达式往往难以调试生成的 SQL 也不一定是最优的出问题的时候排查成本很高。反过来如果用原生 SQL 能明显简化逻辑那就用。ORM 是工具不是教条为了“纯 ORM”而写一堆看不懂的表达式树得不偿失。我见过一个统计报表的查询用 ORM 拼了六十多行换成 SQL 只有十二行跑得还更快这种就该换。8. 排查手册与可复用封装最后这部分讲怎么定位问题。批量查询出了问题症状往往很模糊——“接口慢”“偶尔超时”“数据不对”得靠日志和执行计划去定位。8.1 先把 SQL 日志打开SqlSugar 提供了Aop.OnLogExecuting把实际执行的 SQL 和参数打出来是排查的第一手段db.Aop.OnLogExecuting (sql, pars) { var paramText string.Join(, , pars.Select(p ${p.ParameterName}{p.Value})); Console.WriteLine($[SQL] {sql}); Console.WriteLine($[PARAM] {paramText}); };只在开发和预发环境开生产环境要关掉或者采样否则日志量能把磁盘撑爆。我一般用配置开关控制出问题时动态打开定位完就关。拿到 SQL 之后看三件事参数个数有没有异常多、WHERE条件有没有用到索引字段、有没有意外的全表扫描。参数个数这个最直观一眼就能看出是不是没分片。8.2 常见问题速查表下面这些是我这几年实际遇到过、并且不止遇到一次的问题整理成表方便对照现象常见原因处理方式参数过多报错IN列表超过数据库上限分片查询SQL Server 控 1000 以内结果顺序和入参对不上IN查询不保证顺序用字典重建顺序接口偶发超时大批量拉全字段GC 压力大投影必要列控制单批行数内存持续增长一次性ToList几十万行改 keyset 分批处理查询慢但数据不多条件字段没索引加索引看执行计划主表带子表查出一堆重复用了联表而非分离查询改分层批量加字典拼装分页翻到后面越来越慢offset 深分页改 keyset 游标分页字典取值抛异常关联数据缺失TryGetValue替代索引器分组统计很慢客户端聚合用SqlFunc.Aggregate*下推到数据库这张表建议直接贴到团队的排查文档里新人遇到问题先自查一遍能省不少沟通成本。8.3 一套可直接抄的封装约定最后把前面的东西收拢成几条团队约定落地成本很低但收益明显第一条所有按主键批量取数的代码必须走统一的分片方法。不允许在业务代码里直接写QueryableT().In(...)因为你没法保证调用方传进来的是十个还是三千个。统一收口之后阈值改起来只改一处。第二条批量查询方法统一返回ListT或者DictionaryTKey, T不返回IQueryable。返回IQueryable看着灵活实际上延迟执行的边界很难控制调用方可能在外层再套一层Where或者ToList导致重复查询或者查询范围失控。老老实实把数据查完再返回是更稳的做法。第三条单次查询行数超过五千的强制走分批处理。这个约定能挡住绝大部分内存问题。实现上给个通用的QueryInBatches方法内部用 keyset 游标外部只传处理回调用起来跟单次查询差不多。第四条日志里记录批量查询的入参数量和实际返回行数。这两个数字放在一起看能快速发现异常。比如传了两千个 ID 只返回三个多半是数据问题或者条件写错了传了十个返回一万行那肯定是漏了条件曾经有个同事就是忘了加租户过滤一次查出全库数据还好日志里数字不对被及时发现了。写到这里这套东西基本覆盖了日常项目里九成以上的批量查询场景。真正落地的时候不用一次全上先把分片封装和字典拼装这两个最常用的做起来剩下的遇到具体问题再补。批量查询这件事核心从来不是用哪个 API而是时刻清楚“这一次数据库往返到底带回来多少数据、这些数据在内存里怎么流动”这两点想明白了代码自然不会写歪。
RELATED

相关推荐

从“无标题”到可交付:项目命名、结构化与最小可交付单元实战

从“无标题”到可交付:项目命名、结构化与最小可交付单元实战

“无标题”这三个字,大部分人扫一眼的反应是“漏填了吧”。我第一次看到这个标题的时候也是同样的想法,顺手就想跳过。但后来翻了翻自己的硬盘和笔记库,发现真正躺着的东西里,十个有八个就是这个状态:一个没有命名的代…

📅 2026/11/5 4:56:17
Python自动化测试:高效获取Web元素信息实战

Python自动化测试:高效获取Web元素信息实战

1. 项目概述:Python自动化获取元素信息刚入行做自动化测试那会儿,我最头疼的就是定位页面元素。明明看着按钮就在那里,脚本却死活找不到。后来才发现,获取元素信息是门基本功,更是个精细活。这个项目要解决的核心问题就…

📅 2026/11/5 4:56:17
AI语音交互系统OpenClaw的配置与优化实践

AI语音交互系统OpenClaw的配置与优化实践

1. 项目概述:让AI助手开口说话的技术实现第一次听到自己搭建的AI助手用自然的人声回应时,那种成就感至今难忘。OpenClaw语音交互系统(内部代号Jarvis)是一套开箱即用的智能语音解决方案,它能将任何文本型AI助手升级为具…

📅 2026/11/5 4:51:17
MORE NEWS

更多资讯

📰

2026年AI语音合成技术评测与选型指南

1. 项目背景与调研目标2026年的AI语音合成市场已经进入技术成熟期,各类TTS(Text-to-Speech)工具在音质、情感表达和多语言支持等方面都有了显著突破。这次调研源于我们内容创作团队的实际需求——需要为全球化的视频项目寻找一款能够兼顾自然…

📰

激光雷达与相机融合:C++点云投影着色与标定排查

简介:围绕点云着色与激光雷达—摄像头融合的 C 实践包,面向具备一定计算机视觉基础、希望用 PCL 与 OpenCV 完成多传感器数据关联的自动驾驶学习者。它针对点云缺少颜色、图像与三维点难以对应的问题,给出从 KITTI 数据读取、点云滤波预处理&…

📰

普元EOS8.3.1基线导出失败排查与解决方案

1. 问题现象与初步分析最近在使用普元EOS8.3.1低代码开发平台时,遇到了一个比较棘手的问题:在尝试导出项目基线时,前端界面没有任何错误提示,但导出操作始终无法成功。通过浏览器开发者工具(F12)检查网络请…

📰

维修价值评估系统:量化环保与经济效益的智能决策工具

1. 项目概述:重新定义物品的生命周期"东西坏了就扔掉"已经成为现代消费社会的常态。根据国际环保组织统计,全球每年产生的电子垃圾超过5000万吨,其中仅有不到20%得到妥善回收处理。这种线性经济模式不仅造成资源浪费,更…

📰

MATLAB药片缺陷检测:先背景校正与形态学,再边缘精修

简介:面向图像处理初学者与 MATLAB 算法实践者的药片缺陷及有无检测案例,围绕小波去噪抑制高斯噪声、自适应中值滤波应对椒盐噪声,并结合形态学重构去除复杂背景,再由边缘算子提取轮廓、扫描匹配定位目标并判定好坏与有无&#xf…

📰

C++结构体内存对齐与填充,为什么sizeof结果和成员总和不一样?

很多人觉得 sizeof 就是把所有成员大小相加,写代码的时候经常被结构体对齐狠狠打脸。CPU访问内存的时候,对齐地址访问效率更高。所以编译器默认会给结构体做内存对齐、内存填充。对齐规则核心两条:1. 成员的起始地址,必须是自身大…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬