13

我搜索了很多关于我的性能问题并尝试了各种不同的东西,但我似乎无法让它足够快地工作。这是我最简单形式的问题:

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

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

这是测试:

我创建了一个 POCO 父类和一个子 POCO 类。父母有 n 个孩子,孩子有 1 个父母。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 秒)才能将所有内容连接起来。我什至尝试关闭从代理创建到延迟加载的所有内容,但无济于事。有人可以帮帮我吗 ?

4

2 回答 2

5

这不是答案,因为我没有提高性能的解决方案,但评论部分没有足够的空间容纳以下内容。我只想添加一些额外的测试和观察。

首先,我几乎可以准确地再现所有七个测试的测量时间。我使用 EF 4.1 进行测试。

一些有趣的事情需要注意:

  • 从(快速)测试 2 我可以得出结论,对象物化(将从数据库服务器返回的行和列转换为对象)并不慢。

  • 这也可以通过在测试 3 中加载实体而不进行更改跟踪来确认:

    parents = entities.Parents.AsNoTracking().ToList();
    // ...
    children = entities.Childs.AsNoTracking().ToList();
    

    尽管还必须物化 25001 个对象,但此代码运行速度很快(但导航属性之间不会建立任何关系!)。

  • 同样从(快速)测试 2 中,我得出结论,为更改跟踪创建实体快照并不慢。

  • 在测试 3 和 4 中,当从数据库加载实体时,父级和 25000 个子级之间的关系得到修复,即 EF 将所有Child实体添加到父级的Childs集合Parent中,并将每个子级中的 设置为加载的父级。显然,这一步很慢,正如您已经猜到的:

    我认为问题在于父级和子级之间的导航属性的连接。

    尤其是关系的集合方面似乎是问题:如果您在类中注释掉Childs导航属性Parent(关系仍然是必需的一对多关系),测试 3 和 4 很快,尽管 EF 仍然设置Parent所有 25000 个Child实体的财产。

    我不知道为什么在关系修复期间填充导航集合如此缓慢。如果您以天真的方式手动模拟它,就像这样......

    entities.Configuration.ProxyCreationEnabled = false;
    
    children = entities.Childs.AsNoTracking().ToList();
    parents = entities.Parents.AsNoTracking().ToList();
    
    parents[0].Childs = new List<Child>();
    foreach (var c in children)
    {
        if (c.ParentId == parents[0].ParentId)
        {
            c.Parent = parents[0];
            parents[0].Childs.Add(c);
        }
    }
    

    ……进展很快。显然,内部关系修复不能以这种简单的方式工作。也许需要检查集合是否已经包含要测试的孩子:

    foreach (var c in children)
    {
        if (c.ParentId == parents[0].ParentId)
        {
            c.Parent = parents[0];
            if (!parents[0].Childs.Contains(c))
                parents[0].Childs.Add(c);
        }
    }
    

    这要慢得多(大约 4 秒)。

无论如何,关系修复似乎是性能瓶颈。如果您需要更改跟踪和正确连接实体之间的关系,我不知道如何改进它。

于 2012-10-15T18:24:51.850 回答
5

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

在此处输入图像描述

修复关系循环运行。这意味着对于 25.000 条记录,您有 25.000 次迭代,但每次迭代都在内部CheckIfNavigationPropertyContainsEntity调用EntityCollection

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 次迭代,性能跟踪也显示。

我为此问题在 EF CodePlex 上创建了工作项。

于 2012-10-15T21:37:45.623 回答