EF Core — невозможно выполнить агрегатную функцию для выражения, содержащего агрегат или подзапрос

Я пытаюсь написать запрос, который дает мне среднее время между изменениями состояния объекта базы данных. При каждом обновлении в таблице аудита создается запись. Эта таблица содержит поля «Статус» и «Предыдущий статус», что позволяет мне легко запрашивать изменения. Я могу получить нужный результат с помощью следующего запроса, но проблема в том, что он выполняется в памяти, а не в базе данных, что не является лучшим решением.

//this is the best one so far but it does the average function in memory
private async Task<double> GetAvgBetweenStatus()
{
    return (await _context.Set<Audit_Opportunity>().Where(x => x.Status == OpportunityStatus.Solutioned && x.PreviousStatus == OpportunityStatus.Qualified).Select(x => new
    {
        Date1 = x.AuditDate,
        Date2 = _context.Set<Audit_Opportunity>().Where(o => o.OpportunityId == x.OpportunityId && o.Id < x.Id && o.PreviousStatus == OpportunityStatus.Identified).OrderByDescending(o => o.Id).First().AuditDate
    })
    .Select(x =>
        new
        {
            Duration = EF.Functions.DateDiffSecond(x.Date2, x.Date1)
        })
    .ToListAsync() // <--- here it is doing the query  ) 
    .Average(x => x.Duration); // <--- this bit is done in memory / not good!
}

Если я попытаюсь удалить метод ToListAsync() и использовать, например, AverageAsync(), я получу исключение «Невозможно выполнить агрегатную функцию для выражения, содержащего агрегат или подзапрос».

Он выдает следующий неверный оператор SQL

SELECT AVG(CAST(DATEDIFF(second, (
SELECT TOP(1) [a0].[AuditDate]
FROM [Audit_Opportunity] AS [a0]
WHERE [a0].[OpportunityId] = [a].[OpportunityId] AND [a0].[Id] < [a].[Id] AND [a0].[PreviousStatus] = 1), [a].[AuditDate]) AS float))

ИЗ [Audit_Opportunity] КАК [a] ГДЕ [a].[Status] = 3 И [a].[ПредыдущийStatus] = 2

Я думаю, мне нужно добавить SELECT AVG(....) FROM T вот так:

** Я использую секунды Dateiff только для тестирования. ***

SELECT AVG(t.Duration) FROM(
SELECT DATEDIFF(SECOND, (
    SELECT TOP(1) [a0].[AuditDate]
    FROM [Audit_Opportunity] AS [a0]
    WHERE [a0].[OpportunityId] = [a].[OpportunityId] AND [a0].[Id] < [a].[Id] AND [a0].[PreviousStatus] = 1 order by [a0].Id DESC), [a].[AuditDate]) AS [Duration]
FROM [Audit_Opportunity] AS [a]
WHERE [a].[Status] = 3 AND [a].[PreviousStatus] = 2) AS T

Независимо от того, что я пытаюсь сделать, это либо вызывает исключение, либо работает, но создает беспорядок в запросе, который, скорее всего, очень неэффективен.

Вот основные столбцы, которые являются ключевыми для этого запроса.

Добавьте тег для вашей СУБД (выше это похоже на SQL Server). Это повысит видимость тех, кто, скорее всего, поможет. Опубликуйте образец данных в виде текста для таблицы уценки (а не изображения), чтобы мы могли его вырезать и вставить. Какой у вас тип данных AuditDate? Опубликованные значения имеют странный формат. Кроме того, ваш первый оператор SQL кажется неполным. Пожалуйста, исправьте.

T N 09.08.2024 20:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в том, что EF преобразовал ваше выражение LINQ в синтаксически (и даже семантически) правильный запрос, но столкнулся с ограничением SQL Server. Хитрость заключается в том, чтобы попытаться найти эквивалентную форму, которая генерирует другой, но поддерживаемый SQL-запрос.

Если бы мне пришлось закодировать это на SQL Server, я бы использовал CROSS APPLY (SELECT TOP 1 ...) для поиска предыдущей (начальной) записи аудита. Что-то вроде:

SELECT AVG(CAST(DATEDIFF(second, prior.AuditDate, a.AuditDate) AS float))
FROM Audit_Opportunity AS a
CROSS APPLY (
    SELECT TOP 1 a0.*
    FROM Audit_Opportunity a0
    WHERE a0.OpportunityId = a.OpportunityId
    AND a0.Id < a.Id
    AND a0.PreviousStatus = 1
    ORDER BY a0.Id DESC
) prior
WHERE a.Status = 3
AND a.PreviousStatus = 2

Если я использую инструмент (который я оставлю безымянным) для преобразования приведенного выше SQL в эквивалентный LINQ, я получу следующее:

var result = dbContext.Audit_Opportunity
    .Where(a => a.Status == 3 && a.PreviousStatus == 2)
    .Select(a => new
    {
        Duration = dbContext.Audit_Opportunity
            .Where(a0 => a0.OpportunityId == a.OpportunityId
                         && a0.Id < a.Id
                         && a0.PreviousStatus == 1)
            .OrderByDescending(a0 => a0.Id)
            .Select(a0 => EF.Functions.DateDiffSecond(a0.AuditDate, a.AuditDate))
            .FirstOrDefault()
    })
    .Select(t => (float?)t.Duration) // Convert duration to float
    .Average(); // Calculate the average

Однако я подозреваю, что приведенное выше достаточно близко к вашему исходному запросу, и его может постичь та же судьба перевода. «Хороший» SQL-запрос, который вы включили в свой пост, дал аналогичный перевод.

Я нашел следующее «Как мне написать этот перекрестный запрос в LINQ-to-SQL?» вопрос, на который был многообещающий ответ. Адаптируя этот ответ к вашему случаю, я придумал следующие запросы LINQ (один с использованием синтаксиса запросов LINQ, а другой с использованием синтаксиса метода LINQ).

var result = 
    (from a in dbContext.Audit_Opportunity
     where a.Status == 3 && a.PreviousStatus == 2
     from a0 in (
         from a0 in dbContext.Audit_Opportunity
         where a0.OpportunityId == a.OpportunityId
           && a0.Id < a.Id
           && a0.PreviousStatus == 1
         orderby a0.Id descending
         select a0
         )
         .Take(1)
     select (float?)EF.Functions.DateDiffSecond(a0.AuditDate, a.AuditDate)
    ).Average();
var result = dbContext.Audit_Opportunity
    .Where(a => a.Status == 3 && a.PreviousStatus == 2)
    .SelectMany(a => dbContext.Audit_Opportunity
        .Where(a0 => a0.OpportunityId == a.OpportunityId
                    && a0.Id < a.Id
                    && a0.PreviousStatus == 1)
        .OrderByDescending(a0 => a0.Id)
        .Take(1)
        .Select(a0 => (float?)EF.Functions.DateDiffSecond(a0.AuditDate, a.AuditDate))
    )
    .Average();

Они не проверены, но, надеюсь, их можно будет перевести в работоспособную форму SQL.

Другая идея (которую я не конкретизировал) может заключаться в том, чтобы добавить к подзапросу второе значение выбора (например, a0.Id), чтобы его больше нельзя было транслировать как скалярный подзапрос. Чтобы избежать оптимизации, нам пришлось бы ссылаться на это значение каким-нибудь безобидным способом во внешнем запросе, например where inner.Id > 0. Это некрасиво, но может потребовать другого перевода.

Если ничего не помогает, запасным вариантом является кодирование и выполнение необработанного SQL-запроса.

(Для справки, вот скрипт , содержащий данные, исходные опубликованные запросы и запрос перекрестного применения сверху.)

Я никогда не думал попробовать выполнить запрос таким образом, ваше выражение сработало отлично и дало именно тот SQL-запрос, который мне нужен. Большое спасибо за вашу помощь в этом.

Steven Price 10.08.2024 10:24

@StevenPrice - Интересно, какой вариант LINQ сработал?

T N 10.08.2024 16:43

Привет, ваш последний работал отлично, единственное, что мне нужно было настроить, это перечисления, dbcontext и возврат двойного значения вместо числа с плавающей запятой.

Steven Price 11.08.2024 17:10

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