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

EF 6 против относительной производительности EF 5 при развертывании в IIS8

У меня есть приложение MVC 4 с EF 6. После обновления с EF 5 до EF 6 я заметил проблему производительности с одним из моих запросов linq-объектов. Сначала я был взволнован, потому что на моем ящике разработки я заметил 50% -ное улучшение от EF 5 до EF 6. Этот запрос возвращает около 73 000 записей. SQL, выполняемый на производственном сервере, был перехвачен с помощью Activity Monitor, Recent Expensive Queries, это время также включено в следующие таблицы. Следующие числа после разогрева DB:

Разработка: 64-разрядная ОС, SS 2012, 2 ядра, 6 ГБ оперативной памяти, IIS Express.

EF 5 ~30 sec
EF 6 ~15 sec
SQL ~26 sec

Производство: 64-разрядная ОС, SS 2012, 32 ядра, 32 ГБ оперативной памяти, IIS8.

EF 5 ~8 sec
EF 6 ~4 minutes
SQL ~6 sec.

Я включил спецификации, чтобы дать представление о том, какая относительная производительность должна быть. Таким образом, кажется, что, когда я использую EF 6 в своей среде разработки, я получаю повышение производительности, когда публикую на своем производственном сервере огромную проблему с производительностью. Базы данных аналогичны, если не совсем то же самое. Все индексы были перестроены, SQL-запрос также, похоже, указывает на то, что нет причин подозревать, что база данных виновата. Пул приложений -.Net 4.0 в производстве. Оба сервера разработки и производства имеют .Net 4.5. Я не знаю, что проверить дальше или как отладить эту проблему, какие-нибудь идеи о том, что делать или как отлаживать дальше?

Update: Использование SQL Server Profiler обнаружило, что EF5 и EF6 создают несколько разные TSQL. Разница TSQL следующая:

EF5: LEFT OUTER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]
EF6: INNER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]

Этот же TSQL из EF6 также работает по-разному в зависимости от сервера/базы данных, на которой выполняется TSQL. После проверки плана запросов для EF6 и медленной базы данных (серийный сервер SS build 11.0.3000 Enterprise Edition) этот план выполняет все сканирование и не ищет по сравнению с идентичным экземпляром (тестовый сервер SS build 11.0.3128 Developers Edition), который имеет несколько запросов это делает разницу. Настенное время составляет > 4 минуты для производства и 12 секунд для небольшого тестового сервера. EF помещает эти запросы в sp_executesql proc, перехваченный sp_executesql proc использовался для указанного выше момента. Я не получаю медленное время (плохой план запроса) сгенерированным кодом EF5 или EF6 при выполнении на сервере разработки. Также странно, если я удалю TSQL из sp_executesql и запустил его на производственном сервере, запрос выполняется быстро (6 секунд). Итак, для медленного плана выполнения необходимо выполнить три вещи:

1. Execute on production server build 11.0.3000
2. Use Inner Join with Pins table (EF6 generated code).
3. Execute TSQL inside of sp_executesql.

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

select DB_ID() 
DBCC Flushprocindb(database_Id)
and 
DBCC FREEPROCCACHE(plan_handle)

Промывка сверху не повлияла на план запроса. Любые предложения, что попробовать дальше?

Ниже приведен запрос linq:

    result =
    (
    from p1 in context.CookSales

    join l2 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year1 } equals new { ID = l2.PinId, YEAR = l2.StatusYear } into list2
    from p3 in list2.DefaultIfEmpty()
    join l3 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year2 } equals new { ID = l3.PinId, YEAR = l3.StatusYear } into list3
    from p4 in list3.DefaultIfEmpty()
    join l4 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year3 } equals new { ID = l4.PinId, YEAR = l4.StatusYear } into list4
    from p5 in list4.DefaultIfEmpty()
    join l10 in context.CookSaleStatus on new { ID = p1.PinId, YEAR = year4 } equals new { ID = l10.PinId, YEAR = l10.StatusYear } into list10
    from p11 in list10.DefaultIfEmpty()

    join l5 in context.ILCookAssessors on p1.PinId equals l5.PinID into list5
    from p6 in list5.DefaultIfEmpty()
    join l7 in context.ILCookPropertyTaxes on new { ID = p1.PinId } equals new { ID = l7.PinID } into list7
    from p8 in list7.DefaultIfEmpty()

    join l13 in context.WatchLists on p1.PinId equals l13.PinId into list13
    from p14 in list13.DefaultIfEmpty()

    join l14 in context.Pins on p1.PinId equals l14.PinID into list14
    from p15 in list14.DefaultIfEmpty()
    orderby p1.Volume, p1.PIN
    where p1.SaleYear == userSettings.SaleYear 
    where ((p1.PinId == pinId) || (pinId == null))
    select new SaleView
    {
        id = p1.id,
        PinId = p1.PinId,
        Paid = p1.Paid == "P" ? "Paid" : p1.Paid,
        Volume = p1.Volume,
        PinText = p15.PinText,
        PinTextF = p15.PinTextF,
        ImageFile = p15.FnaImage.TaxBodyImageFile,
        SaleYear = p1.SaleYear,
        YearForSale = p1.YearForSale,
        Unpaid = p1.DelinquentAmount,
        Taxes = p1.TotalTaxAmount,
        TroubleTicket = p1.TroubleTicket,
        Tag1 = p1.Tag1,
        Tag2 = p1.Tag2,
        HasBuildingPermit = p1.Pin1.BuildingPermitGeos.Any(p => p.PinId == p1.PinId),
        BidRate = p1.BidRate,
        WinningBid = p1.WinningBid,
        WinningBidderNumber = p1.BidderNumber,
        WinningBidderName = p1.BidderName,
        TaxpayerName = p1.TaxpayerName,
        PropertyAddress = SqlFunctions.StringConvert((double?)p1.TaxpayerPropertyHouse) + " " + p1.TaxpayerPropertyDirection + " "
                        + p1.TaxpayerPropertyStreet
                        + " " + p1.TaxpayerPropertySuffix +
                        System.Environment.NewLine + (p1.TaxpayerPropertyCity ?? "") + ", " + (p1.TaxpayerPropertyState ?? "") +
                        " " + (p1.TaxpayerPropertyZip ?? ""),
        MailingAddress = (p1.TaxpayerName ?? "") + System.Environment.NewLine + (p1.TaxpayerMailingAddress ?? "") +
                        System.Environment.NewLine + (p1.TaxpayerMailingCity ?? "") + ", " + (p1.TaxpayerMailingState ?? "") +
                        " " + (p1.TaxpayerMailingZip ?? ""),
        Status1 = p3.Status.Equals("Clear") ? null : p3.Status,
        Status2 = p4.Status.Equals("Clear") ? null : p4.Status,
        Status3 = p5.Status.Equals("Clear") ? null : p5.Status,
        Status4 = p11.Status.Equals("Clear") ? null : p11.Status,
        Township = p6.Township,
        AssessorLastUpdate = p6.LastUpdate,
        Age = p6.Age,
        LandSquareFootage = p6.LandSquareFootage,
        BuildingSquareFootage = p6.BuildingSquareFootage,
        CurrLand = p6.CurrLand,
        CurrBldg = p6.CurrBldg,
        CurrTotal = p6.CurrTotal,
        PriorLand = p6.PriorLand,
        PriorBldg = p6.PriorBldg,
        PriorTotal = p6.PriorTotal,
        ClassDescription = p6.ClassDescription,
        Class = p1.Classification == null ? p6.Class.Trim() : p1.Classification,
        TaxCode = p6.TaxCode,
        Usage = p6.Usage,

        Status0 = (p8.CurrentTaxYear != null && p8.CurrentTaxYearPaidAmount == 0) ? "Paid" : null, 
        LastTaxYearPaidAmount = p8.LastTaxYearPaidAmount,
        NoteStatus = p15.PinNotes.Any(p => p.PinId == p15.PinID),
        EntryComment = p1.EntryComment,
        IsInScavenger = p14.IsInScavenger ?? false,
        IsInTbs = p14.IsInTbs ?? false,
        RedeemVts = (p3.Redeemed == "VTS" || p4.Redeemed == "VTS" || p5.Redeemed == "VTS" || p11.Redeemed == "VTS") ? true : false,
        FivePercenter = (p3.FivePercenter || p4.FivePercenter || p5.FivePercenter || p11.FivePercenter) ? true : false,
    }
    ).ToList();

SQL, который сгенерирован с этим запросом, кажется разумным. (Я не включил его, потому что, когда я вставляю его в него, он не отформатирован и не читается.)

4b9b3361

Ответ 1

Во время исследования этой проблемы я обнаружил несколько вещей о SQL Server, которые я не знал. Для некоторых это может быть общеизвестным, но для меня это не так. Вот мои основные моменты.

  • EF использует динамический sql для всех запросов, в частности sp_exectutesql(). sp_executesql() выполняет динамический SQL, если вы удаляете этот SQL и выполняете как adhoc-запрос в SSMS, не ожидайте получить одинаковые результаты производительности. Это документировано хорошо здесь и ссылки этот документ, который я настоятельно рекомендую прочитать, если вы имеют такие проблемы.
  • В определенных условиях EF5 создает различные динамические SQL, чем EF6.
  • Трудно оптимизировать linq для объектов, потому что вы можете получать разные результаты в зависимости от аппаратного обеспечения, это объясняется в ссылках. Моя первоначальная цель заключалась в оптимизации запроса linq при обновлении до EF6. Я заметил, что не с использованием свойств навигации улучшил производительность на моих dev и тестовых серверах, но убил его в процессе производства.
  • Конечный результат с приемлемой производительностью во всех средах был комбинацией свойств соединения и навигации. В конце концов, если бы я использовал все свойства навигации, с самого начала все было бы лучше. Используемые ключи соединения были из неправильных таблиц, когда вы пишете специальный SQL, это не имеет значения, но для динамического SQL. Если бы я использовал навигацию, не было бы никаких ключей, чтобы ошибиться. Однако лучшая производительность была с одним соединением и остальными навигационными свойствами. Сгенерированный динамический SQL-код очень похож для всех сценариев, но оптимизатор плана запросов SQL Server получает более четкие подсказки при использовании свойств навигации (это предположение).

Ключевой частью изменения linq было следующее:

                from p1 in context.CookSales
                join p15 in context.Pins on p1.PinId equals p15.PinID
                where p1.SaleYear == userSettings.SaleYear
                where ((p1.PinId == pinId) || (pinId == null))
                orderby p1.Volume, p1.PIN
                select new SaleView bla bla

Таблица Pins содержит первичный ключ для PinId, тогда как все остальные таблицы имеют PinId как внешний ключ. Сохранение контактов в качестве соединения, а не свойство навигации улучшило производительность.