Очень разная производительность EF с очень похожими запросами - PullRequest
5 голосов
/ 05 марта 2011

У нас есть два запроса Entity Framework, один с Include один с отдельным запросом. Вот они

        ConfigModelContainer model = new ConfigModelContainer();
        var scope = model.Scopes.Include("Settings")
            .Where(s => (s.Level == intLevel && s.Name == name))
            .First();

        ConfigModelContainer model = new ConfigModelContainer();
        var scope = model.Scopes
            .Where(s => (s.Level == intLevel && s.Name == name))
            .First();
        var settings = model.Settings.Where(s => s.Scope.Id == scope.Id).ToList();

еще один случай, имеющий такую ​​же производительность, как и первый (Query2)

        var scope1 = model.Scopes
            .Where(s => (s.Level == intLevel && s.Name == name))
            .First();
        scope1.Settings.Load();

Первый работает в течение 30 секунд, второй работает в течение секунды. Это так странно, что у меня нет идей.

Кто-нибудь знает, почему это может произойти?

Редактировать : фактические запросы TSQL выполняются очень быстро (за секунду)

Редактировать 2 : Вот запросы:

Первый:

SELECT 
[Project2].[Level] AS [Level], 
[Project2].[Id] AS [Id], 
[Project2].[Name] AS [Name], 
[Project2].[ParentScope_Id] AS [ParentScope_Id], 
[Project2].[C1] AS [C1], 
[Project2].[Id1] AS [Id1], 
[Project2].[Type] AS [Type], 
[Project2].[Value] AS [Value], 
[Project2].[Scope_Id] AS [Scope_Id]
FROM ( SELECT 
    [Limit1].[Id] AS [Id], 
    [Limit1].[Name] AS [Name], 
    [Limit1].[Level] AS [Level], 
    [Limit1].[ParentScope_Id] AS [ParentScope_Id], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[Type] AS [Type], 
    [Extent2].[Value] AS [Value], 
    [Extent2].[Scope_Id] AS [Scope_Id], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM   (SELECT TOP (1) 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Level] AS [Level], 
        [Extent1].[ParentScope_Id] AS [ParentScope_Id]
        FROM [dbo].[Scopes] AS [Extent1]
        WHERE ([Extent1].[Level] = @p__linq__0) AND ([Extent1].[Name] = @p__linq__1) ) AS [Limit1]
    LEFT OUTER JOIN [dbo].[Settings] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Scope_Id]
)  AS [Project2]
ORDER BY [Project2].[Id] ASC, [Project2].[C1] ASC

Во-вторых:

SELECT 
[Limit1].[Level] AS [Level], 
[Limit1].[Id] AS [Id], 
[Limit1].[Name] AS [Name], 
[Limit1].[ParentScope_Id] AS [ParentScope_Id]
FROM ( SELECT TOP (1) 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Level] AS [Level], 
    [Extent1].[ParentScope_Id] AS [ParentScope_Id]
    FROM [dbo].[Scopes] AS [Extent1]
    WHERE ([Extent1].[Level] = @p__linq__0) AND ([Extent1].[Name] = @p__linq__1)
)  AS [Limit1]

SELECT 
1 AS [C1], 
[Extent1].[Id] AS [Id], 
[Extent1].[Type] AS [Type], 
[Extent1].[Value] AS [Value], 
[Extent1].[Scope_Id] AS [Scope_Id]
FROM [dbo].[Settings] AS [Extent1]
WHERE [Extent1].[Scope_Id] = @EntityKeyValue1

Третье:

SELECT 
[Limit1].[Level] AS [Level], 
[Limit1].[Id] AS [Id], 
[Limit1].[Name] AS [Name], 
[Limit1].[ParentScope_Id] AS [ParentScope_Id]
FROM ( SELECT TOP (1) 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Level] AS [Level], 
    [Extent1].[ParentScope_Id] AS [ParentScope_Id]
    FROM [dbo].[Scopes] AS [Extent1]
    WHERE ([Extent1].[Level] = @p__linq__0) AND ([Extent1].[Name] = @p__linq__1)
)  AS [Limit1]

SELECT 
1 AS [C1], 
[Extent1].[Id] AS [Id], 
[Extent1].[Type] AS [Type], 
[Extent1].[Value] AS [Value], 
[Extent1].[Scope_Id] AS [Scope_Id]
FROM [dbo].[Settings] AS [Extent1]
WHERE [Extent1].[Scope_Id] = @p__linq__0

Редактировать 3 :

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

    static void Main(string[] args)
    {
        int intLevel = 2;
        string name = "fb226050-4f92-4fca-9442-f76565b33877";
        Stopwatch sw = new Stopwatch();
        using (CMEntities model = new CMEntities())
        {
            sw.Start();
            for (int i = 0; i < 5; i++)
            {

                var scope1 = model.Scopes.Include("Settings")
                   .Where(s => (s.Level == intLevel && s.Name == name))
                   .First();

                Console.WriteLine("Query:1, Iter:{0}, Time:{1}", i, sw.ElapsedMilliseconds);
                sw.Reset();
                sw.Start();
            }
        }
        Console.WriteLine();
        using (CMEntities model = new CMEntities())
        {
            sw.Start();
            for (int i = 0; i < 5; i++)
            {

                var scope1 = model.Scopes
                   .Where(s => (s.Level == intLevel && s.Name == name))
                   .First();
                scope1.Settings.Load();
                Console.WriteLine("Query:2, Iter:{0}, Time:{1}", i, sw.ElapsedMilliseconds);
                sw.Reset();
                sw.Start();
            }
        }
        Console.WriteLine();
        using (CMEntities model = new CMEntities())
        {
            for (int i = 0; i < 5; i++)
            {
                var scope = model.Scopes
                    .Where(s => (s.Level == intLevel && s.Name == name))
                    .First();
                var settings = model.Settings.Where(s => s.Scope.Id == scope.Id).ToList();
                Console.WriteLine("Query:3, Iter:{0}, Time:{1}", i, sw.ElapsedMilliseconds);
                sw.Reset();
                sw.Start();
            }                
        }
    }
    }

Результаты:

Query:1, Iter:0, Time:2477
Query:1, Iter:1, Time:1831
Query:1, Iter:2, Time:1933
Query:1, Iter:3, Time:1774
Query:1, Iter:4, Time:1949

Query:2, Iter:0, Time:2036
Query:2, Iter:1, Time:1870
Query:2, Iter:2, Time:1921
Query:2, Iter:3, Time:1751
Query:2, Iter:4, Time:1758

Query:3, Iter:0, Time:188
Query:3, Iter:1, Time:201
Query:3, Iter:2, Time:185
Query:3, Iter:3, Time:203
Query:3, Iter:4, Time:217

Редактировать 4 : я переписал код с помощью NHibernate:

    static void Main(string[] args)
    {

        var cfg = new StoreConfiguration();
        var sessionFactory = Fluently.Configure()
          .Database(MsSqlConfiguration.MsSql2005
              .ConnectionString("Data Source=.;Initial Catalog=CM;Integrated Security=True;MultipleActiveResultSets=True")
          )
          .Mappings(m =>
                m.AutoMappings.Add(
                    AutoMap.AssemblyOf<Entities.Scope>(cfg)
                        .Conventions
                            .Add(
                                Table.Is(x => x.EntityType.Name + "s"),
                                PrimaryKey.Name.Is(x => "Id"),
                                ForeignKey.EndsWith("_id")
                            )
                    )
          )             
          .BuildSessionFactory();
        Stopwatch sw = new Stopwatch();
        for (int i = 0; i < 5; i++)
        {
            sw.Start();
            var session = sessionFactory.OpenSession();
            int intLevel = 2;
            string name = "fb226050-4f92-4fca-9442-f76565b33877";
            var scope = session.CreateCriteria<Entities.Scope>()
                .SetFetchMode("Settings", FetchMode.Eager)
                .Add(Restrictions.Eq("Name", name))
                .Add(Restrictions.Eq("Level", intLevel))                    
                .UniqueResult<Entities.Scope>();
            Console.WriteLine("Query:0, Iter:{0}, Time:{1}", i, sw.ElapsedMilliseconds);
            sw.Reset();
        }
    }

Результаты:

Query:0, Iter:0, Time:446
Query:0, Iter:1, Time:223
Query:0, Iter:2, Time:303
Query:0, Iter:3, Time:275
Query:0, Iter:4, Time:284

Таким образом, NHibernate формирует правильную коллекцию в 10 раз быстрее, чем EF. Это действительно грустно.

Вот запрос, сгенерированный NHibernate:

SELECT this_.id            AS id0_1_, 
       this_.name          AS name0_1_, 
       this_.LEVEL         AS level0_1_, 
       settings2_.scope_id AS scope4_3_, 
       settings2_.id       AS id3_, 
       settings2_.id       AS id1_0_, 
       settings2_.TYPE     AS type1_0_, 
       settings2_.VALUE    AS value1_0_, 
       settings2_.scope_id AS scope4_1_0_ 
FROM   scopes this_ 
       LEFT OUTER JOIN settings settings2_ 
         ON this_.id = settings2_.scope_id 
WHERE  this_.name = @p0 
       AND this_.LEVEL = @p1 

Ответы [ 4 ]

1 голос
/ 19 марта 2011

Когда вы говорите, что реальные запросы TSQL выполняются быстро, вы говорите о запросах с ручным кодированием?

Попробуйте использовать SQL Profiler, чтобы увидеть, что генерируется EF 3.5. Возможно, это покажет, почему производительность будет такой разной, и даст некоторое представление о том, можно ли и как улучшить производительность первого запроса.

Кроме того, вот несколько постов в блоге, в которых приводятся конкретные примеры улучшения генерации sql в EF 4. Даже если переход на EF 4 невозможен, они могут дать пищу для размышлений.

Улучшения сгенерированного SQL в .NET 4.0 Beta1

Улучшения сгенерированного SQL в .NET 4.0

Редактировать

Вот код, который я использовал, чтобы попытаться воспроизвести ваши результаты. Это использует SQL Server 2008 R2, VS 2010 (без SP1) и Entity Framework 4.0. Я должен был угадать на схеме; надеюсь, это близко.

Чтобы создать таблицы и заполнить их:

set nocount on

create table Scopes
(
    [Id]                int identity primary key,
    [Level]             int,
    [Name]              nvarchar(50),
    [ParentScope_Id]    int foreign key references Scopes(Id)
)
create table Settings
(
    [Id]            int identity primary key,
    [Type]          nvarchar(20),
    [Value]         nvarchar(50),
    [Scope_Id]      int foreign key references Scopes(Id)   
)
go

declare @scopeId int,
        @scopeCount int,
        @settingCount int,
        @value nvarchar(50)

set @scopeCount = 0

while @scopeCount < 10
begin   
    insert into Scopes([Level], [Name]) values(1, 'Scope ' + cast(@scopeCount as nvarchar))
    select @scopeId = @@IDENTITY
    set @settingCount = 0

    while @settingCount < 10000
    begin
        set @value = 'Setting ' + cast(@scopeId as nvarchar) + '.' + cast(@settingCount as nvarchar)
        insert into Settings([Type], [Value], [Scope_Id]) values ('Test', @value, @scopeId)
        set @settingCount = @settingCount + 1
    end

    set @scopeCount = @scopeCount + 1
end

Использование консольного приложения для проверки:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace so_q5205281
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new EFTestEntities())
            {
                int level = 1;
                string name = "Scope 4";

                ExecQuery1(context, level, name);
                ExecQuery1(context, level, name);
                ExecQuery1(context, level, name);

                ExecQuery2(context, level, name);
                ExecQuery2(context, level, name);
                ExecQuery2(context, level, name);
            }

            Console.ReadLine();
        }

        static void ExecQuery1(EFTestEntities context, int level, string name)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();

            var scope = context.Scopes.Include("Settings")
                .Where(s => s.Level == level && s.Name == name)
                .First();

            int settingsCount = scope.Settings.Count();

            stopwatch.Stop();

            Console.WriteLine("Query 1, scope name: {0}, settings count: {1}, seconds {2}", scope.Name, settingsCount, stopwatch.Elapsed.TotalSeconds);
        }

        static void ExecQuery2(EFTestEntities context, int level, string name)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();

            var scope = context.Scopes
                .Where(s => s.Level == level && s.Name == name)
                .First();

            var settings = context.Settings.Where(s => s.Scope.Id == scope.Id).ToList();

            int settingsCount = scope.Settings.Count();

            stopwatch.Stop();

            Console.WriteLine("Query 2, scope name: {0}, settings count: {1}, seconds {2}", scope.Name, settingsCount, stopwatch.Elapsed.TotalSeconds);
        }
    }
}

Модель EF была создана с использованием настроек по умолчанию и обновлением модели из базы данных:

enter image description here

SQL, отправленный из EF для первого запроса:

exec sp_executesql N'SELECT 
[Project2].[Id] AS [Id], 
[Project2].[Level] AS [Level], 
[Project2].[Name] AS [Name], 
[Project2].[ParentScope_Id] AS [ParentScope_Id], 
[Project2].[C1] AS [C1], 
[Project2].[Id1] AS [Id1], 
[Project2].[Type] AS [Type], 
[Project2].[Value] AS [Value], 
[Project2].[Scope_Id] AS [Scope_Id]
FROM ( SELECT 
    [Limit1].[Id] AS [Id], 
    [Limit1].[Level] AS [Level], 
    [Limit1].[Name] AS [Name], 
    [Limit1].[ParentScope_Id] AS [ParentScope_Id], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[Type] AS [Type], 
    [Extent2].[Value] AS [Value], 
    [Extent2].[Scope_Id] AS [Scope_Id], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM   (SELECT TOP (1) 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Level] AS [Level], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[ParentScope_Id] AS [ParentScope_Id]
        FROM [dbo].[Scopes] AS [Extent1]
        WHERE ([Extent1].[Level] = @p__linq__0) AND ([Extent1].[Name] = @p__linq__1) ) AS [Limit1]
    LEFT OUTER JOIN [dbo].[Settings] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Scope_Id]
)  AS [Project2]
ORDER BY [Project2].[Id] ASC, [Project2].[C1] ASC',N'@p__linq__0 int,@p__linq__1 nvarchar(4000)',@p__linq__0=1,@p__linq__1=N'Scope 4'

и для второго запроса:

exec sp_executesql N'SELECT TOP (1) 
[Extent1].[Id] AS [Id], 
[Extent1].[Level] AS [Level], 
[Extent1].[Name] AS [Name], 
[Extent1].[ParentScope_Id] AS [ParentScope_Id]
FROM [dbo].[Scopes] AS [Extent1]
WHERE ([Extent1].[Level] = @p__linq__0) AND ([Extent1].[Name] = @p__linq__1)',N'@p__linq__0 int,@p__linq__1 nvarchar(4000)',@p__linq__0=1,@p__linq__1=N'Scope 4'

exec sp_executesql N'SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Type] AS [Type], 
[Extent1].[Value] AS [Value], 
[Extent1].[Scope_Id] AS [Scope_Id]
FROM [dbo].[Settings] AS [Extent1]
WHERE [Extent1].[Scope_Id] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=5

и вывод:

Query 1, scope name: Scope 4, settings count: 10000, seconds 0.6657546
Query 1, scope name: Scope 4, settings count: 10000, seconds 0.1608498
Query 1, scope name: Scope 4, settings count: 10000, seconds 0.1097625
Query 2, scope name: Scope 4, settings count: 10000, seconds 0.0742593
Query 2, scope name: Scope 4, settings count: 10000, seconds 0.0551458
Query 2, scope name: Scope 4, settings count: 10000, seconds 0.0555465
1 голос
/ 08 марта 2011

Пара вещей, которые нужно проверить ...

  1. Как отношения между областями действия и настройками определены в модели сущностей? Используются ли правильные внешние ключи? Как насчет множественности (один ко многим и т. Д.)?
  2. Улучшится ли ваше время выполнения, если вы измените свой первый запрос на:

.

ConfigModelContainer model = new ConfigModelContainer();
var scope = model.Scopes.Include("Settings")
                 .First<Scope>(s => s.Level == intLevel && s.Name == name);

Я точно не знаю, будет ли Linq оптимизировать этот запрос несколько иначе, чем ваш, но, возможно, посмотрим, произойдет ли это.

Что касается обновления до EF 4.0, я не знаю, насколько это возможно, если ваш проект находится в .NET 3.5. Тем не менее, у меня никогда не было этой проблемы, когда наши проекты использовали .NET 3.5 / EF 1.

0 голосов
/ 22 марта 2011

Я бы рассчитал два сгенерированных EF запроса, чтобы проверить их точное время выполнения. Может быть, вы найдете какую-то разницу.

Первый запрос упорядочен (см. Запрос, сгенерированный EF), что должно заставить его работать немного медленнее, чем второй запрос (который не приказал). Если ваши настройки велики, возможно, вы ищете время выполнения этой сортировки.

Я также замечаю, что последующие запуски (кроме первого) выполняются быстрее, чем первая итерация. Это ожидается, потому что последующие запуски не должны создавать экземпляры всех объектов - все они создаются при первом запуске. Но разница невелика, поэтому узкое место в вашей производительности, очевидно, не связано с созданием объектов.

Вы также удаляете контекст сущности, прежде чем пытаться выполнить второй запрос.

Таким образом, единственное отличие (кроме сортировки первого запроса) состоит в том, что в первом запросе вы дублируете четыре поля в областях в каждой записи, объединенной с настройками. Таким образом, у вас есть n количество дублированных четырех полей Области, где n = количество настроек. Поскольку эти четыре поля кажутся небольшими, это не должно создавать 10-кратную разницу в производительности, которую вы измерили, хотя эта 10-кратная разница составляет всего 2 секунды.

Сколько настроек у вас для этой конкретной строки Scopes?

0 голосов
/ 19 марта 2011

Можете ли вы напечатать результаты следующего?

    ConfigModelContainer model = new ConfigModelContainer();
    ObjectQuery<Scope> scope = model.Scopes.Include("Settings")
        .Where(s => (s.Level == intLevel && s.Name == name))
        .First();

    Trace.WriteLine(scope.ToTraceString());

И посмотреть, что он печатает в обоих случаях.

Если вы меняете позиции запроса, второй выполняется быстрее или медленнее?Если это начало программы, то всякий раз, когда вы запускаете первый запрос, EF будет требовать времени для извлечения метаданных, проверки метаданных и установления соответствия.Я думаю, что время инициализации EF, если вы говорите, простой запрос выполняется быстрее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...