Подтвердить что ты не робот

CodeFirst loading 1 родитель, связанный с 25 000 детей, медленный

Я много раз искал проблемы с производительностью и пробовал всевозможные вещи, но я просто не могу заставить его работать достаточно быстро. Вот моя проблема в этом простейшем виде:

Я использую сущность framework 5, и я хочу иметь возможность ленить загружать дочерние экземпляры родителя, когда пользователь выбирает этот родитель, поэтому мне не нужно вытаскивать всю базу данных. Однако у меня были проблемы с производительностью при ленивой загрузке детей. Я думаю, что проблема заключается в подключении свойств навигации между родителями и детьми. Я также думаю, что это должно быть то, что я сделал неправильно, потому что считаю, что это простой случай.

Итак, я поставил программу для тестирования одной ленивой нагрузки, чтобы изолировать проблему.

Здесь Тест:

Я создал класс POCO Parent и класс POCO для детей. У родителя есть дети. У ребенка и ребенка есть 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; }
}

DbContext:

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

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

TSQL Script для создания базы данных и данных:

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 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 564 4549 секунд

Загрузите родителя из DbSet и детей из include: 8,6428788 секунд

Загрузите родителя из DbSet и детей из include: 9,1416586 секунд со всем отключенным

Анализ

Всякий раз, когда родитель и дети находятся в одном и том же DbContext, требуется много времени (9 секунд), чтобы подключить все. Я даже пытался отключить все от создания прокси до ленивой загрузки, но безрезультатно. Может кто-нибудь, пожалуйста, помогите мне?

4b9b3361

Ответ 1

Я ответил на аналогичный вопрос ранее. Мой предыдущий ответ содержит теорию, отвечающую на эту проблему, но с вашим подробным вопросом я могу прямо указать, где проблема. Сначала можно запустить один из проблемных случаев с профилировщиком производительности. Это результат использования DotTrace при использовании режима трассировки:

enter image description here

Фиксирующие отношения выполняются в цикле. Это означает, что для 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 для этой проблемы.

Ответ 2

Это не ответ, поскольку у меня нет решения для повышения производительности, но в разделе комментариев недостаточно места для следующего. Я просто хочу добавить несколько дополнительных тестов и наблюдений.

Во-первых, я смог воспроизвести ваши измеренные времена почти точно для всех семи тестов. Я использовал 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 секунд).

Во всяком случае, исправление отношений похоже на производительность botteneck. Я не знаю, как улучшить его, если вам нужно отслеживание изменений и правильные отношения между вашими прикрепленными объектами.