资讯动态

SqlSugar基础查询全解析:从DbContext到联表查询实战

发布时间:2026/8/17 19:59:26 来源:尧图企业网站定制
1. 项目概述为什么从SqlSugar的基础查询开始如果你正在用.NET做后端开发尤其是涉及到数据库操作那你大概率听说过或者正在用SqlSugar。它作为一个轻量级的ORM框架这几年在社区里的热度一直不低原因很简单它足够简单也足够强大能让你用更少的代码、更直观的方式去操作数据库。但很多朋友包括我早期也一样一上来就想搞“高级功能”比如分库分表、读写分离结果连最基础的查询都写得磕磕绊绊各种报错效率没提上去反而踩了一堆坑。所以今天我们不谈那些花里胡哨的就扎扎实实地把SqlSugar的基础查询给捋清楚。这就像练武功马步扎不稳后面的招式都是花架子。所谓基础查询核心就是SELECT语句的那些事怎么查单条、查列表、查数量、按条件查、排序、分页。别看这些操作简单但里面门道不少比如First和Single用错了会抛异常分页参数没处理好性能直接拉胯还有那个让人头疼的ToListAsync和ToList到底该用哪个。我见过不少项目因为基础查询写得随意导致后期维护困难性能瓶颈也往往出在这些最常用的操作上。因此这篇内容我会结合我这些年踩过的坑和总结的经验把SqlSugar基础查询的每一个细节都掰开揉碎了讲目标是让你看完之后不仅能写出正确的查询更能写出高效、健壮的查询。无论你是刚接触SqlSugar的新手还是想巩固基础的老手相信都能有所收获。2. 核心概念与DbContext初始化在深入查询之前我们必须先打好地基。SqlSugar的核心是围绕SqlSugarClient或ISqlSugarClient这个对象展开的现在更推荐使用依赖注入的方式通过SqlSugarScope来管理生命周期。理解这个“入口”的配置是避免后续各种连接问题和性能问题的关键。2.1 理解SqlSugarScope与连接配置SqlSugarScope是SqlSugar 5.x之后推荐的单例模式核心对象它内部维护了连接池能自动管理连接的开启和关闭比直接使用SqlSugarClient更省心。初始化它本质上就是告诉SqlSugar你要连接哪个数据库、用什么账号、以及一些全局的行为规则。using SqlSugar; public static class SqlSugarHelper { // 使用SqlSugarScope作为单例 public static readonly SqlSugarScope Db new SqlSugarScope(new ConnectionConfig() { ConnectionString Serverlocalhost;DatabaseTestDB;User Idsa;Passwordyourpassword;, // 你的连接字符串 DbType DbType.SqlServer, // 数据库类型如SqlServer、MySql、PostgreSQL等 IsAutoCloseConnection true, // 自动关闭连接设为true最省心 InitKeyType InitKeyType.Attribute // 实体主键、标识列通过特性如[SugarColumn]来识别 }); }这里有几个参数必须搞清楚DbType这是你数据库类型的枚举。填错了SqlSugar生成的SQL语句就会不对比如把GETDATE()生成到MySQL里。常见的值有DbType.SqlServer、DbType.MySql、DbType.PostgreSQL、DbType.Oracle。IsAutoCloseConnection我强烈建议设为true。这意味着你不需要手动调用Open()和Close()SqlSugar会在需要时从连接池取连接用完后自动归还。如果你设为false就必须自己管理连接生命周期很容易忘记关闭导致连接泄露。InitKeyType这决定了SqlSugar如何识别实体类的主键和自增列。设为Attribute是最清晰的方式通过在实体属性上打[SugarColumn(IsPrimaryKey true, IsIdentity true)]这样的标签来声明。另一种方式是InitKeyType.SystemTable即从数据库系统表读取但这种方式有局限性且不如特性声明灵活直观。注意在生产环境中连接字符串绝对不要硬编码在代码里。应该从appsettings.json、环境变量或配置中心读取。另外对于.NET 6/7/8的项目更推荐在Program.cs中使用依赖注入的方式注册SqlSugarScope这样可以更好地与ASP.NET Core的生命周期集成也方便进行单元测试。2.2 实体类映射的“正确姿势”ORM是“对象关系映射”所以实体类是重中之重。一个定义清晰的实体类能让后续的查询、插入、更新操作事半功倍。[SugarTable(Student)] // 指定表名如果类名和表名一致可省略 public class Student { [SugarColumn(IsPrimaryKey true, IsIdentity true)] // 主键且自增 public int Id { get; set; } [SugarColumn(Length 50, IsNullable false)] // 字段长度50不可为空 public string Name { get; set; } public int Age { get; set; } [SugarColumn(ColumnName class_id)] // 数据库字段名与属性名不同时用ColumnName指定 public int ClassId { get; set; } [SugarColumn(IsIgnore true)] // 此属性不映射到数据库 public string DisplayInfo ${Name}({Age}); [Navigate(NavigateType.OneToOne, nameof(ClassId))] // 一对一导航属性需要联表查询 public Class Class { get; set; } }关键点解析[SugarTable]当你的类名和数据库表名不一致时比如类叫Student表叫t_stu必须用这个特性指明。一致时可省略但我建议显式写上代码更清晰。[SugarColumn]这是最常用的特性。IsPrimaryKey和IsIdentity必须准确标记。这影响到Insert后能否返回自增ID以及Update时能否正确识别WHERE条件。Length对于字符串类型指定数据库字段长度。不指定时SqlSugar会有默认值但为了和数据库设计一致最好明确指定。IsNullable指示字段是否允许为NULL。这会影响Insert和Update时SqlSugar的判空逻辑。ColumnName解决“数据库字段名是下划线风格C#属性名是驼峰风格”这类命名差异问题。用了它你就能用student.Name去操作数据库里的name字段。IsIgnore标记后这个属性会被SqlSugar完全忽略不参与任何数据库操作。常用于计算属性、临时字段或敏感信息。导航属性[Navigate]用于定义对象之间的关系一对一、一对多等。这里有个巨大的坑导航属性本身不会自动加载数据它只是定义了一个关系路径。你必须使用Includes或Mapper方法进行联表查询时数据才会填充到这个属性里。很多新手以为定义了导航属性就能直接访问student.Class.Name结果发现是null问题就出在这里。3. 基础查询操作全解析地基打牢了现在我们开始盖房子。基础查询是使用频率最高的操作SqlSugar提供了链式调用的API写起来非常流畅。我们从最简单的开始。3.1 查询单条记录First、Single与它们的“坑”当你确定查询条件最多只返回一条记录时比如用主键查就需要用单条查询方法。// 假设我们有一个Student表Id是主键 var student1 Db.QueryableStudent().Where(s s.Id 1).First(); var student2 Db.QueryableStudent().Where(s s.Id 1).Single(); var student3 Db.QueryableStudent().Where(s s.Id 1).FirstAsync().Result; // 异步版本First()vsSingle()怎么选这是最容易混淆的地方选错了程序就会抛异常。First()返回序列中的第一个元素。如果序列为空没查到则抛出InvalidOperationException异常。如果序列有多个元素它只取第一个不会报错。Single()返回序列中的唯一元素。如果序列为空或多于一个元素都会抛出InvalidOperationException异常。结论与建议当你用唯一性条件查询时如主键Id 1用Single()更安全因为它能帮你验证“是否真的只有一条”这个假设。如果有人误删了数据或条件写错Single()会立刻用异常告诉你出问题了。当你进行非唯一性条件查询或者排序后取第一条时必须用First()。例如Db.QueryableOrder().Where(o o.UserId 1001).OrderByDescending(o o.CreateTime).First()获取用户最新订单。异步方法FirstAsync()和SingleAsync()是它们的异步版本在ASP.NET Core等支持异步的上下文中应优先使用避免阻塞线程。实操心得我个人的习惯是只要是按主键查一律用Single。这相当于加了一道保险。对于其他明确可能返回多条记录的场景再用First。另外SqlSugar还提供了FirstOrDefault和SingleOrDefault它们在找不到元素时返回null而不是抛异常在业务逻辑允许“查不到”的情况下非常有用可以避免不必要的异常处理。3.2 查询列表ToList的同步与异步抉择查询多条记录是最常见的场景对应SQL的SELECT * FROM ...。// 同步查询直接返回ListStudent ListStudent listSync Db.QueryableStudent().Where(s s.Age 18).ToList(); // 异步查询返回TaskListStudent推荐在Web应用中使用 ListStudent listAsync await Db.QueryableStudent().Where(s s.Age 18).ToListAsync(); // 带排序的查询 ListStudent orderedList Db.QueryableStudent() .Where(s s.ClassId 10) .OrderBy(s s.Age) // 按年龄升序 .OrderByDescending(s s.Name) // 再按姓名降序OrderBy之后调用是追加排序条件 .ToList();核心要点QueryableT()这是查询的起点它返回一个ISugarQueryableT对象代表一个待执行的查询。Where()指定查询条件支持丰富的Lambda表达式。SqlSugar会将其转换为对应的SQLWHERE子句。它是延迟执行的只有在调用ToList、First、Count等方法时SQL才会真正发送到数据库。OrderBy/OrderByDescending排序。可以多次调用相当于SQL中的ORDER BY age ASC, name DESC。ToList()vsToListAsync()这是性能关键。ToList()是同步方法。它会阻塞当前线程直到数据库返回所有数据。在桌面应用或控制台程序中可能问题不大。ToListAsync()是异步方法。它不会阻塞当前线程在等待数据库响应时线程可以被释放去处理其他请求。在ASP.NET Core、Web API等I/O密集型的Web服务器应用中必须优先使用异步方法。这能显著提高服务器的并发处理能力I/O密集型避免线程池线程被大量阻塞导致服务吞吐量下降。延迟执行Deferred Execution的妙用因为Queryable和Where是延迟执行的所以你可以在最终执行前动态构建查询条件。ISugarQueryableStudent query Db.QueryableStudent(); if (!string.IsNullOrEmpty(keyword)) { query query.Where(s s.Name.Contains(keyword)); } if (minAge 0) { query query.Where(s s.Age minAge); } // 直到这里SQL都还没生成 var result query.OrderBy(s s.Id).ToList(); // 此时才生成SQL并执行这种方式比在代码里拼接SQL字符串要安全、优雅得多。3.3 查询数量、是否存在与聚合查询除了查数据本身我们经常需要知道有多少条数据或者做一些统计。// 1. 查询总记录数 - Count int totalCount Db.QueryableStudent().Count(); // SELECT COUNT(1) FROM Student int countWithCondition Db.QueryableStudent().Where(s s.Age 20).Count(); // 2. 判断是否存在符合条件的记录 - Any bool exists Db.QueryableStudent().Where(s s.Name 张三).Any(); // SELECT 1 FROM Student WHERE name张三 LIMIT 1 // Any比 Count() 0 更高效因为找到一条就返回。 // 3. 聚合查询 - Max, Min, Sum, Avg int maxAge Db.QueryableStudent().Max(s s.Age); double avgAge Db.QueryableStudent().Avg(s s.Age); decimal totalScore Db.QueryableScore().Where(s s.StudentId 1).Sum(s s.Points); // 4. 分组聚合 - GroupBy var groupResult Db.QueryableStudent() .GroupBy(s s.ClassId) .Select(s new { ClassId s.ClassId, Count SqlFunc.AggregateCount(s.Id), AvgAge SqlFunc.AggregateAvg(s.Age) }) .ToList(); // 生成的SQL类似于SELECT class_id, COUNT(id), AVG(age) FROM Student GROUP BY class_id性能提示用Any()替代Count() 0当你只关心“有没有”而不关心“有多少”时Any()是更好的选择。它的SQL通常是SELECT 1 FROM ... WHERE ... LIMIT 1数据库找到第一条匹配记录就返回效率更高。而Count()需要扫描所有匹配的行。聚合查询的NULL处理数据库里如果都是NULLMax、Min、Sum、Avg可能会返回NULL。C#中int、double等值类型不能为NULL所以SqlSugar通常会返回默认值如0。如果你的业务逻辑需要区分“没有数据”和“总和为0”可以考虑使用可空类型或者先Any()判断一下。4. 条件构建与分页查询实战实际项目中查询很少是简单的SELECT *总是伴随着动态的条件和分页需求。4.1 灵活构建动态查询条件前端传过来的搜索条件五花八门我们需要安全地将其组合到查询中。错误示范SQL注入风险string name Request.Query[name]; // 假设用户输入了 OR 11 var sql $SELECT * FROM Student WHERE Name {name}; // 永远不要这样写正确姿势使用Expression或ConditionalFilter// 方法1使用Expression动态拼接最常用 public ListStudent SearchStudents(string name, int? minAge, int? maxAge) { var query Db.QueryableStudent(); if (!string.IsNullOrEmpty(name)) { query query.Where(s s.Name.Contains(name)); // Contains会被翻译为 LIKE %{name}% } if (minAge.HasValue) { query query.Where(s s.Age minAge.Value); } if (maxAge.HasValue) { query query.Where(s s.Age maxAge.Value); } return query.ToList(); } // SqlSugar会将Lambda表达式转换为参数化查询有效防止SQL注入。 // 方法2使用ConditionalFilter适用于更复杂的动态逻辑 var query Db.QueryableStudent(); query.WhereIF(!string.IsNullOrEmpty(name), s s.Name.Contains(name)) .WhereIF(minAge 0, s s.Age minAge); // WhereIF方法第一个参数是bool条件为true时才应用后面的过滤条件。代码更紧凑。关于Contains、StartsWith、EndsWiths.Name.Contains(keyword)生成Name LIKE %keyword%s.Name.StartsWith(keyword)生成Name LIKE keyword%s.Name.EndsWith(keyword)生成Name LIKE %keyword注意前模糊(%keyword)和全模糊(%keyword%)查询会导致数据库索引失效在大数据表上慎用。如果必须用请考虑使用全文检索技术如Elasticsearch。4.2 分页查询的正确实现与性能考量分页是Web应用的标配。SqlSugar提供了非常便捷的分页方法但用法不对性能差十倍。// 标准分页查询 int pageIndex 1; // 当前页码通常从1开始 int pageSize 10; // 每页条数 RefAsyncint totalCount 0; // 用于接收总记录数的引用参数 ListStudent pageList await Db.QueryableStudent() .Where(s s.Age 10) .OrderBy(s s.Id) // 分页必须排序否则每次返回的顺序可能不一致。 .ToPageListAsync(pageIndex, pageSize, totalCount); Console.WriteLine($当前页数据{pageList.Count}条总记录数{totalCount.Value}条); // 通常你需要将 pageList 和 totalCount 一起返回给前端用于生成分页控件。分页查询的三大黄金法则必须排序 (OrderBy)没有ORDER BY的分页查询数据库每次返回的数据顺序可能都是随机的取决于执行计划这会导致用户看到的数据错乱、重复或丢失。这是分页最最基本的要求。理解ToPageList的参数ToPageListAsync(pageIndex, pageSize, totalCount)。pageIndex: 页码从1开始。pageSize: 每页大小。totalCount: 一个RefAsyncint类型的输出参数。方法执行后总记录数会填充到这里。这里有个坑这个totalCount是本次查询条件对应的总记录数SqlSugar会额外执行一条COUNT(*)的SQL来获取它。这意味着一次分页查询实际上可能产生了两条SQL一条COUNT(*)一条带OFFSET ... FETCH ...或LIMIT ...的分页查询。警惕深度分页的性能问题OFFSET分页即pageIndex很大时在数据量大的情况下性能很差。例如OFFSET 10000 ROWS FETCH NEXT 10 ROWS ONLY数据库需要先扫描并跳过前10000行效率低下。优化方案对于深度分页考虑使用“游标分页”或“键集分页”。即记录上一页最后一条记录的ID或排序字段值下一页查询用WHERE id lastId ORDER BY id LIMIT 10。这需要业务逻辑和前端配合。ToPageList内部发生了什么以SQL Server为例当你调用ToPageListAsync(2, 10, ref total)时SqlSugar大致会生成并执行以下SQL-- 1. 先查总数 SELECT COUNT(1) FROM Student WHERE Age 10; -- 2. 再查分页数据 SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY Id) AS RowIndex, * FROM Student WHERE Age 10 ) AS T WHERE RowIndex BETWEEN 11 AND 20; -- (pageIndex-1)*pageSize1 到 pageIndex*pageSize了解这个你就能明白为什么分页查询的成本相对较高。5. 联表查询与导航属性填充单表查询满足不了复杂业务我们经常需要关联多张表。SqlSugar提供了两种主流的联表方式Mapper和Includes。5.1 使用Mapper进行手动映射联表查询Mapper方式更接近原生SQL的思维灵活性强适合复杂的多表关联场景。// 假设有Student和Class表Student.ClassId 关联 Class.Id var list Db.QueryableStudent() .LeftJoinClass((s, c) s.ClassId c.Id) // 左连接 .Where((s, c) s.Age 18 c.Name.Contains(实验班)) .Select((s, c) new StudentViewModel // 自定义返回对象避免循环引用和性能浪费 { StudentId s.Id, StudentName s.Name, ClassName c.Name, Teacher c.TeacherName }) .ToList(); // 如果需要将关联的Class对象填充到Student的导航属性里可以使用.Mapper var listWithNav Db.QueryableStudent() .LeftJoinClass((s, c) s.ClassId c.Id) .Select((s, c) s) // 主表选择Student .Mapper(s s.Class, s s.ClassId) // 关键将关联的Class映射到Student.Class属性 .ToList(); // 此时listWithNav中每个Student对象的Class属性都被填充了。Mapper方法详解MapperT, T2(FuncT, T2 setAction, FuncT, object whereField)这是最常用的重载。setAction: 指定如何设置导航属性通常是s s.Class。whereField: 指定用于关联的字段通常是s s.ClassId。SqlSugar内部会根据这个关联条件将之前Join出来的Class数据匹配并设置到对应的Student对象上。优点控制力强可以处理非常复杂的多级嵌套关联。缺点代码稍显繁琐需要手动指定映射关系。5.2 使用Includes进行自动映射一对一/一对多Includes是Mapper的语法糖在简单的一对一、一对多关联中写起来更简洁。// 一对一映射 (Student - Class) var student Db.QueryableStudent() .Includes(s s.Class) // 自动根据Student.ClassId和Class.Id进行关联 .First(s s.Id 1); // 现在 student.Class 不为null // 一对多映射 (Class - ListStudent) // 首先在Class实体中定义导航属性 public class Class { [SugarColumn(IsPrimaryKey true)] public int Id { get; set; } public string Name { get; set; } [Navigate(NavigateType.OneToMany, nameof(Student.ClassId))] // 一对多关联字段是Student.ClassId public ListStudent Students { get; set; } } // 查询时使用Includes var classWithStudents Db.QueryableClass() .Includes(c c.Students) // 自动查询关联的所有Student .First(c c.Id 10); // 现在 classWithStudents.Students 包含了这个班级的所有学生列表Includes的工作原理与局限Includes看起来很美好但它背后可能执行了“N1查询”。以上面的Includes(c c.Students)为例先执行一条SQL查询ClassSELECT * FROM Class WHERE id 10。再执行一条SQL查询关联的StudentSELECT * FROM Student WHERE class_id IN (10)。 如果主查询返回了多个Class比如ToList()那么第二步的SQL就会变成WHERE class_id IN (10, 20, 30...)。这比在一条SQL里用JOIN要清晰但有时效率不如手动优化过的JOIN。局限Includes主要适用于一对一、一对多这种明确的导航关系。对于多对多或者关联条件非常复杂的情况还是得用Mapper。避坑指南无论是Mapper还是Includes都要警惕循环引用和数据膨胀。循环引用例如Student有一个Class属性Class又有一个Students属性ListStudent。如果你用Includes同时加载了两边序列化成JSON时会陷入死循环。解决方案是使用[JsonIgnore]特性忽略其中一个导航属性或者使用Select返回DTO数据传输对象只选择需要的字段。数据膨胀联表查询时如果主表的一条记录对应子表的多条记录用JOIN方式会导致主表数据重复。Mapper和Includes在内部做了处理会帮你组装成对象树避免了这个问题。6. 常见报错排查与性能优化锦囊即使理解了所有概念在实际编码中依然会遇到各种报错和性能问题。这里我整理了几个最常见的问题和解决方法。6.1 高频报错排查速查表报错信息/现象可能原因解决方案SqlSugar.SqlSugarException: Connection open error .1. 连接字符串错误。2. 数据库服务未启动。3. 网络不通或防火墙拦截。4. 连接池耗尽长时间运行后。1. 仔细检查连接字符串的服务器、数据库名、用户名、密码。2. 确认SQL Server/MySQL等服务已启动。3. 使用telnet或数据库客户端测试连通性。4. 检查代码中是否有连接未关闭确保IsAutoCloseConnectiontrue或在数据库层面调整连接池设置。SqlSugar.SqlSugarException: The table ‘XXX’ doesn‘t exist.1. 表名写错或数据库不存在此表。2. 实体类未用[SugarTable]指定表名且类名与表名不一致。3. 数据库连接到了错误的库。1. 在数据库管理工具中确认表名。2. 在实体类上添加[SugarTable(“实际表名”)]。3. 检查连接字符串中的Database或Initial Catalog。System.InvalidOperationException: Sequence contains no elements调用First()或Single()时查询结果为空。使用FirstOrDefault()或SingleOrDefault()替代它们会在无结果时返回null。或者在调用前先用Any()判断。System.InvalidOperationException: Sequence contains more than one element调用Single()或SingleOrDefault()时查询结果多于一条。确认你的查询条件是否足以唯一确定一条记录如使用主键。如果不是改用First()或FirstOrDefault()。导航属性为null定义了[Navigate]属性但查询时没有使用Includes或Mapper加载。查询时必须显式调用.Includes(s s.Class)或.Mapper来加载导航属性。更新/删除时提示“找不到主键”实体类的主键属性未用[SugarColumn(IsPrimaryKeytrue)]标记。检查实体类确保主键属性正确标记。自增主键通常还需标记IsIdentitytrue。6.2 性能优化核心要点只查询需要的字段Select这是最重要的优化原则。避免使用QueryableT().ToList()这种查全表所有字段的方式。// 不好 var list Db.QueryableOrder().ToList(); // SELECT * FROM Order // 好 var list Db.QueryableOrder().Select(o new { o.Id, o.OrderNo, o.CreateTime }).ToList(); // 或者使用DTO var list Db.QueryableOrder().Select(o new OrderBriefDto { Id o.Id, No o.OrderNo }).ToList();网络传输的数据量越少速度越快内存占用也越小。善用索引优化Where条件确保查询条件中的字段尤其是等值查询和范围查询的字段建立了数据库索引。避免在索引列上使用函数或计算如WHERE YEAR(CreateTime) 2023这会导致索引失效。应改为WHERE CreateTime 2023-01-01 AND CreateTime 2024-01-01。谨慎使用LIKE %keyword%全模糊查询如前所述它会让索引失效。异步查询Async在Web应用中务必使用ToListAsync(),FirstAsync(),CountAsync()等异步方法。这能释放线程池线程提高应用的并发处理能力。分页查询的优化如前所述避免深度分页。考虑使用“上一页最后一条ID”的方式进行分页。如果UI允许可以提供“仅加载更多”的模式而不是传统的页码跳转。监控生成的SQLSqlSugar提供了方便的SQL输出功能在开发阶段开启它能帮你发现潜在的性能问题。Db.Aop.OnLogExecuting (sql, pars) { Console.WriteLine(sql); // 输出SQL语句 // 也可以输出参数 Console.WriteLine(string.Join(,, pars?.Select(it it.ParameterName : it.Value))); };观察生成的SQL是否符合预期有没有多余的JOIN或者SELECT *。批量操作对于大量数据的插入、更新使用SqlSugar提供的InsertRange、UpdateRange等方法或者使用Storageable进行大数据处理这比在循环中执行单条SQL语句要高效得多。基础查询是SqlSugar的基石掌握这些内容你就能应对日常开发中80%的数据库查询场景。记住写出正确的查询只是第一步写出高效的查询才是我们追求的目标。多观察生成的SQL多思考业务场景你的代码质量会越来越高。在后续的分享中我们会继续深入SqlSugar的增删改、事务、以及更高级的查询特性。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价