Я пытаюсь написать запрос, который дает мне среднее время между изменениями состояния объекта базы данных. При каждом обновлении в таблице аудита создается запись. Эта таблица содержит поля «Статус» и «Предыдущий статус», что позволяет мне легко запрашивать изменения. Я могу получить нужный результат с помощью следующего запроса, но проблема в том, что он выполняется в памяти, а не в базе данных, что не является лучшим решением.
//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
Независимо от того, что я пытаюсь сделать, это либо вызывает исключение, либо работает, но создает беспорядок в запросе, который, скорее всего, очень неэффективен.
Вот основные столбцы, которые являются ключевыми для этого запроса.
Проблема в том, что 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-запрос, который мне нужен. Большое спасибо за вашу помощь в этом.
@StevenPrice - Интересно, какой вариант LINQ сработал?
Привет, ваш последний работал отлично, единственное, что мне нужно было настроить, это перечисления, dbcontext и возврат двойного значения вместо числа с плавающей запятой.
Добавьте тег для вашей СУБД (выше это похоже на SQL Server). Это повысит видимость тех, кто, скорее всего, поможет. Опубликуйте образец данных в виде текста для таблицы уценки (а не изображения), чтобы мы могли его вырезать и вставить. Какой у вас тип данных
AuditDate
? Опубликованные значения имеют странный формат. Кроме того, ваш первый оператор SQL кажется неполным. Пожалуйста, исправьте.