2012-10-15 13 views
13

パフォーマンスの問題で多くの検索を行い、さまざまなことを試しましたが、十分に速く動作するようには見えません。ここで私の問題は、最も単純なフォームです:CodeFirst読み込み1親が25,000人の子供にリンクされています

私はエンティティフレームワーク5を使用しています。私は、その親を選択したときに親の子インスタンスを遅延読み込みできるようにしたいので、全体をプルする必要はありませんデータベース。しかし、私は、子供の怠惰な読み込みでパフォーマンスの問題を抱えています。問題は、親と子の間のナビゲーションプロパティのアップです。私はこれが単純なケースだと信じているので、間違っていたものでなければならないと考えています。

私は問題を特定するために単一の遅延ロードをテストするプログラムを用意しました。ここで

はテストです:

私はPOCOの親クラスを作成し、子POCOクラス。親はn子供と子供は1親を持っています。 SQL Serverデータベースには1つのみの親があり、その単一の親には25,000の子があります。私はこのデータを読み込むためにさまざまな方法を試しました。同じDbContextで子と親のどちらかをロードすると、本当に時間がかかります。しかし、それらを異なるDbContextにロードすると、本当に高速にロードされます。しかし、それらのインスタンスを同じDbContextにしたい。ここで

は私のテスト・セットアップとあなたがそれを複製するために必要なすべてです:

POCOS:

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; } 
} 

DbContext:

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

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

TSQLスクリプトを作成しますデーターベースASEとデータ:

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; 

アプリケーション。

<?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から親だけ

ロード:0972秒の接続文字列を含む設定

DbSetからの子のみをロード:0,714秒onds

ロードDbSetから親:8,6026秒

ロードDbSetから子供:0,6864秒、その後、DbSetから親をロード:7,5816159 0001秒は、その後、DbSetから子どもを読み込みます秒

ロードDbSetから親:0秒、その後、親の遅延ロードされたナビゲーションプロパティから子供をロード:8,5644549秒

ロードDbSetとから子供から親が含まれます:8,6428788秒を

Loa DからDbSetと子供から親は、次のとおりです。すべてと9,1416586秒は、親と子供が同じDbContextであるときはいつでも、それは(9秒)長い時間がかかる

分析

をオフにすべてを結ぶ。私はプロキシの作成から遅延ロードまでのすべてを無効にしようとしましたが、無駄にしました。誰か助けてくれますか?

+0

+1:大きな質問と分析! – Slauma

答えて

5

私は以前にsimilar questionと回答しました。私の以前の答えはこの問題に答える理論を含んでいますが、あなたの詳細な質問で私は問題がどこにあるのかを直接指摘できます。まず、パフォーマンスプロファイラで問題の1つを実行します。トレースモードを使用する場合は、ドットトレースからもたらされる:

enter image description here

固定関係がループ内で実行されます。それは25.000の記録のためにあなたが25.000回の反復を有することを意味するが、これらの反復のそれぞれは、内部CheckIfNavigationPropertyContainsEntityEntityCollectionに呼び出します:アイテムは、ナビゲーションプロパティに追加されているように、内側ループの反復の

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を作成しました。

+0

それは素晴らしい答えです、ありがとうございます。また、EF CodePlexで作業項目を作成していただき、ありがとうございます。 – CurlyFire

5

私はパフォーマンスを改善するための解決策はありませんが、コメントセクションには次のようなスペースがありません。私はいくつかの追加のテストと観測を加えたいだけです。

まず、7回のテストすべてで測定した時間をほぼ正確に再現できました。私はテストのためにEF 4.1を使用しました。

注意すべきいくつかの興味深い:

  • テスト2(速い)から、私は(オブジェクトにデータベースサーバから返された行と列を変換する)、そのオブジェクトのマテリアが遅くはないと結論でしょう。

  • は、これはまた、変更の追跡せずに、テスト3のエンティティをロードすることによって確認されています

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

    25001個のオブジェクトもマテリアライズする必要があります(ただし、ナビゲーションプロパティの間には関係はなくなりますが、このコードは速く走ります設立!)。

  • また、(高速な)テスト2から、変更トラッキングのエンティティスナップショットを作成するのが遅くないと結論づけます。

  • テスト3と4では、エンティティがデータベースからロードされると、親と25000の子の間の関係が修正されます。EFはすべてのChildエンティティを親のChildsコレクションに追加し、各子のParentをロードされた親に設定します。どうやらこのステップでは、すでに推測として、遅い:

    私は問題は親と子の間のナビゲーションプロパティ のワイヤアップだと思います。

    特に関係のコレクション側が問題のようです:あなたはParentクラスでChildsナビゲーションプロパティをコメントアウトした場合(関係はまだ、必要な1対多の関係で)3と4をテスト高速ですが、EFはまだすべての25000 ChildエンティティのParentプロパティを設定します。

    リレーションシップフィックスアップ中にナビゲーションコレクションをいっぱいにするのがなぜかわかりません。素朴なやり方で手作業でシミュレートしてみたら...

    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); 
        } 
    } 
    

    ...それは速くなります。明らかに関係のフィックスアップは内部的にこの単純な方法では機能しません。おそらく、コレクションにテスト対象の子が既に含まれているかどうかをチェックする必要があります。これはかなり遅い(約4秒)。

とにかく、リレーションシップフィックスアップはパフォーマンスボトルネックのようです。変更追跡と添付されたエンティティ間の関係を修正する必要がある場合は、改善する方法がわかりません。

+0

私に手を差し伸べてくれてありがとう!私はあなたのコメントと追加の分析で投稿を編集します。私はあなたのように私の分析をフォーマットするでしょう、それはポイントを推測するためにどのテストが使われたかがはっきりしています。 – CurlyFire

+0

私は助けを待っている間に別のテストをしました。私はこれと同じPOCOとテストコードを使用するNHibernateデータアクセス層を作成しました(最後の2回を除く)、各テストは1秒のマークの下で実行されます。 – CurlyFire

+0

@CurlyFire:NHibernateはリレーションを自動的に修正しますか?たとえば、テスト3では、NHはロードされたすべてのエンティティの 'Child.Parent'と' Parent.Childs'を正しいオブジェクトに設定していますか? – Slauma

関連する問題