Entity Framework: Эффективная группировка по месяцам
Я немного поработал над этим, и самое лучшее, что я нашел до сих пор, - использовать Asenumerable для всего набора данных, чтобы фильтрация происходила в linq для объектов, а не для БД. Я использую последнюю версию EF.
Мой рабочий (но очень медленный) код:
var trendData =
from d in ExpenseItemsViewableDirect.AsEnumerable()
group d by new {Period = d.Er_Approved_Date.Year.ToString() + "-" + d.Er_Approved_Date.Month.ToString("00") } into g
select new
{
Period = g.Key.Period,
Total = g.Sum(x => x.Item_Amount),
AveragePerTrans = Math.Round(g.Average(x => x.Item_Amount),2)
};
Это дает мне месяцы в формате YYYY-MM вместе с общей суммой и средней суммой. Однако это занимает несколько минут каждый раз.
Другим обходным решением является выполнение запроса на обновление в SQL, поэтому у меня есть поле YYYYMM для групповой работы. Изменение БД не является легким решением, поэтому любые предложения будут оценены.
В потоке я нашел идею кода выше (http://stackoverflow.com/info/1059737/group-by-weeks-in-linq-to -entities) упоминает "до тех пор, пока .NET 4.0". Что-то недавно появилось, что помогает в этой ситуации?
Ответы
Ответ 1
Причиной низкой производительности является то, что вся таблица извлекается в память (AsEnumerable()). Вы можете группировать его по годам и месяцам следующим образом
var trendData =
(from d in ExpenseItemsViewableDirect
group d by new {
Year = d.Er_Approved_Date.Year,
Month = d.Er_Approved_Date.Month
} into g
select new
{
Year = g.Key.Year,
Month = g.Key.Month,
Total = g.Sum(x => x.Item_Amount),
AveragePerTrans = Math.Round(g.Average(x => x.Item_Amount),2)
}
).AsEnumerable()
.Select(g=>new {
Period = g.Year + "-" + g.Month,
Total = g.Total,
AveragePerTrans = g.AveragePerTrans
});
изменить
Исходный запрос из моего ответа пытался выполнить конкатенацию между int и строкой, которая не переводится EF в SQL-операторы. Я мог бы использовать класс SqlFunctions, но запрос получился добрым уродливым. Таким образом, я добавил AsEnumerable() после того, как была создана группировка, что означает, что EF выполнит групповой запрос на сервере, получит год, месяц и т.д., Но пользовательская проекция выполняется над объектами (что следует после AsEnumerable()).
Ответ 2
Когда дело доходит до группы по месяцам, я предпочитаю выполнять эту задачу следующим образом:
var sqlMinDate = (DateTime) SqlDateTime.MinValue;
var trendData = ExpenseItemsViewableDirect
.GroupBy(x => SqlFunctions.DateAdd("month", SqlFunctions.DateDiff("month", sqlMinDate, x.Er_Approved_Date), sqlMinDate))
.Select(x => new
{
Period = g.Key // DateTime type
})
Поскольку он сохраняет тип datetime в результате группировки.
Ответ 3
Аналогично тому, что написал cryss, я делаю следующее для EF. Обратите внимание, что мы должны использовать EntityFunctions для вызова всех поставщиков баз данных, поддерживаемых EF. SqlFunctions работает только для SQLServer.
var sqlMinDate = (DateTime) SqlDateTime.MinValue;
(from x in ExpenseItemsViewableDirect
let month = EntityFunctions.AddMonths(sqlMinDate, EntityFunctions.DiffMonths(sqlMinDate, x.Er_Approved_Date))
group d by month
into g
select new
{
Period = g.Key,
Total = g.Sum(x => x.Item_Amount),
AveragePerTrans = Math.Round(g.Average(x => x.Item_Amount),2)
}).Dump();
Вкус сгенерированного SQL (из аналогичной схемы):
-- Region Parameters
DECLARE @p__linq__0 DateTime2 = '1753-01-01 00:00:00.0000000'
DECLARE @p__linq__1 DateTime2 = '1753-01-01 00:00:00.0000000'
-- EndRegion
SELECT
1 AS [C1],
[GroupBy1].[K1] AS [C2],
[GroupBy1].[A1] AS [C3]
FROM ( SELECT
[Project1].[C1] AS [K1],
FROM ( SELECT
DATEADD (month, DATEDIFF (month, @p__linq__1, [Extent1].[CreationDate]), @p__linq__0) AS [C1]
FROM [YourTable] AS [Extent1]
) AS [Project1]
GROUP BY [Project1].[C1]
) AS [GroupBy1]