【问题标题】:Why is My One to Many Query So Slow with Linq to Entities?为什么我的 Linq to Entities 一对多查询这么慢?
【发布时间】:2017-05-04 21:39:06
【问题描述】:

我有两个 ViewModel:

public class ProductViewModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List<PartViewModel> Parts { get; set; }
}

public class PartViewModel
{
    public int Id { get; set; }

    public string Name { get; set; }
}

我正在像这样查询数据库,以获取产品列表以及相关部件:

var prods = _context.Products.Select(pr => new ProductViewModel
{
    Id = pr.Id,
    Name = pr.Name,
    Parts = pr.Parts.Select(prt => new PartViewModel
    {
        Id = prt.Id,
        Name = prt.Name
    }).ToList()
}).ToList();

Product 表中有大约 8800 条记录,而 Part 表中只有 1 条记录。此查询需要将近 4 分钟才能运行。当我像这样删除零件清单时:

var prods = _context.Products.Select(pr => new ProductViewModel
    {
        Id = pr.Id,
        Name = pr.Name
    }).ToList();

...大约需要 4 秒。

这是我在数据库中的表定义,通过 Code First EF 创建(我确保显示索引,因为这可能是索引问题:

CREATE TABLE [dbo].[Product](
    [Id] [int] NOT NULL,
    [Name] [nvarchar](max) NOT NULL,
 CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

ALTER TABLE [dbo].[Product] ADD  CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Part](
    [Id] [int] NOT NULL,
    [Name] [nvarchar](max) NOT NULL,
    [ProductId] [int] NULL,
 CONSTRAINT [PK_Part] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

ALTER TABLE [dbo].[Part]  WITH CHECK ADD  CONSTRAINT [FK_Part_Product_ProductId] FOREIGN KEY([ProductId])
REFERENCES [dbo].[Product] ([Id])
GO

ALTER TABLE [dbo].[Part] CHECK CONSTRAINT [FK_Part_Product_ProductId]
GO

CREATE NONCLUSTERED INDEX [IX_Part_ProductId] ON [dbo].[Part]
(
    [ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Part] ADD  CONSTRAINT [PK_Part] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

最后,这里有两个代码优先实体:

[Table("Product")]
public partial class Product
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Product()
    {
        Parts = new HashSet<Part>();
    }

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Required]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<Part> Parts { get; set; }
}

[Table("Part")]
public class Part
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Required]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }    

    public virtual Product Product { get; set; }
}

如果您需要更多代码或信息,请告诉我。你能看出我做错了什么吗?怎样才能以更快的方式恢复数据?

【问题讨论】:

    标签: c# .net entity-framework linq


    【解决方案1】:
    Parts = pr.Parts.Select(prt => new PartViewModel
    {
        Id = prt.Id,
        Name = prt.Name
    
    }).ToList();
    

    这就是问题所在,对于Products 中的每个产品,您正在具体化Parts 的项目列表,这意味着对表Parts 的8800 个查询。

    如果您将ProductViewModelParts 的类型更改为IEnumerable&lt;PartViewModel&gt;,您可以这样做:

    Parts = pr.Parts.Select(prt => new PartViewModel
    {
        Id = prt.Id,
        Name = prt.Name
    
    });
    

    这样就解决了问题。

    【讨论】:

    • 你能告诉我当我“删除 ToList”时代码会是什么样子吗?
    • 完成,只是删除 ToList()。
    • 对不起!!我完全误读了代码,我查看的是 Product,而不是 ProductViewModel。您可以在课堂上将 List 替换为 IEnumerable 吗?它应该没有害处,因为您将在其中枚举项目并且与 linq 完全兼容。
    • @crackedcornjimmy 我不确定“作为 List”是否正确...我建议将 ProductViewModel 上的 Parts 属性更改为 IEnumerable
    • 这就是答案。永远不要ToList EF 查询,除非你必须这样做
    【解决方案2】:

    分离查询。

    var prods = _context.Products.Select(pr => new ProductViewModel
        {
            Id = pr.Id,
            Name = pr.Name
        }).ToList();
    
        var parts = _context.Parts.Select(prt => new PartViewModel
        {
            Id = prt.Id,
        ProductId = prt.ProductId,
            Name = prt.Name
        }).ToList();
    
    
    prods.ForEach( pr => pr.Parts = parts.Where(prt=> prt.ProductId == pr.Id).ToList())
    

    【讨论】:

    • 这效率极低
    • 当对数据库的单个查询会做得更好时,为什么要这样做?
    • 它比数据库@Gusman 的 8000 个子查询效率高得多。我想不出在单个查询中做到这一点的方法。
    • 阅读我的回答,在那里你可以看到如何去做。
    【解决方案3】:

    您可以删除 ToList() 调用,但您会留下 IQueryable 类型。处理这个问题的最简单方法是使用 AutoMapper 之类的工具,并直接在查询中将其映射到 ProductViewModel。所以代码看起来像这样:

    using AutoMapper.QueryableExtensions;
    
    var parts = _context.Parts
                    .Include(part => part.Whatever)
                    .OrderByDescending(part => part.Whatever)
                    .AsNoTracking()
                    .ProjectTo<PartsListViewModel>()
    

    现在您选择了所有需要的部分,您可以通过调用实际执行查询:

    parts.ToList();
    

    所以一般的想法是过滤所有你想要的东西,然后使用像 ToList() 或 Count() 这样的调用来实际执行查询。

    此外,如果您添加 AsNoTracking() 调用,您可以稍微优化您的查询。这将禁用更改跟踪,因此您对模型对象所做的任何更改都不会被保存。如果您需要更改数据库中的值,请注意不要调用它,但对于只读场景,最好包含该调用,因为它可以防止意外的数据更改并运行得更快。

    更多关于自动映射器的信息,请访问:https://github.com/AutoMapper/AutoMapper/wiki/Queryable-Extensions

    【讨论】:

    • 如果您阅读查询,您将看到每个产品都有其不同的部件,当它返回单个部件实例时,这将如何工作?
    • 这将返回 Iqueryable 类型,因此是零件列表。实际上,第一个查询没有返回任何内容,因为此时它是一个等待执行的查询。只有当你在它上面调用一些方法时,比如 List(),它才会执行。
    • 当通过 ToList 执行最终查询时,内部查询也将被具体化,Linq 足够聪明,可以将其投影到简单的连接中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-09
    • 2019-06-29
    • 2012-03-25
    • 1970-01-01
    • 1970-01-01
    • 2011-03-07
    相关资源
    最近更新 更多