13

私は自分のパフォーマンスの問題をたくさん調べて、いろいろなことを試しましたが、それを十分に速く動作させることができないようです。最も単純な形式に対する私の問題は次のとおりです。

私はエンティティフレームワーク5を使用しており、ユーザーが親を選択したときに親の子インスタンスを遅延ロードできるようにしたいので、データベース全体をプルする必要はありません。ただし、子の遅延読み込みでパフォーマンスの問題が発生しています。問題は、親と子の間のナビゲーションプロパティの接続にあると思います。また、これは単純なケースだと思うので、私が間違ったことに違いないと思います。

そこで、問題を特定するために単一の遅延ロードをテストするプログラムを作成しました。

これがテストです:

POCO親クラスと子POCOクラスを作成しました。親にはn人の子供がいて、子供には1人の親がいます。SQL Serverデータベースには親が1つだけあり、その1つの親には25,000の子があります。このデータをロードするためにさまざまな方法を試しました。子と親のどちらかを同じDbContextにロードするときはいつでも、非常に長い時間がかかります。しかし、それらを異なるDbContextにロードすると、非常に高速にロードされます。ただし、これらのインスタンスを同じ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; }
}

DbContext:

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

私は以前に同様の質問に答えました。私の以前の回答には、この問題に答える理論が含まれていますが、あなたの詳細な質問で、問題がどこにあるかを直接指摘することができます。まず、パフォーマンスプロファイラーで問題のあるケースの1つを実行しましょう。これは、トレースモードを使用する場合の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回の反復になります。

この問題については、EFCodePlexで作業項目を作成しました。

于 2012-10-15T21:37:45.623 に答える
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、各子の を読み込まれた親に設定します。すでにお察しのとおり、このステップは明らかに遅いです。

    問題は、親と子の間のナビゲーション プロパティの接続だと思います。

    Childs特に、リレーションシップのコレクション側が問題のようです。クラスのナビゲーション プロパティをコメント アウトするParentと (リレーションシップは依然として 1 対多のリレーションシップが必要です)、テスト 3 と 4 は高速ですが、EF はまだParent25000Childエンティティすべてのプロパティ。

    リレーションシップの修正中にナビゲーション コレクションを埋めるのがなぜこんなに遅いのか、私にはわかりません。単純な方法で手動でシミュレートすると、次のようになります...

    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 に答える