2012-10-15 4 views
13

Szukałem wiele na temat mojego problemu z wydajnością i próbowałem różnych rzeczy, ale po prostu nie mogę sprawić, żeby działało wystarczająco szybko. Oto mój problem z jego najprostszą formą:CodeFirst loading 1 rodzic powiązany z 25 000 dzieci jest wolny

Używam framework entity 5 i chcę móc leniwko ładować instancje podrzędne rodzica, gdy użytkownik wybierze tego rodzica, więc nie muszę ciągnąć całego Baza danych. Jednak miałem problemy z wydajnością z leniwym ładowaniem dzieci. Myślę, że problemem jest podłączenie właściwości nawigacyjnych między rodzicem a dziećmi. Myślę też, że musi to być coś, co zrobiłem źle, ponieważ uważam, że jest to prosty przypadek.

Więc przygotowałem program do przetestowania pojedynczego leniwego obciążenia, aby wyizolować problem.

Oto Test:

Stworzyłem klasę Parent POCO i dziecko POCO klasy. Rodzic ma n Dzieci i Dziecko ma 1 Rodzic. W bazie danych programu SQL Server jest tylko 1 element nadrzędny i 25 000 elementów podrzędnych dla tego elementu nadrzędnego. Próbowałem różnych metod ładowania tych danych. Ilekroć ładuję dziecko i rodzica w tym samym DbContext, zajmuje to naprawdę dużo czasu. Ale jeśli załaduję je w różnych kontekstach DbContext, ładuje się naprawdę szybko. Jednak chcę, aby te wystąpienia były w tym samym kontekście DbContext.

Oto moja konfiguracja testy i wszystko, czego potrzeba, by go powielać:

Poços:

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 skryptów do tworzenia Datab ASE i dane:

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 zawierający ciąg połączenia:

<?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> 

test klasy konsola:

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

     } 

    } 
} 

Oto wyniki tych testów:

ładować tylko rodzic z DbSet: 0,972 sekund

Załaduj tylko dzieci z DbSet: 0,714 s onds

obciążenia rodzic z DbSet: 0,001 sekund, a następnie załadować dzieci z DbSet: 8,6026 sekundy

obciążenia dzieci z DbSet: 0,6864 sekundy, a następnie załadować macierzystych z DbSet: 7,5816159 sekund

obciążenia rodzic z DbSet: 0 sekund, a następnie załadować dzieci z leniwego obciążonej nieruchomości nawigacji rodziców: 8,5644549 sekundy

obciążenia rodzic z DbSet i dzieci z innymi: 8,6428788 sekundy

Loa d rodzic z DbSet i dzieci z innymi: 9,1416586 sekundy z wszystkiego wyłączony

Analiza

Kiedy rodzic i dzieci są w tym samym DbContext, to zajmuje dużo czasu (9 sekund) wszystko połączyć. Próbowałem nawet wyłączyć wszystko, od tworzenia proxy do leniwego ładowania, ale bez skutku. Czy ktoś może mi pomóc?

+0

+1: świetne pytanie i analiza! – Slauma

Odpowiedz

5

Poprzednio odpowiadałem similar question. Moja poprzednia odpowiedź zawiera teorię odpowiadającą na ten problem, ale z twoim szczegółowym pytaniem mogę bezpośrednio wskazać, gdzie jest problem. Najpierw uruchom jeden z problematycznych przypadków za pomocą profilera wydajności. Jest to wynikiem dotTrace podczas korzystania z trybu śledzenia:

enter image description here

Fixing stosunki działa w pętli. Oznacza to, że do 25.000 zapisów masz 25.000 iteracji, ale każdy z tych iteracji nazywa wewnętrznie CheckIfNavigationPropertyContainsEntity na 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; 
} 

Liczba iteracji pętli wewnętrznej rośnie jako elementy są dodawane do właściwości nawigacji. Matematyka jest w mojej poprzedniej odpowiedzi - jest to seria arytmetyczna, w której całkowita liczba iteracji pętli wewnętrznej wynosi 1/2 * (n^2 - n) => n^2 złożoność. Wewnętrzna pętla wewnątrz zewnętrznej pętli powoduje 312.487.500 iteracji w twoim przypadku, jak również pokazuje śledzenie wydajności.

Dla tego wydania utworzyłem work item on EF CodePlex.

+0

To jest świetna odpowiedź, wielkie dzięki. Jestem również wdzięczny, że utworzyłeś element pracy na EF CodePlex. – CurlyFire

5

Nie jest to odpowiedź, ponieważ nie mam rozwiązania poprawiającego wydajność, ale sekcja komentarzy nie ma wystarczającej ilości miejsca na poniższe. Chcę tylko dodać kilka dodatkowych testów i obserwacji.

Po pierwsze, mogę dokładnie odtworzyć zmierzone czasy dla wszystkich siedmiu testów. Użyłem EF 4.1 do testu.

Kilka ciekawych rzeczy do uwaga:

  • Od (szybkim) próba 2 Chciałbym stwierdzić, że obiekt materializację (konwersja wiersze i kolumny zwracane z serwera bazy danych na obiekty) nie jest wolny.

  • Zostało to również potwierdzone przez załadowanie jednostek w teście 3 bez zmian Śledzenie:

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

    Ten kod działa szybko chociaż 25001 obiekty mają być zmaterializowane, jak również (ale nie ma zależności pomiędzy właściwościami nawigacji będzie ustanowiony!).

  • Również z testu (szybkiego) 2 Stwierdziłbym, że tworzenie snapshotów jednostek do śledzenia zmian nie jest wolne.

  • W testach 3 i 4 relacje między rodzicem a 25000 dzieci zostają naprawione, gdy elementy są ładowane z bazy danych, tj.EF dodaje wszystkie jednostki Child do kolekcji rodzica Childs i ustawia Parent w każdym potomku na obciążonego rodzica. Widocznie ten krok jest powolny, jak już zgadliście:

    Myślę, że problem jest z drutu się właściwości nawigacji między rodzicem i dziećmi.

    Zwłaszcza strona zbiór relacji wydaje się być problem: Jeśli wykomentuj właściwość Childs nawigacyjnego w klasie Parent (relacja jest nadal wymagana relacja jeden-do-wielu wtedy) testuje 3 i 4 są szybkie, chociaż EF nadal ustawia właściwość Parent dla wszystkich encji 25000 Child.

    Nie wiem, dlaczego zapełnianie kolekcji nawigacji podczas naprawiania relacji jest tak powolne. Jeśli zasymulujesz to ręcznie w naiwny sposób, tak jak ...

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

    ... idzie szybko. Oczywiście wewnętrzna relacja nie działa w ten prosty sposób. Może to musi być sprawdzone, jeśli kolekcja zawiera już testowane dziecko:

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

    Jest to znacznie wolniej (około 4 sekund).

W każdym razie poprawa relacji wydaje się być wąskim gardłem wydajności. Nie wiem, jak to poprawić, jeśli potrzebujesz śledzenia zmian i poprawnych relacji między Twoimi powiązanymi jednostkami.

+0

Dzięki za udzielenie mi pomocy! Będę edytować post z twoimi komentarzami i dodatkową analizą. Będę również formatował moją analizę tak, jak zrobiłeś, jest o wiele jaśniejszy, który test został użyty do wyprowadzenia punktu. – CurlyFire

+0

Poddałem także kolejny test, czekając na pomoc. Stworzyłem warstwę dostępu do danych NHibernate, która używa tych samych POCO i kodu testowego, jak ten (z wyjątkiem ostatnich 2), a każdy test przebiega pod znacznikiem 1 sekundy. – CurlyFire

+0

@CurlyFire: Czy NHibernate rozwiązuje relacje automatycznie? Na przykład, w teście 3, czy NH ustawi "Child.Parent" i "Parent.Childs" dla wszystkich załadowanych obiektów na właściwe obiekty? – Slauma

Powiązane problemy