Оптимизировать запрос структуры сущности

Я пытаюсь сделать клонирование stackoverflow в свое время, чтобы изучить EF6 и MVC5, в настоящее время я использую OWin для аутентификации.

Все работает нормально, когда у меня есть 50-60 вопросов, я использовал генератор данных Red Gate и попытался развернуть его до 1 миллиона вопросов с помощью несколько тысяч строк дочерних таблиц без отношения просто "стресс" ORM. Вот как выглядит linq

var query = ctx.Questions
               .AsNoTracking()     //read-only performance boost.. http://visualstudiomagazine.com/articles/2010/06/24/five-tips-linq-to-sql.aspx
               .Include("Attachments")                                
               .Include("Location")
               .Include("CreatedBy") //IdentityUser
               .Include("Tags")
               .Include("Upvotes")
               .Include("Upvotes.CreatedBy")
               .Include("Downvotes")
               .Include("Downvotes.CreatedBy")
               .AsQueryable();

if (string.IsNullOrEmpty(sort)) //default
{
    query = query.OrderByDescending(x => x.CreatedDate);
}
else
{
    sort = sort.ToLower();
    if (sort == "latest")
    {
        query = query.OrderByDescending(x => x.CreatedDate);
    }
    else if (sort == "popular")
    {
        //most viewed
        query = query.OrderByDescending(x => x.ViewCount);
    }
}

var complaints = query.Skip(skipCount)
                      .Take(pageSize)
                      .ToList(); //makes an evaluation..

Излишне говорить, что я получаю таймауты SQL и после установки Miniprofiler, и посмотрите на выраженный sql-оператор, это чудовищные немногие сто строк.

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

Если бы этот случай, EF сам был бы только подходящим для небольших проектов?

Ответы

Ответ 1

Я не вижу ничего явно неправильного в вашем запросе LINQ (.AsQueryable() не должен быть обязательным, но он ничего не изменит, если вы его удалите). Конечно, не включают ненужные свойства навигации (каждый добавляет SQL JOIN), но если все требуется, оно должно быть ОК.

Теперь, когда код С# выглядит нормально, пришло время увидеть сгенерированный код SQL. Как вы уже сделали, первым шагом является извлечение SQL-запроса, который выполняется. Есть .Net способы сделать это, для SQL Server я лично всегда начинаю профилирование SQL Server сессия.

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

Теперь вопрос в том, должны ли вы добавить все эти индексы, которые ваш SQL Server говорит вам, что они отсутствуют? Не обязательно. См. Например Не просто слепо создавать те недостающие индексы. Вам нужно будет выбрать, какие индексы должны быть добавлены, что не должно.

В качестве кодового подхода для вас создаются индексы, я предполагаю, что это индексы только для первичного и внешнего ключей. Это хорошее начало, но этого недостаточно. Я не знаю количество строк в ваших таблицах, но явный индекс, который можно добавить только (инструмент создания кода не может это сделать, поскольку он связан с вашими бизнес-запросами), для например, индекс в столбце CreatedDate, поскольку вы заказываете свои товары по этому значению. Если вы этого не сделаете, SQL Server должен будет выполнить проверку таблицы на 1M строк, что, конечно же, будет катастрофическим с точки зрения производительности.

Итак:

  • попробуйте удалить некоторые Include, если вы можете
  • посмотрите фактический план выполнения, чтобы узнать, где проблема с производительностью в вашем запросе.
  • добавить только отсутствующие индексы, которые имеют смысл, в зависимости от того, как вы заказываете/фильтруете данные, которые вы получаете из БД

Ответ 2

Скорее всего, проблема, с которой вы столкнулись, - это декартово произведение.

Основываясь только на некоторых примерах данных:

var query = ctx.Questions // 50 
  .Include("Attachments") // 20                                
  .Include("Location") // 10
  .Include("CreatedBy") // 5
  .Include("Tags") // 5
  .Include("Upvotes") // 5
  .Include("Upvotes.CreatedBy") // 5
  .Include("Downvotes") // 5
  .Include("Downvotes.CreatedBy") // 5

  // Where Blah
  // Order By Blah

Это возвращает число строк вверх

50 x 20 x 10 x 5 x 5 x 5 x 5 x 5 x 5 = 156,250,000

Серьезно... это число INSANE для возвращаемых строк.

У вас действительно есть два варианта, если у вас есть эта проблема:

Во-первых: простой способ, полагаться на Entity-Framework для автоматического подключения моделей, когда они входят в контекст. А потом используйте объекты AsNoTracking() и удалите контекст.

// Continuing with the query above:

var questions = query.Select(q => q);
var attachments = query.Select(q => q.Attachments);
var locations = query.Select(q => q.Locations);

Это сделает запрос в таблице, но вместо 156 МИЛЛИОНОВ строк вы загружаете только 110 строк. Но классная часть - все они подключены в EF Context Cache, поэтому теперь переменная questions полностью заполнена.

Второе: Создайте хранимую процедуру, которая возвращает несколько таблиц и EF материализует классы.

Ответ 3

Как вы уже знаете, метод Include генерирует чудовищный SQL.

Отказ от ответственности: Я являюсь владельцем проекта Entity Framework Plus (EF +)

Метод EF + Query IncludeOptimized позволяет оптимизировать SQL, сгенерированный точно так же, как и EF Core do.

Вместо генерации чудовищного SQL создается несколько SQL (по одному для каждого из них). Эта функция также в качестве бонуса позволяет фильтровать связанные объекты.

Документы: EF + Query IncludeOptimized

var query = ctx.Questions
               .AsNoTracking()
               .IncludeOptimized(x => x.Attachments)                                
               .IncludeOptimized(x => x.Location)
               .IncludeOptimized(x => x.CreatedBy) //IdentityUser
               .IncludeOptimized(x => x.Tags)
               .IncludeOptimized(x => x.Upvotes)
               .IncludeOptimized(x => x.Upvotes.Select(y => y.CreatedBy))
               .IncludeOptimized(x => x.Downvotes)
               .IncludeOptimized(x => x.Downvotes.Select(y => y.CreatedBy))
               .AsQueryable();

Ответ 4

Взгляните на раздел 8.2.2 этот документ от Microsoft:

8.2.2 Проблемы производительности с несколькими включенными

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

Требуется относительно долгое время для запроса с несколькими Include заявления в нем, чтобы пройти через наш внутренний компилятор плана для производства команда магазина. Большая часть этого времени проводится, пытаясь оптимизировать полученный запрос. Команда сгенерированного хранилища будет содержать Outer Join или Union для каждого Include, в зависимости от вашего сопоставления. Подобные запросы приведут к большим связям с вашими базы данных в одной полезной нагрузке, которая будет поглощать любую полосу пропускания проблемы, особенно когда в полезной нагрузке имеется много избыточности (т.е. с несколькими уровнями Include для пересечения ассоциаций в направление "один ко многим" ).

Вы можете проверить случаи, когда ваши запросы возвращаются чрезмерно больших полезных нагрузок, обратившись к базовому TSQL для запроса, используя ToTraceString и выполнение команды store в SQL Server Management Studio, чтобы увидеть размер полезной нагрузки. В таких случаях вы можете попытаться уменьшить количество операторов Include в вашем запросе, чтобы просто ввести данных, которые вам нужны. Или вы можете разбить свой запрос на меньший последовательность подзапросов, например:

Перед тем, как разбить запрос:

using (NorthwindEntities context = new NorthwindEntities()) {
var customers = from c in context.Customers.Include(c => c.Orders)
                where c.LastName.StartsWith(lastNameParameter)
                select c;

foreach (Customer customer in customers)
{
    ...
} }

После разрыва запроса:

using (NorthwindEntities context = new NorthwindEntities()) {
var orders = from o in context.Orders
             where o.Customer.LastName.StartsWith(lastNameParameter)
             select o;

orders.Load();

var customers = from c in context.Customers
                where c.LastName.StartsWith(lastNameParameter)
                select c;

foreach (Customer customer in customers)
{
    ...
} }

Это будет работать только на отслеживаемые запросы, поскольку мы используем способность контекста должна выполнять определение идентичности и объединение исправление автоматически.

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