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]