Проблемы производительности с ядром EF QueryFilter

В нашей системе мы столкнулись с проблемами производительности при использовании QueryFilters в ядре EF. Проблема в том, что ядро ​​EF фильтрует внутри LEFT JOIN, а не вне его.

Сгенерированный SQL выглядит примерно так:

SELECT [pom].[Id],
        [pom].[DeleteDate],
        [pom].[UpdateDate],
        [pom].[Version],
        [t].[Id],
        [t].[MandatorId],
        [t].[NetPriceAmount],
        [t].[NetPriceCurrencyIso4217Code]
FROM [externaldata].[PurchaseOfferMetadata] AS [pom]
    LEFT JOIN (
    SELECT [po].[Id],
            [po].[MandatorId],
            [po].[NetPriceAmount],
            [po].[NetPriceCurrencyIso4217Code]
    FROM [externaldata].[PurchaseOffer] AS [po]
    WHERE [po].[MandatorId] = 1
) AS [t] ON [pom].[Id] = [t].[Id]
WHERE [pom].[Id] IN 
    (CAST(3094411 AS bigint), 
    CAST(4757070 AS bigint), 
    CAST(4757112 AS bigint), 
    CAST(5571232 AS bigint))

Проблемная часть WHERE [po].[MandatorId] = 1. Если бы это было во втором операторе WHERE, запрос выполнялся бы намного быстрее.

Модель базы данных настроена следующим образом:

modelBuilder.Entity<PurchaseOffer>()
      .HasQueryFilter(po => po.MandatorId == 1)
      .ToTable(nameof(PurchaseOffer), schema: ExternalDataSchemaName);

modelBuilder.Entity<PurchaseOfferMetadata>()
      .HasOne(pom => pom.PurchaseOffer)
      .WithOne(po => po.Metadata)
      .HasForeignKey<PurchaseOffer>(po => po.Id);

В базе данных мы установили внешний ключ следующим образом:

IF OBJECT_ID('[externaldata].[FK_PurchaseOffer_PurchaseOfferMetadata]', 'F') IS NULL
BEGIN
    ALTER TABLE [externaldata].[PurchaseOffer] ADD CONSTRAINT [FK_PurchaseOffer_PurchaseOfferMetadata] FOREIGN KEY
    (
        [Id]
    )
    REFERENCES [externaldata].[PurchaseOfferMetadata] ([Id])
END;

Основной запрос EF выглядит следующим образом:

var existingPurchaseOfferMetadatasById = await db.PurchaseOfferMetadatas
   .Where(pom => purchaseOfferIds.Contains(pom.Id))
   .Include(pom => pom.PurchaseOffer)
   .ToDictionaryAsync(pom => pom.Id, cancellationToken);

В настоящее время у нас есть следующее количество записей в каждой таблице:

  • ПокупкаПредложениеМетаданные: 12 654 639
  • ПокупкаПредложение: 1'689'634

Кто-нибудь также сталкивался с этой проблемой и может найти решение?

EF выдал именно тот запрос, который вы написали в инструкции LINQ. .Include() также должен включать тех родителей, у которых нет детей, поэтому он всегда будет выполнять левое внешнее соединение (а правая таблица имеет фильтр в качестве «базового» фильтра). Звучит так, как будто вы хотите сделать вместо этого внутреннее соединение, которое, как вы заметите, также отфильтровывает родителей без дочерних элементов, и в этом случае вам будет полезно указать его, то есть с помощью функции .Join (ВНУТРЕННЕЕ соединение), или указать дополнительный фильтр на родительский набор данных.

DevilSuichiro 19.03.2019 16:05

Вы использовали фильтр запроса в таблице заказов .HasQueryFilter(po => po.MandatorId == 1), который будет применяться везде, где есть ссылка на таблицу PurchaseOrder, как на нее ссылаются. Если вы удалите этот QueryFilter и вместо этого добавите предложение where в свой запрос, это должно лучше оптимизировать запрос. Если вы используете шаблон репозитория или тому подобное для управления поиском данных, я бы подумал о размещении этих условий низкого уровня, а не построителя модели. Построитель контекстной модели является скорее гарантией, но я подозреваю, что это приведет к проблемам с производительностью ради соответствия.

Steve Py 19.03.2019 22:35

@StevePy Мы сделали именно то, что вы сказали, и это значительно улучшило производительность. Тем не менее, запросы не идеальны, и мы ищем другие идеи для повышения производительности. В EF core 3.0 могут быть внесены некоторые оптимизации: EF Core 3.0

sandrowi 26.03.2019 08:24

Первое и главное улучшение производительности, которое вы можете сделать с помощью EF, — это использовать .Select() для заполнения более простых моделей представления из вашего графа сущностей, а не извлекать сущности и связанные с ними биты. (либо с ленивой загрузкой Выберите N+1 ловушки, либо даже с жадной загрузкой) Выбор меньшего количества данных означает меньший объем памяти на сервере, меньше данных по сети и позволяет лучше настроить индексирование для обычных операций. Для слишком сложных моделей, таких как сценарии, похожие на отчеты, вы можете привязать сущности к материализованным представлениям, чтобы сохранить простоту кода и сложность базы данных.

Steve Py 26.03.2019 12:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
5
634
1

Ответы 1

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

using Dapper;

[...]

public class SalsaZima
{
  public int ID { get; set; }
  public string MySalsaColumn { get; set; }
  public string MyZimaColumn { get; set; }
}

[...]

// get data from database
using (IDbConnection dp = Dapper)
{
  string query = @"SELECT 
                     s.ID
                    ,s.MySalsaColumn
                    ,z.MyZimaColumn
                   FROM dbo.Salsa s 
                   LEFT JOIN dbo.Zima z
                     ON s.ZimaID = z.ID";

  return dp.Query<SalsaZima>(query).ToList();
}

Связано: stackoverflow.com/questions/43569749/…

Dave Skender 14.06.2019 20:20

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

Dave Skender 14.06.2019 20:29

Другие вопросы по теме