EF Core如何通过DbContext查询导航属性指定字段
EF Core 嵌套导航属性指定字段查询性能对比与方案选型
问题背景
正在探索最高效的EF Core查询写法,用于选取导航属性中的嵌套指定字段,确保API响应仅包含所需的相关信息。目前已尝试两种实现方案:
- 使用
Include()搭配ThenInclude()的贪婪加载方案 - 社区推荐的
Select()投影方案
测试发现两种方案生成的数据库查询逻辑高度相似,从数据库检索的列完全一致;唯一可观测的差异是:Include()方案会将非必要列的数据加载到实体对象的对应属性中,而Select()方案不会执行这部分多余加载。
当前项目采用分层架构模式,不适合使用匿名类型作为返回值,且实体中未赋值的null属性不会造成业务问题——最终仅会将查询到的有效字段映射到DTO中,但仍然关注查询本身的执行效率问题,想确认:
- 是否存在更优的从导航属性查询单列字段的方案?
- 如果不存在更优方案,上述两种写法中哪一种性能更好?
现有实现与生成SQL对比
1. Include贪婪加载实现
public async Task<IEnumerable<Orden>> GetDetailed() { return await _context.Ordenes .Include(o => o.Detalles) .ThenInclude(od => od.ProductosAlmacenados) .Include(o => o.Detalles) .ThenInclude(od => od.Tela) .Include(o => o.Detalles) .ThenInclude(od => od.Color) .ToListAsync(); }
生成的SQL语句
SELECT `o`.`ID`, `o`.`Estatus`, `o`.`Fecha_Registro`, `o`.`Fecha_Requerida`, `t0`.`Color_ID`, `t0`.`Orden_ID`, `t0`.`Tela_ID`, `t0`.`Cantidad`, `t0`.`ID`, `t0`.`ID0`, `t0`.`Almacen_ID`, `t0`.`OrdenDetalles_Color_ID`, `t0`.`OrdenDetalles_Orden_ID`, `t0`.`OrdenDetalles_Tela_ID`, `t0`.`Cantidad0`, `t0`.`Nombre`, `t0`.`Nombre0` FROM `orden` AS `o` LEFT JOIN ( SELECT `o0`.`Color_ID`, `o0`.`Orden_ID`, `o0`.`Tela_ID`, `o0`.`Cantidad`, `t`.`ID`, `c`.`ID` AS `ID0`, `a`.`Almacen_ID`, `a`.`OrdenDetalles_Color_ID`, `a`.`OrdenDetalles_Orden_ID`, `a`.`OrdenDetalles_Tela_ID`, `a`.`Cantidad` AS `Cantidad0`, `t`.`Nombre`, `c`.`Nombre` AS `Nombre0` FROM `ordendetalles` AS `o0` INNER JOIN `tela` AS `t` ON `o0`.`Tela_ID` = `t`.`ID` INNER JOIN `color` AS `c` ON `o0`.`Color_ID` = `c`.`ID` LEFT JOIN `almacen_productos` AS `a` ON ((`o0`.`Color_ID` = `a`.`OrdenDetalles_Color_ID`) AND (`o0`.`Orden_ID` = `a`.`OrdenDetalles_Orden_ID`)) AND (`o0`.`Tela_ID` = `a`.`OrdenDetalles_Tela_ID`) ) AS `t0` ON `o`.`ID` = `t0`.`Orden_ID` ORDER BY `o`.`ID`, `t0`.`Color_ID`, `t0`.`Orden_ID`, `t0`.`Tela_ID`, `t0`.`ID`, `t0`.`ID0`, `t0`.`Almacen_ID`, `t0`.`OrdenDetalles_Color_ID`, `t0`.`OrdenDetalles_Orden_ID`

2. Select投影实现
public async Task<IEnumerable<Orden>> GetDetailed() { var ordenes = await _context.Ordenes .Select(o => new Orden { Id = o.Id, Estatus = o.Estatus, FechaRegistro = o.FechaRegistro, FechaRequerida = o.FechaRequerida, Detalles = o.Detalles.Select(d => new OrdenDetalles { Color = new Color { Nombre = d.Color.Nombre }, Tela = new Tela { Nombre = d.Tela.Nombre, }, Cantidad = d.Cantidad, ProductosAlmacenados = d.ProductosAlmacenados }).ToList() } ).ToListAsync(); return ordenes; }
生成的SQL语句
SELECT `o`.`ID`, `o`.`Estatus`, `o`.`Fecha_Registro`, `o`.`Fecha_Requerida`, `t0`.`Nombre`, `t0`.`Nombre0`, `t0`.`Cantidad`, `t0`.`Color_ID`, `t0`.`Orden_ID`, `t0`.`Tela_ID`, `t0`.`ID`, `t0`.`ID0`, `t0`.`Almacen_ID`, `t0`.`OrdenDetalles_Color_ID`, `t0`.`OrdenDetalles_Orden_ID`, `t0`.`OrdenDetalles_Tela_ID`, `t0`.`Cantidad0` FROM `orden` AS `o` LEFT JOIN ( SELECT `c`.`Nombre`, `t`.`Nombre` AS `Nombre0`, `o0`.`Cantidad`, `o0`.`Color_ID`, `o0`.`Orden_ID`, `o0`.`Tela_ID`, `c`.`ID`, `t`.`ID` AS `ID0`, `a`.`Almacen_ID`, `a`.`OrdenDetalles_Color_ID`, `a`.`OrdenDetalles_Orden_ID`, `a`.`OrdenDetalles_Tela_ID`, `a`.`Cantidad` AS `Cantidad0` FROM `ordendetalles` AS `o0` INNER JOIN `color` AS `c` ON `o0`.`Color_ID` = `c`.`ID` INNER JOIN `tela` AS `t` ON `o0`.`Tela_ID` = `t`.`ID` LEFT JOIN `almacen_productos` AS `a` ON ((`o0`.`Color_ID` = `a`.`OrdenDetalles_Color_ID`) AND (`o0`.`Orden_ID` = `a`.`OrdenDetalles_Orden_ID`)) AND (`o0`.`Tela_ID` = `a`.`OrdenDetalles_Tela_ID`) ) AS `t0` ON `o`.`ID` = `t0`.`Orden_ID` ORDER BY `o`.`ID`, `t0`.`Color_ID`, `t0`.`Orden_ID`, `t0`.`Tela_ID`, `t0`.`ID`, `t0`.`ID0`, `t0`.`Almacen_ID`, `t0`.`OrdenDetalles_Color_ID`, `t0`.`OrdenDetalles_Orden_ID`

相关实体定义
Orden实体
public partial class Orden { public Orden() { Detalles = new HashSet<OrdenDetalles>(); } public int Id { get; set; } public DateTime FechaRegistro { get; set; } public DateTime FechaRequerida { get; set; } public Estado Estatus { get; set; } public virtual ICollection<OrdenDetalles> Detalles { get; set; } }
OrdenDetalles实体
public partial class OrdenDetalles { public OrdenDetalles() { ProductosAlmacenados = new HashSet<ProductoAlmacenado>(); } public int ColorId { get; set; } public int OrdenId { get; set; } public int TelaId { get; set; } public int Cantidad { get; set; } public virtual Color Color { get; set; } public virtual Orden Orden { get; set; } public virtual Tela Tela { get; set; } public virtual ICollection<ProductoAlmacenado> ProductosAlmacenados { get; set; } }
结论
- 不存在比显式
Select()投影更优的嵌套导航属性指定字段查询方案。观测到的SQL高度一致完全符合EF Core运行逻辑:两个方案在数据库端的执行效率几乎没有差别,返回字段、关联逻辑、排序规则完全一致,数据库层面的IO、CPU开销基本持平。 - 应用端性能上
Select()方案明确优于Include()方案:Include()会加载被关联实体的所有映射字段,哪怕后续业务根本用不上这些字段,会额外占用内存,同时增加EF Core实体跟踪、对象实例化的开销Select()只会实例化明确声明需要的字段,不会加载多余属性,内存占用更低,对象构造速度更快- 注意:当前直接投影到实体类的写法,EF Core不会跟踪这些实体的状态,如果后续要对查询出的实体做更新操作,需要手动附加到上下文;如果是只读查询场景(比如接口返回数据映射DTO),这种写法没有任何问题,还可以搭配
AsNoTracking()进一步降低跟踪开销。
- 如果要继续优化性能,可以在投影时也明确指定
ProductosAlmacenados集合下需要的字段,不要直接赋值整个集合,能进一步减少数据库返回的数据量,降低两端开销。
内容的提问来源于stack exchange,提问作者Cbas G
相关产品推荐
相关产品推荐

