c# - CodeFirst 加载 1 个链接到 25 000 个子级的父级很慢

标签 c# sql-server entity-framework orm ef-code-first

我对我的性能问题进行了大量搜索并尝试了各种不同的方法,但我似乎无法让它足够快地运行。这是我的最简单形式的问题:

我正在使用 Entity Framework 5,我希望能够在用户选择父项时延迟加载父项的子实例,这样我就不必拉取整个数据库。但是,我一直在延迟加载 child 时遇到性能问题。我认为问题在于父级和子级之间导航属性的连接。我还认为这一定是我做错了什么,因为我相信这是一个简单的案例。

所以我提出了一个程序来测试单个延迟加载以隔离问题。

这是测试:

我创建了一个 POCO 父类和一个子 POCO 类。 Parent 有 n 个 Children,Child 有 1 个 Parent。 SQL Server 数据库中只有 1 个父项,而该单亲项有 25 000 个子项。我尝试了不同的方法来加载这些数据。每当我在同一个 DbContext 中加载子项和父项时,都会花费很长时间。但是如果我将它们加载到不同的 DbContexts 中,它的加载速度会非常快。但是,我希望这些实例位于同一个 DbContext 中。

这是我的测试设置以及复制它所需的一切:

POCO:

public class Parent
{
    public int ParentId { get; set; }

    public string Name { get; set; }

    public virtual List<Child> Childs { get; set; }
}

public class Child
{
    public int ChildId { get; set; }

    public int ParentId { get; set; }

    public string Name { get; set; }

    public virtual Parent Parent { get; set; }
}

数据库上下文:

public class Entities : DbContext
{
    public DbSet<Parent> Parents { get; set; }

    public DbSet<Child> Childs { get; set; }
}

创建数据库和数据的 TSQL 脚本:

USE [master]
GO

IF EXISTS(SELECT name FROM sys.databases
    WHERE name = 'PerformanceParentChild')
    alter database [PerformanceParentChild] set single_user with rollback immediate
    DROP DATABASE [PerformanceParentChild]
GO

CREATE DATABASE [PerformanceParentChild]
GO
USE [PerformanceParentChild]
GO
BEGIN TRAN T1;
SET NOCOUNT ON

CREATE TABLE [dbo].[Parents]
(
    [ParentId] [int] CONSTRAINT PK_Parents PRIMARY KEY,
    [Name] [nvarchar](200) NULL
)
GO

CREATE TABLE [dbo].[Children]
(
    [ChildId] [int] CONSTRAINT PK_Children PRIMARY KEY,
    [ParentId] [int] NOT NULL,
    [Name] [nvarchar](200) NULL
)
GO

INSERT INTO Parents (ParentId, Name)
VALUES (1, 'Parent')

DECLARE @nbChildren int;
DECLARE @childId int;

SET @nbChildren = 25000;
SET @childId = 0;

WHILE @childId < @nbChildren
BEGIN
   SET @childId = @childId + 1;
   INSERT INTO [dbo].[Children] (ChildId, ParentId, Name)
   VALUES (@childId, 1, 'Child #' + convert(nvarchar(5), @childId))
END

CREATE NONCLUSTERED INDEX [IX_ParentId] ON [dbo].[Children] 
(
    [ParentId] ASC
)
GO

ALTER TABLE [dbo].[Children] ADD CONSTRAINT [FK_Children.Parents_ParentId] FOREIGN KEY([ParentId])
REFERENCES [dbo].[Parents] ([ParentId])
GO

COMMIT TRAN T1;

包含连接字符串的 App.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add
      name="Entities"
      providerName="System.Data.SqlClient"
      connectionString="Server=localhost;Database=PerformanceParentChild;Trusted_Connection=true;"/>
  </connectionStrings>
</configuration>

测试控制台类:

class Program
{
    static void Main(string[] args)
    {
        List<Parent> parents;
        List<Child> children;

        Entities entities;
        DateTime before;
        TimeSpan childrenLoadElapsed;
        TimeSpan parentLoadElapsed;

        using (entities = new Entities())
        {
            before = DateTime.Now;
            parents = entities.Parents.ToList();
            parentLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load only the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds");
        }

        using (entities = new Entities())
        {
            before = DateTime.Now;
            children = entities.Childs.ToList();
            childrenLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load only the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds");
        }

        using (entities = new Entities())
        {
            before = DateTime.Now;
            parents = entities.Parents.ToList();
            parentLoadElapsed = DateTime.Now - before;

            before = DateTime.Now;
            children = entities.Childs.ToList();
            childrenLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds" +
                                               ", then load the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds");
        }

        using (entities = new Entities())
        {
            before = DateTime.Now;
            children = entities.Childs.ToList();
            childrenLoadElapsed = DateTime.Now - before;

            before = DateTime.Now;
            parents = entities.Parents.ToList();
            parentLoadElapsed = DateTime.Now - before;


            System.Diagnostics.Debug.WriteLine("Load the children from DbSet:" + childrenLoadElapsed.TotalSeconds + " seconds" +
                                               ", then load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds");
        }

        using (entities = new Entities())
        {
            before = DateTime.Now;
            parents = entities.Parents.ToList();
            parentLoadElapsed = DateTime.Now - before;

            before = DateTime.Now;
            children = parents[0].Childs;
            childrenLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load the parent from DbSet:" + parentLoadElapsed.TotalSeconds + " seconds" +
                                               ", then load the children from Parent's lazy loaded navigation property:" + childrenLoadElapsed.TotalSeconds + " seconds");
        }

        using (entities = new Entities())
        {
            before = DateTime.Now;
            parents = entities.Parents.Include(p => p.Childs).ToList();
            parentLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load the parent from DbSet and children from include:" + parentLoadElapsed.TotalSeconds + " seconds");

        }

        using (entities = new Entities())
        {
            entities.Configuration.ProxyCreationEnabled = false;
            entities.Configuration.AutoDetectChangesEnabled = false;
            entities.Configuration.LazyLoadingEnabled = false;
            entities.Configuration.ValidateOnSaveEnabled = false;

            before = DateTime.Now;
            parents = entities.Parents.Include(p => p.Childs).ToList();
            parentLoadElapsed = DateTime.Now - before;
            System.Diagnostics.Debug.WriteLine("Load the parent from DbSet and children from include:" + parentLoadElapsed.TotalSeconds + " seconds with everything turned off");

        }

    }
}

以下是这些测试的结果:

仅从 DbSet 加载父级:0,972 秒

仅从 DbSet 加载子项:0,714 秒

从 DbSet 加载父级:0,001 秒,然后从 DbSet 加载子级:8,6026 秒

从 DbSet 加载子项:0,6864 秒,然后从 DbSet 加载父项:7,5816159 秒

从 DbSet 加载父级:0 秒,然后从父级的延迟加载导航属性加载子级:8,5644549 秒

从 DbSet 加载父项,从 include:8,6428788 秒加载子项

从 DbSet 加载父项,从 include:9,1416586 秒加载子项,一切都关闭

分析

只要父项和子项在同一个 DbContext 中,就需要很长时间(9 秒)来连接所有内容。我什至尝试关闭从代理创建到延迟加载的所有功能,但无济于事。有人可以帮我吗?

最佳答案

我回答了similar question之前。我之前的回答包含回答这个问题的理论,但有了你的详细问题,我可以直接指出问题所在。首先让我们使用性能分析器运行一个有问题的案例。这是使用跟踪模式时 DotTrace 的结果:

enter image description here

固定关系循环运行。这意味着对于 25.000 条记录,您有 25.000 次迭代,但这些迭代中的每一次都在内部调用 EntityCollection 上的 CheckIfNavigationPropertyContainsEntity:

internal override bool CheckIfNavigationPropertyContainsEntity(IEntityWrapper wrapper)
{
    if (base.TargetAccessor.HasProperty)
    {
        object navigationPropertyValue = base.WrappedOwner.GetNavigationPropertyValue(this);
        if (navigationPropertyValue != null)
        {
            if (!(navigationPropertyValue is IEnumerable))
            {
                throw new EntityException(Strings.ObjectStateEntry_UnableToEnumerateCollection(base.TargetAccessor.PropertyName, base.WrappedOwner.Entity.GetType().FullName));
            }
            foreach (object obj3 in navigationPropertyValue as IEnumerable)
            {
                if (object.Equals(obj3, wrapper.Entity))
                {
                    return true;
                }
            }
        }
    }
    return false;
}

随着项目添加到导航属性,内循环的迭代次数增加。数学在我之前的回答中——它是算术级数,其中内循环的总迭代次数是 1/2 * (n^2 - n) => n^2 复杂度。在您的案例中,外循环中的内循环导致 312.487.500 次迭代,性能跟踪也显示了这一点。

我创建了 work item on EF CodePlex对于这个问题。

关于c# - CodeFirst 加载 1 个链接到 25 000 个子级的父级很慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12898790/

相关文章:

c# - 在现有 SqlConnection 中打开 DbContext 连接

javascript - 将XML中的Web数据传递到SQL Server数据库的明智方法

entity-framework - .CreateObjectSet<T>、.Set<T> 和 .CreateQuery<T> 之间的区别?

c# - 这个无效的对象名称从何而来?

entity-framework - EF 4 中的内部连接

c# - iText 7 是否支持叙利亚语?

c# - 如何访问属性内部的属性?

c# - 如何将c#中的struct转换为vb中的结构?

sql - 选择查询返回列包含域名的所有行

sql - 过滤 View