我对我的性能问题进行了大量搜索并尝试了各种不同的方法,但我似乎无法让它足够快地运行。这是我的最简单形式的问题:
我正在使用 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 的结果:
固定关系循环运行。这意味着对于 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/