Как решить проблему с наибольшим числом групп с помощью Entity Framework (Core)? - PullRequest
3 голосов
/ 30 марта 2019

Вопрос

Дано , например , следующий набор данных:

new Entity { Id = 1, Group = 1, Value = "ABC", ... },
new Entity { Id = 2, Group = 1, Value = "DEF", ... },
new Entity { Id = 3, Group = 1, Value = "FGH", ... },
new Entity { Id = 4, Group = 1, Value = "LOP", ... },
new Entity { Id = 5, Group = 2, Value = "ALO", ... },
new Entity { Id = 6, Group = 2, Value = "PEO", ... },
new Entity { Id = 7, Group = 2, Value = "AHB", ... },
new Entity { Id = 8, Group = 2, Value = "DHB", ... },
new Entity { Id = 9, Group = 2, Value = "QPA", ... },
new Entity { Id = 10, Group = 2, Value = "LAN", ... },
// ... millions more records

как я могу сделать запрос, который эффективен (избегает проблемы с запросом N + 1) и дает мне первые 3 записи для каждого Group, упорядоченного по Value?

new Entity { Id = 1, Group = 1, Value = "ABC", ... },
new Entity { Id = 2, Group = 1, Value = "DEF", ... },
new Entity { Id = 3, Group = 1, Value = "FGH", ... },
new Entity { Id = 5, Group = 2, Value = "ALO", ... },
new Entity { Id = 7, Group = 2, Value = "AHB", ... },
new Entity { Id = 8, Group = 2, Value = "DHB", ... },
// ...

Что я пробовал?

В большинстве решений LINQ или Entity Framework для переполнения стека используется GroupBy с Take, который оценивается на стороне клиента (это означает, что все записи импортируются в память, а затем группировка происходит вне базы данных).

Я пробовал с:

var list = await _dbContext.Entities
    .Select(x => new 
    { 
        OrderKey = _dbContext.Entities.Count(y =>
            x.Group == y.Group
                && y.Value < x.Value),
        Value = x,
     })
     .Where(x => x.OrderKey < 3)
     .OrderBy(x => x.OrderKey)
     .Select(x => x.Value)
     .ToListAsync(cancellationToken);

но я уверен, что это неэффективно.

Бонусный вопрос

Как извлечь эту логику в метод расширения для IQueryable<T>, который возвращает IQueryable<T>?

1 Ответ

3 голосов
/ 31 марта 2019

Интересный вопрос. Основная проблема, которую я вижу, заключается в том, что не существует стандартной конструкции SQL для выполнения такой операции - большинство баз данных предоставляют свои собственные операторы для работы с "окном" набора строк, как у SqlServer SELECT - OVER и т. д. Для этого также нет «стандартного» оператора / шаблона LINQ.

Учитывая

IQueryable<Entity> source

типичный способ выполнения такой операции в LINQ -

var query = source.GroupBy(e => e.Group)
    .SelectMany(g => g.OrderBy(e => e.Value).Take(3));

который EF6 переводит в следующий SQL

SELECT
    [Limit1].[Id] AS [Id],
    [Limit1].[Group] AS [Group],
    [Limit1].[Value] AS [Value]
    FROM   (SELECT DISTINCT
        [Extent1].[Group] AS [Group]
        FROM [dbo].[Entity] AS [Extent1] ) AS [Distinct1]
    CROSS APPLY  (SELECT TOP (3) [Project2].[Id] AS [Id], [Project2].[Group] AS [Group], [Project2].[Value] AS [Value]
        FROM ( SELECT
            [Extent2].[Id] AS [Id],
            [Extent2].[Group] AS [Group],
            [Extent2].[Value] AS [Value]
            FROM [dbo].[Entity] AS [Extent2]
            WHERE [Distinct1].[Group] = [Extent2].[Group]
        )  AS [Project2]
        ORDER BY [Project2].[Value] ASC ) AS [Limit1]

Я не могу сказать, хороший это перевод или плохой, но по крайней мере это какой-то перевод. Важно то, что в настоящее время EF Core (последняя версия 2.2.3 на момент написания) не может перевести его на SQL и будет использовать оценку клиента (как вы упомянули).

Таким образом, в настоящее время, похоже, существует только 3 переводимых способа LINQ для написания такого запроса:

(1) (твое)

var query = source.Where(e => source.Count(
    e2 => e2.Group == e.Group && e2.Value.CompareTo(e.Value) < 0) < 3);

переводится как

  SELECT [e].[Id], [e].[Group], [e].[Value]
  FROM [Entity] AS [e]
  WHERE (
      SELECT COUNT(*)
      FROM [Entity] AS [e2]
      WHERE ([e2].[Group] = [e].[Group]) AND [e2].[Value] < [e].[Value]
  ) < 3

(2) * 1 028 *

var query = source.Where(e => source.Where(e2 => e2.Group == e.Group)
    .OrderBy(e2 => e2.Value).Take(3).Contains(e));

переводится как

  SELECT [e].[Id], [e].[Group], [e].[Value]
  FROM [Entity] AS [e]
  WHERE [e].[Id] IN (
      SELECT TOP(3) [e2].[Id]
      FROM [Entity] AS [e2]
      WHERE [e2].[Group] = [e].[Group]
      ORDER BY [e2].[Value]
  )

(3) * 1 034 *

var query = source.SelectMany(e => source.Where(e2 => e2.Group == e.Group)
    .OrderBy(e2 => e2.Value).Take(3).Where(e2 => e2.Id == e.Id));

переводится как

  SELECT [t].[Id], [t].[Group], [t].[Value]
  FROM [Entity] AS [e]
  CROSS APPLY (
      SELECT TOP(3) [e2].[Id], [e2].[Group], [e2].[Value]
      FROM [Entity] AS [e2]
      WHERE [e2].[Group] = [e].[Group]
      ORDER BY [e2].[Value]
  ) AS [t]
  WHERE [t].[Id] = [e].[Id]

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

Основной недостаток # 1 оператора сравнения (как видно из примера - нельзя использовать < для string с, для Guid с это еще хуже), и также не будет работать правильно, если Value не является уникальным внутри группировки.

С другой стороны это может быть самый быстрый из трех. Но возможно, что план выполнения для № 2 и № 3 (и даже № 1) будет одинаковым.

С учетом вышесказанного я не собираюсь предоставлять обобщенный метод, поскольку для всех этих подходов требуются разные параметры, и только общим в конечном итоге является селектор группы Expression<Func<T, TGroupKey>> (например, e => e.Group). Но (особенно для № 2 и № 3) можно написать такой метод - для этого потребуются некоторые ручные Expression манипуляции, и в целом я не уверен, что это стоит затраченных усилий

...