LINQ2SQL and NHibernate, grouping by calculated columns - c#

In a SQL Server database, I have a table with log entries - timestamp and description.
I need to get logs occurrence in specified intervals (30minutes).
E.g.:
01.01.2015 00:00 - 100
01.01.2015 00:30 - 200
01.01.2015 01:00 - 2
and so on...
What I have:
var logs = session.Query<Log>;
//... other operations on logs queryable
var intervalInMinutes = 30;
var logsFrequency = logs
.GroupBy(l => new
{
Year = l.LogTimestamp.Year,
Month = l.LogTimestamp.Month,
Day = l.LogTimestamp.Day,
Hour = l.LogTimestamp.Hour,
Minute = (l.LogTimestamp.Minute / intervalInMinutes) * intervalInMinutes,
})
.Select(g => new LogsOccurence()
{
StartingDatetime = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, g.Key.Minute, 0),
Frequency = g.Count()
})
.ToList();
Following query fails:
An exception of type 'NHibernate.Exceptions.GenericADOException' occurred in NHibernate.dll but was not handled in user code
Additional information: could not execute query
Inner exception: Column 'dbo.Log.LogTimestamp' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
Generated select statement is:
select
datepart(year, Log0_.LogTimestamp) as col_0_0_
, datepart(month, Log0_.LogTimestamp) as col_1_0_
, datepart(day, Log0_.LogTimestamp) as col_2_0_
, datepart(hour, Log0_.LogTimestamp) as col_3_0_
, datepart(minute, Log0_.LogTimestamp) as col_4_0_
, cast(count(*) as INT) as col_5_0_
from
dbo.[Log] Log0_
group by
datepart(year, Log0_.LogTimestamp)
, datepart(month, Log0_.LogTimestamp)
, datepart(day, Log0_.LogTimestamp)
, datepart(hour, Log0_.LogTimestamp)
, datepart(minute, Log0_.LogTimestamp)/#p0*#p1
Name:p1 - Value:30 Name:p2 - Value:30
Operator precedence / order is wrong in groupby clause
datepart(minute, Log0_.LogTimestamp) / #p0 * #p1
Select part for minutes is different than group part for minutes - select fails
What is wrong with my Linq, and how to write correct one?
I am using NHibernate build 4.0.4.GA.

Operator precedence / order is wrong in groupby clause
The operator precedence looks correct to me. Multiplication and division have the same level of precedence, in both C# and T-SQL. Thus...
datepart(minute, Log0_.LogTimestamp) / #p0 * #p1
... is equivalent to...
(datepart(minute, Log0_.LogTimestamp) / #p0) * #p1
Select part for minutes is different than group part for minutes - select fails.
It is also different in the LINQ query. Since the GroupBy has...
Minute = (l.LogTimestamp.Minute / intervalInMinutes) * intervalInMinutes,
..., the Select should also have...
g.Key.Minute / intervalInMinutes * intervalInMinutes
That might possibly fix it.

Related

Implement an SQL query in LINQ

I'm trying implement the follow query in LINQ, but I don't find solution:
SQL:
SELECT COUNT(*) AS AmountMonths
FROM (SELECT SUBSTRING(CONVERT(NVARCHAR(12), pay_date, 112), 1, 6) AS Month
FROM #tmp
GROUP BY SUBSTRING(CONVERT(NVARCHAR(12), pay_date, 112), 1, 6)) AS AmountMonths
What I need is get the amounts of months in which the clients made payments, with the condition that there may be months in which no payments have been made.
In C# I tried the following:
int amountMonths = payDetail.GroupBy(x => Convert.ToDateTime(x.PayDate)).Count();
and
int amountMonths = payDetail.GroupBy(x => Convert.ToDateTime(x.PayDate).Month).Count();
But I am not getting the expected result.
(Assuming you're using EF Core)
You're almost there. You could do:
var amountMonths = context.AmountMonths.GroupBy(c => new { c.PayDate.Year, c.PayDate.Month }).Count();
This will translate to something like:
SELECT COUNT(*)
FROM (
SELECT DATEPART(year, [a].[PayDate]) AS [a]
FROM [AmountMonths] AS [a]
GROUP BY DATEPART(year, [a].[PayDate]), DATEPART(month, [a].[Pay_Date])
) AS [t]
which I'd find preferable over creating a string and chopping it up. EOMONTH isn't a standard mapped function, alas, otherwise it can be used to convert a date to month level granularity

How to find next (n) open days from SQL opening Hours Database

I am using a SQL database schema similar to the one found on this link.
Best way to store working hours and query it efficiently
I am storing the Opening hours for a location using this basic schema
Shop - INTEGER
DayOfWeek - INTEGER (0-6)
OpenTime - TIME
CloseTime - TIME
What i am trying to do however is for the current DateTime (i.e. today) get the NEXT (n) number of days that the shop is open. So for example if i wasnted to find the next three days that the shop was open and configured in the opening hours the shop is closed on a Sunday and todays date is 21/02/2015 (Saturday) I would like to return the days (21/02/2015)Saturday, (23/02/2015)Monday and (23/02/2015)Tuesday.
If it was Sunday i would return (23/02/2015)Monday, (24/02/2015)Tuesday and (25/02/2015)Wednesday (as its closed on sunday) and finally if it was (20/02/2015)Friday it would return (20/02/2015)Friday, (21/02/2015)Saturday, (23/02/2015)Monday.
I dont know if this is easier to do in SQL or C# but i am mentally struggling in if figuring out how to calculate this.
Any pointers, guidance would be great.
Thank you
This will give you up to 10 days ahead in a fairly efficient way. First test data:
DECLARE #DaysAhead TABLE (
Delta INT
)
INSERT INTO #DaysAhead (Delta)
SELECT 0
UNION ALL SELECT 1
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
UNION ALL SELECT 6
UNION ALL SELECT 7
UNION ALL SELECT 8
UNION ALL SELECT 9
UNION ALL SELECT 10
DECLARE #Opening TABLE (
Shop INT,
DayOfWk INT,
DayNm varchar(10),
OpenTime TIME,
CloseTime TIME
)
INSERT INTO #Opening (Shop, DayOfWk, DayNm, OpenTime, CloseTime)
SELECT 1, 5, 'Fri', '09:00', '17:00' --
UNION ALL SELECT 1, 6, 'Sat' ,'09:00', '17:00'
--UNION ALL SELECT 0, 'Sun', '09:00', '17:00' -- Not open on Sunday
UNION ALL SELECT 1, 1, 'Mon', '09:00', '17:00'
UNION ALL SELECT 1, 2, 'Tue', '09:00', '17:00'
UNION ALL SELECT 1, 3, 'Wed', '09:00', '17:00'
Which can be queried like this:
DECLARE #dt datetime='21-Feb-2015'
DECLARE #dow int=datepart(dw, #dt)-1
SELECT TOP 3 o.Shop, o.DayOfWk, o.DayNm, o.OpenTime, o.CloseTime FROM (
SELECT Delta, ((#dow+Delta)%7) as DayOfWk
FROM #DaysAhead
) daysAhead
INNER JOIN #Opening o on o.DayOfWk=daysAhead.DayOfWk
ORDER BY daysAhead.Delta
Results:
DECLARE #dt datetime='20-Feb-2015' -- Fri
1 5 Fri 09:00:00.0000000 17:00:00.0000000
1 6 Sat 09:00:00.0000000 17:00:00.0000000
1 1 Mon 09:00:00.0000000 17:00:00.0000000
DECLARE #dt datetime='21-Feb-2015' -- Sat
1 6 Sat 09:00:00.0000000 17:00:00.0000000
1 1 Mon 09:00:00.0000000 17:00:00.0000000
1 2 Tue 09:00:00.0000000 17:00:00.0000000
DECLARE #dt datetime='22-Feb-2015' -- Sun
1 1 Mon 09:00:00.0000000 17:00:00.0000000
1 2 Tue 09:00:00.0000000 17:00:00.0000000
1 3 Wed 09:00:00.0000000 17:00:00.0000000
First you can use a simple query like the following to get the days of the week that the shop is open
Select DayOfWeek
From OpenHours
Where ShopId = #ShopID
This assumes that there will not be entries for days that are not open. Adjust this query if instead the open hour column is null, or less than or equal to the close time for days that are not open.
After you run that query and get the results back and preferably translate them into a List<DayOfWeek> you can do the following in your code.
List<Day0fWeek> openDays = GetOpenDaysFromDB();
DateTime start = DateToStartFrom;
int n = numberOfDays;
List<DateTime> nextNOpenDays = new List<DateTime>();
while(nextNOpenDays.Count < n)
{
if(openDays.Contains(start.DayOfWeek))
nextNOpenDays.Add(start);
start = start.AddDays(1);
}
You can use a case to make a day earlier in this week look like that day next week. Here's an example to look up the next open day:
select top 1 dateadd(day, day_diff, #dt) as dt
from (
select case
when dayofweek <= datepart(dw, #dt) then dayofweek + 7
else dayofweek
end - datepart(dw, #dt) as day_diff
, *
from dbo.OpeningHours
) sub1
order by
day_diff
You can then recurse to find more than one day. If we store the above snippet in a function called get_next_open_day, the recursive common table expression could look like:
; with cte as
(
select dbo.get_next_open_day(#dt) as open_day
, 1 as day_number
union all
select dbo.get_next_open_day(prev_day.open_day)
, prev_day.day_number + 1
from cte as prev_day
where prev_day.day_number < #number_of_days
)
select cte.open_day
, datename(dw, cte.open_day)
from cte
option (maxrecursion 100)
;
Here's a full working example:
use Test
if object_id('OpeningHours') is not null
drop table OpeningHours;
if object_id('dbo.get_next_open_day') is not null
drop function dbo.get_next_open_day;
create table OpeningHours (dayofweek int, opentime time, closetime time);
insert dbo.OpeningHours values
(2, '9:00', '17:00'),
(3, '9:00', '17:00'),
(4, '9:00', '17:00'),
(5, '9:00', '17:00'),
(6, '9:00', '21:00'),
(7, '10:00', '17:00')
;
go
create function dbo.get_next_open_day(
#dt date)
returns date
as begin return
(
select top 1 dateadd(day, day_diff, #dt) as dt
from (
select case
when dayofweek <= datepart(dw, #dt) then dayofweek + 7
else dayofweek
end - datepart(dw, #dt) as day_diff
, *
from dbo.OpeningHours
) sub1
order by
day_diff
)
end
go
--declare #dt date = '2015-02-18' -- Wed
--declare #dt date = '2015-02-20' -- Fri
declare #dt date = '2015-02-22' -- Sun
declare #number_of_days int = 10
; with cte as
(
select dbo.get_next_open_day(#dt) as open_day
, 1 as day_number
union all
select dbo.get_next_open_day(prev_day.open_day)
, prev_day.day_number + 1
from cte as prev_day
where prev_day.day_number < #number_of_days
)
select cte.open_day
, datename(dw, cte.open_day)
from cte
option (maxrecursion 100)
;
The implementation of multiple shops is left as an exercise for the reader.
Try this:
DECLARE #t TABLE(WeekID INT, OpenTime time)
DECLARE #c INT = 10
INSERT INTO #t VALUES
(1, '10:00'),--sunday
(2, '10:00'),--monday
(4, '10:00'),--wednsday
(5, '10:00')--thursday
;WITH Tally (n) AS
(
-- 1000 rows
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)
CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n)
CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n)
)
SELECT TOP (#c) DATEADD(dd, t.n, GETDATE())
FROM Tally t
JOIN #t s ON DATEPART(w, DATEADD(dd, t.n, GETDATE())) = s.WeekID
Output:
Date
2015-02-22 --sunday
2015-02-23 --monday
2015-02-25 --wednsday
2015-02-26 --thursday
2015-03-01 --sunday
2015-03-02 --monday
2015-03-04 --wednsday
2015-03-05 --thursday
2015-03-08 --sunday
2015-03-09 --monday
PS: You can replace GETDATE() with any date to look from.
I managed to find a solution:
public List<DateTime> getDaysOpen(int numberOfDays, DateTime start)
{
List<byte> openDays = this.getOpeningHoursDays();
List<DateTime> nextNOpenDays = new List<DateTime>();
while (nextNOpenDays.Count < numberOfDays)
{
if (openDays.Contains(Convert.ToByte(start.DayOfWeek)))
nextNOpenDays.Add(start);
start = start.AddDays(1);
}
return nextNOpenDays;
}
public List<byte> getOpeningHoursDays()
{
return db.OpeningHours.Where(oh => oh.LocationId == this.Id).Select(oh => oh.DateOfWeek).ToList();
}
This was in my opinion the easiest method to find a solution. Thank you for all your help.

Linq to SQL | Top 5 Distinct Order by Date

I have an SQL query which I want to call from LINQ to SQL in asp.net application.
SELECT TOP 5 *
FROM (SELECT SongId,
DateInserted,
ROW_NUMBER()
OVER(
PARTITION BY SongId
ORDER BY DateInserted DESC) rn
FROM DownloadHistory) t
WHERE t.rn = 1
ORDER BY DateInserted DESC
I don't know whether its possible or not through linq to sql, if not then please provide any other way around.
I think you'd have to change the SQL partition to a Linq group-by. (Effectively all the partition does is group by song, and select the newest row for each group.) So something like this:
IEnumerable<DownloadHistory> top5Results = DownloadHistory
// group by SongId
.GroupBy(row => row.SongId)
// for each group, select the newest row
.Select(grp =>
grp.OrderByDescending(historyItem => historyItem.DateInserted)
.FirstOrDefault()
)
// get the newest 5 from the results of the newest-1-per-song partition
.OrderByDescending(historyItem => historyItem.DateInserted)
.Take(5);
Although McGarnagle answer solves the problem, but when i see the execution plan for the two queries, it was really amazing to see that linq to sql was really too slow as compare to native sql queries. See the generated query for the above linq to sql:
--It took 99% of the two execution
SELECT TOP (5) [t3].[SongId], [t3].[DateInserted]
FROM (
SELECT [t0].[SongId]
FROM [dbo].[DownloadHistory] AS [t0]
GROUP BY [t0].[SongId]
) AS [t1]
OUTER APPLY (
SELECT TOP (1) [t2].[SongId], [t2].[DateInserted]
FROM [dbo].[DownloadHistory] AS [t2]
WHERE [t1].[SongId] = [t2].[SongId]
ORDER BY [t2].[DateInserted] DESC
) AS [t3]
ORDER BY [t3].[DateInserted] DESC
--It took 1% of the two execution
SELECT TOP 5 t.SongId,t.DateInserted
FROM (SELECT SongId,
DateInserted,
ROW_NUMBER()
OVER(
PARTITION BY SongId
ORDER BY DateInserted DESC) rn
FROM DownloadHistory) t
WHERE t.rn = 1
ORDER BY DateInserted DESC

Entity Framework: Efficiently grouping by month

I've done a bit of research on this, and the best I've found so far is to use an Asenumerable on the whole dataset, so that the filtering occurs in linq to objects rather than on the DB. I'm using the latest EF.
My working (but very slow) code is:
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)
};
This gives me months in format YYYY-MM, along with the total amount and average amount. However it takes several minutes every time.
My other workaround is to do an update query in SQL so I have a YYYYMM field to group natively by. Changing the DB isn't an easy fix however so any suggestions would be appreciated.
The thread I found the above code idea (http://stackoverflow.com/questions/1059737/group-by-weeks-in-linq-to-entities) mentions 'waiting until .NET 4.0'. Is there anything recently introduced that helps in this situation?
The reason for poor performance is that the whole table is fetched into memory (AsEnumerable()). You can group then by Year and Month like this
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
});
edit
The original query, from my response, was trying to do a concatenation between an int and a string, which is not translatable by EF into SQL statements. I could use SqlFunctions class, but the query it gets kind ugly. So I added AsEnumerable() after the grouping is made, which means that EF will execute the group query on server, will get the year, month, etc, but the custom projection is made over objects (what follows after AsEnumerable()).
When it comes to group by month i prefer to do this task in this way:
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
})
As it keeps datetime type in the grouping result.
Similarly to what cryss wrote, I am doing the following for EF. Note we have to use EntityFunctions to be able to call all DB providers supported by EF. SqlFunctions only works for 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();
A taste of generated SQL (from a similar schema):
-- 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]

Could this be converted to LINQ?

I have a reasonably long SQL query that I need to run in a .Net application. I can make it a stored procedure but I'd like to avoid it if possible (given that I may have a high number of queries in this app).
With this in mind, could something like the following be converted to LINQ or is it too detailed?
-- Compare current period to historical data
select Name ,
avg(TimeProcessing + TimeRendering + TimeDataRetrieval) / 1000 as 'Current Month' ,
isnull(count(TimeProcessing), 0) as 'Sample' ,
min(l2.[Avg_Exec_Time_Previous_Month]) as 'Previous Month' ,
isnull(min(l2.[Executions_Last_Month]), 0) as 'Sample' ,
min(l3.[Avg_Exec_Time_Two_Months_Ago]) as 'Two Months ago' ,
isnull(min(l3.[Executions_Two_Months_Ago]), 0) as 'Sample'
from marlin.report_execution_log l
inner join marlin.report_catalog c on l.ReportID = c.ItemID
left outer join (
select
l2.ReportID ,
(
avg(l2.TimeProcessing + l2.TimeRendering
+ l2.TimeDataRetrieval) / 1000
) as 'Avg_Exec_Time_Previous_Month' ,
count(l2.TimeProcessing) as 'Executions_Last_Month'
from
marlin.report_execution_log l2
where
TimeEnd between dateadd(MONTH, -2, getdate())
and dateadd(MONTH, -1, getdate())
group by
l2.ReportID
) l2 on l.ReportID = l2.ReportID
left outer join (
select
l3.ReportID ,
(
avg(l3.TimeProcessing + l3.TimeRendering + l3.TimeDataRetrieval) / 1000
) as 'Avg_Exec_Time_Two_Months_Ago' ,
count(l3.TimeProcessing) as 'Executions_Two_Months_Ago'
from
marlin.report_execution_log l3
where
TimeEnd between dateadd(MONTH, -3, getdate())
and dateadd(MONTH, -2, getdate())
group by
l3.ReportID
) l3 on l.ReportID = l3.ReportID
group by l.ReportID ,
Name
order by 2 desc
there are many tools /convertor are available on the internet. you can use this tools
why not you try Sql to Linq
LinqPad

Categories

Resources