.NET Core 6.0中使用Select创建IQueryable为何缓慢?
我们有一个数据层(DataTier),其中包含代表各类表/列的模型,这些模型关联对应的DbContext(命名为OurDbContext)。
Foo模型定义:
public class Foo { public string Field1 { get; set; } public string Field2 { get; set; } }
由于需要给Foo添加一个数据库表中不存在的字段,定义了继承自Foo的Bar类:
public class Bar : Foo { public string Field3 { get; set; } }
尝试创建支持延迟执行的IQueryable<Bar>:
IQueryable<Bar> queryable = OurDbContext.Foo.FromSqlRaw(someSQl) .AsNoTracking() .Select(x => new Bar() { Field1 = x.Field1, Field2 = x.Field2, Field3 = null // 暂设为null,后续填充 });
现象:Intellisense显示Select返回的是IQueryable,但实际创建该IQueryable时速度极慢(耗时10-15秒);而移除Select直接返回IQueryable<Foo>则瞬时完成:
IQueryable<Foo> queryable = OurDbContext.Foo.FromSqlRaw(someSQl) .AsNoTracking();
请问忽略了什么?
核心问题是EF Core在处理Select中构造**继承自实体类的非实体类型(Bar)**时,无法正确生成延迟执行的查询计划,提前触发了查询执行,这就是你看到耗时的根源。
具体原因
EF Core的查询提供者会尝试把Linq表达式翻译成SQL,但Bar不是DbContext中注册的实体类型(你只注册了Foo)。当你在Select里构造Bar实例时,EF Core没法把这个构造逻辑映射到SQL,于是会立即执行前面的FromSqlRaw查询,把所有Foo数据加载到内存,再在内存中逐个转换成Bar对象。而直接返回IQueryable
解决办法
有两种实用的处理方式:
改用独立DTO类而非继承实体类
不要让Bar继承自Foo,定义一个独立的DTO类包含所需字段:public class BarDto { public string Field1 { get; set; } public string Field2 { get; set; } public string Field3 { get; set; } }然后Select构造这个DTO:
IQueryable<BarDto> queryable = OurDbContext.Foo.FromSqlRaw(someSQl) .AsNoTracking() .Select(x => new BarDto() { Field1 = x.Field1, Field2 = x.Field2, Field3 = null });这种情况下EF Core能正确识别这是投影操作,保持延迟执行,不会提前加载数据。
先转成IEnumerable再构造Bar(适合小数据量场景)
如果一定要用继承自Foo的Bar,可以先把IQueryable转成IEnumerable(此时会触发数据库查询),再在内存中转换。但注意这会把所有数据加载到内存,只适合数据量不大的情况: IEnumerable<Bar> queryable = OurDbContext.Foo.FromSqlRaw(someSQl) .AsNoTracking() .AsEnumerable() // 这里触发数据库查询 .Select(x => new Bar() { Field1 = x.Field1, Field2 = x.Field2, Field3 = null });这种方式本质上是明确了查询执行时机,耗时和你之前看到的一致。
额外提示
别让非实体类继承EF Core的实体类,实体类是和数据库表绑定设计的,继承会让EF Core误以为是实体继承映射(比如TPH/TPT),从而产生不必要的查询解析开销。
内容的提问来源于stack exchange,提问作者JustLooking

