Datetime values generation MS SQL - sql

There are two variables of the DateTime values available: for example, #STARTDATETIME = '2020-10-21 14:45' and #ENDDATETIME = '2020-10-22 19:00'
If there is only a single day between dates like STARTDATETIME = '2020-10-21 12:00' and ENDDATETIME = '2020-10-21 16:00' then the variables must save the initial values.
If there are one or more days between the values then the first date must start with the given timestamp to 16:00. The all in-between days time must be as from 08:00 till 16:00. And the last day must start with 08:00 time till the given timestamp.
Full example:
#STARTDATETIME = '2020-10-21 14:45' and #ENDDATETIME = '2020-10-23 19:15'
Desired output (table):
STARTDATETIME | ENDDATETIME
'2020-10-21 14:45' | '2020-10-21 16:00'
'2020-10-22 08:00' | '2020-10-22 16:00'
'2020-10-23 08:00' | '2020-10-23 19:15'

You can use a recursive CTE:
with dates as (
select #STARTDATETIME as startdt,
(case when datediff(day, #STARTDATETIME, #ENDDATETIME) = 0
then #ENDDATETIME
else dateadd(day, 1, convert(date, #STARTDATETIME))
end) as enddt
union all
select enddte,
(case when datediff(day, enddte, #ENDDATETIME) = 0
then #ENDDATETIME
else dateadd(day, 1, convert(date, enddte))
end) as enddt,
#ENDDATETIME as enddatetime
from dates
where enddt < #enddatetime
)
select *
from date;

This one will give you one row per day:
DECLARE #start dateTime = '20201021 14:45:00'
DECLARE #end dateTime = '20201023 19:15:00'
DECLARE #startDate Date = CAST(#start as date)
DECLARE #endDate Date = CAST(#end as date)
DECLARE #startTime Time = CAST(#start as time)
DECLARE #endTime Time = CAST(#end as time)
DECLARE #startOfDay DateTime = '08:00:00'
DECLARE #endOfDay DateTime = '16:00:00';
WITH lv0 AS (SELECT 0 g UNION ALL SELECT 0)
,lv1 AS (SELECT 0 g FROM lv0 a CROSS JOIN lv0 b) -- 4
,lv2 AS (SELECT 0 g FROM lv1 a CROSS JOIN lv1 b) -- 16
,lv3 AS (SELECT 0 g FROM lv2 a CROSS JOIN lv2 b) -- 256
,lv4 AS (SELECT 0 g FROM lv3 a CROSS JOIN lv3 b) -- 65,536
,lv5 AS (SELECT 0 g FROM lv4 a CROSS JOIN lv4 b) -- 4,294,967,296
,Tally (n) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM lv5)
,
dates AS
(
SELECT
DateAdd(day,n-1,#startDate) as [Day]
FROM tally
WHERE n<=DATEDIFF(day,#startDate,#endDate)+1
)
select
[Day],
CASE WHEN [Day]=#startDate THEN #start ELSE CAST([DAY] AS DateTime)+#startOfDay END as [startOfWork],
CASE WHEN [Day]=#endDate THEN #end ELSE CAST([DAY] AS DateTime)+#endOfDay END as [endOfWork]
FROM dates
result:
| Day | startOfWork | endOfWork |
|------------|-------------------------|-------------------------|
| 2020-10-21 | 2020-10-21 14:45:00.000 | 2020-10-21 16:00:00.000 |
| 2020-10-22 | 2020-10-22 08:00:00.000 | 2020-10-22 16:00:00.000 |
| 2020-10-23 | 2020-10-23 08:00:00.000 | 2020-10-23 19:15:00.000 |

Related

Get startdate and enddate of a every week within the specified date range

I want to get startdate and enddate of a every week within the specified date range for eg: I want start_date of week and end_date of week within 2021-01-01 and 2022-12-31
But want week_start_date as 2021-01-01 for the first week and 2021-12-31 as end date of the 52nd week at the end of the year .
Table should fill like this if start_date is: 2021-01-01 and 2022-12-31
week_start date week_end_date
2021-01-01 2021-01-02
2021-01-03 2021-01-09
2021-01-10 2021-01-16
.
.
.
.
.
.
2021-12-26 2021-12-31
2022-01-01 2022-01-01
2022-01-02 2022-01-08
.
.
How to give first_week_date (year no matter what)-01-01 and end_week_date( year no matter what)-12-31 and in between the logic should be able to calculate week_start_date and week_end_date without problem.
I am using SQL Server for this implementation
You can do something like this
DECLARE #fromDate DATE = '2021-01-01';
DECLARE #toDate DATE = '2021-12-31';
SELECT CASE WHEN WeekStart<#fromDate THEN #fromDate ELSE WeekStart END AS week_start_date,
CASE WHEN WeekEnd>#toDate THEN #toDate ELSE WeekEnd END AS week_end_date
FROM
(
SELECT DATEADD(dd, -(DATEPART(dw, date_column)-1), date_column) AS 'WeekStart',
DATEADD(dd, 7-(DATEPART(dw, date_column)), date_column) AS 'WeekEnd'
FROM table_name
WHERE date_column BETWEEN #fromDate AND #toDate
) a;
Where,
table_name is the name of your table.
date_column is the name of the column containing the date.
This code produces the table requested:
DECLARE #minDate DateTime = '20210101'
DECLARE #maxDate DateTime = '20221231';
WITH lv0 AS (SELECT 0 g UNION ALL SELECT 0)
,lv1 AS (SELECT 0 g FROM lv0 a CROSS JOIN lv0 b) -- 4
,lv2 AS (SELECT 0 g FROM lv1 a CROSS JOIN lv1 b) -- 16
,lv3 AS (SELECT 0 g FROM lv2 a CROSS JOIN lv2 b) -- 256
,lv4 AS (SELECT 0 g FROM lv3 a CROSS JOIN lv3 b) -- 65,536
,lv5 AS (SELECT 0 g FROM lv4 a CROSS JOIN lv4 b) -- 4,294,967,296
,Tally (n) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM lv5)
,DateList AS (
SELECT TOP (dateDiff(day,#minDate,#maxDate)+1)
#minDate+n-1 AS DateValue
FROM Tally)
SELECT
datepart(year,DateValue) AS [Year],
datepart(week,DateValue) AS [Week],
min(dateValue) AS StartDate,
max(dateValue) AS EndDate
FROM DateList
GROUP BY
datepart(year,DateValue),
datepart(week,DateValue)
ORDER BY 3

SQL number of rows valid in a time range grouped by time

SQL Server
I have a table with 2 time stamps, time_start and time_end.
e.g.
ID time_start time_end
---- ------------------- -------------------
1 2019-01-01 08:30:00 2019-01-01 09:40:00
2 2019-01-01 09:10:24 2019-01-01 15:14:19
3 2019-01-01 09:21:15 2019-01-01 09:21:19
4 2019-01-01 10:39:45 2019-01-01 10:58:12
5 2019-01-01 11:39:45 2019-01-01 11:40:10
and I would like to group them so I can have the number of rows grouped by a variable time interval.
e.g.
time_interval row_count
------------------- ---------
2019-01-01 07:00:00 0
2019-01-01 08:00:00 1
2019-01-01 09:00:00 3
2019-01-01 10:00:00 2
2019-01-01 11:00:00 1
2019-01-01 12:00:00 0
My interval could be 1 hour, 1 minute, 30 minutes, 1 day, etc...
Think of it as a log-in/log-out situation, and I want to see how may users were logged at any given minute, hour, day, etc...
Try this,
DECLARE #start_date datetime='2019-01-01',
#end_date datetime='2019-01-02',
#i_minutes int=60
DECLARE #t TABLE
(
id int identity(1,1),time_start datetime,time_end datetime
)
INSERT INTO #t(time_start,time_end)VALUES
('2019-01-01 08:30:00','2019-01-01 09:40:00'),
('2019-01-01 09:10:24','2019-01-01 15:14:19'),
('2019-01-01 09:21:15','2019-01-01 09:21:19'),
('2019-01-01 10:39:45','2019-01-01 10:58:12'),
('2019-01-01 11:39:45','2019-01-01 11:40:10')
--SELECT #start_date=min(time_start),#end_date=max(time_end)
--FROM #t
;WITH CTE_time_Interval AS
(
SELECT #start_date AS time_int,#i_minutes AS i_minutes
UNION ALL
SELECT dateadd(minute,#i_minutes,time_int),i_minutes+ #i_minutes
FROM CTE_time_Interval
WHERE time_int<=#end_date
)
,CTE1 AS
(
SELECT ROW_NUMBER()OVER(ORDER BY time_int)AS r_no,time_int
FROM CTE_time_Interval
)
,CTE2 AS
(
SELECT a.time_int AS Int_start_time,b.time_int AS Int_end_time
FROM CTE1 a
INNER JOIN CTE1 b ON a.r_no+1=b.r_no
)
SELECT a.Int_start_time,a.Int_end_time,sum(iif(b.time_start is not null,1,0)) AS cnt
FROM CTE2 a
LEFT JOIN #t b ON
(
b.time_start BETWEEN a.Int_start_time AND a.Int_end_time
OR
b.time_end BETWEEN a.Int_start_time AND a.Int_end_time
OR
a.Int_start_time BETWEEN b.time_start AND b.time_end
OR
a.Int_end_time BETWEEN b.time_start AND b.time_end
)
GROUP BY a.Int_start_time,a.Int_end_time
Hi this is my workaround.
I created a table "test" with your data.
First I get the min and max intervals and after, I get all the intervals between these values with a CTE.
Finally, with this CTE and a left join with the intervals between time_start and time_end I got the answer.
This is for intervals of 1 hour
DECLARE #minDate AS DATETIME;
DECLARE #maxDate AS DATETIME;
SET #minDate = (select case
when (select min(time_start) from test) < (select min(time_end) from test)
then (select min(time_start) from test)
else (select min(time_end) from test) end )
SET #minDate = FORMAT(#minDate, 'dd-MM.yyyy HH:00:00')
SET #maxDate = (select case
when (select max(time_start) from test) > (select max(time_end) from test)
then (select max(time_start) from test)
else (select max(time_end) from test) end )
SET #maxDate = FORMAT(#maxDate, 'dd-MM.yyyy HH:00:00')
;WITH Dates_CTE
AS (SELECT #minDate AS Dates
UNION ALL
SELECT Dateadd(hh, 1, Dates)
FROM Dates_CTE
WHERE Dates < #maxDate)
SELECT d.Dates as time_interval, count(*) as row_count
FROM Dates_CTE d
LEFT JOIN test t on d.Dates
between (FORMAT(t.time_start, 'dd-MM.yyyy HH:00:00'))
and (FORMAT(t.time_end, 'dd-MM.yyyy HH:00:00'))
GROUP BY d.Dates
For intervals of 10 minutes you need some changes.
First I format dates getting minutes (dd-MM.yyyy HH:mm:00 instead of dd-MM.yyyy HH:00:00)
and in the left join I approach time_start and time_end to their 10 minutes time (9:32:00 in 9:30:00) (dateadd(minute, 10 * (datediff(minute, 0, time_start) / 10), 0)):
DECLARE #minDate AS DATETIME;
DECLARE #maxDate AS DATETIME;
SET #minDate = (select case
when (select min(time_start) from test) < (select min(time_end) from test)
then (select min(time_start) from test)
else (select min(time_end) from test) end )
SET #minDate = FORMAT(#minDate, 'dd-MM.yyyy HH:mm:00')
SET #maxDate = (select case
when (select max(time_start) from test) > (select max(time_end) from test)
then (select max(time_start) from test)
else (select max(time_end) from test) end )
SET #maxDate = FORMAT(#maxDate, 'dd-MM.yyyy HH:mm:00')
;WITH Dates_CTE
AS (SELECT #minDate AS Dates
UNION ALL
SELECT Dateadd(minute, 10, Dates)
FROM Dates_CTE
WHERE Dates < #maxDate)
SELECT d.Dates as time_interval, count(*) as row_count
FROM Dates_CTE d
LEFT JOIN test t on d.Dates
between dateadd(minute, 10 * (datediff(minute, 0, time_start) / 10), 0)
and dateadd(minute, 10 * (datediff(minute, 0, time_end) / 10), 0)
GROUP BY d.Dates
And finally I get this results for 1 hour intervals:
+---------------------+-----------+
| time_interval | row_count |
+---------------------+-----------+
| 01/01/2019 08:00:00 | 1 |
| 01/01/2019 09:00:00 | 3 |
| 01/01/2019 10:00:00 | 2 |
| 01/01/2019 11:00:00 | 2 |
| 01/01/2019 12:00:00 | 1 |
| 01/01/2019 13:00:00 | 1 |
| 01/01/2019 14:00:00 | 1 |
| 01/01/2019 15:00:00 | 1 |
+---------------------+-----------+
I hope it works for you.
You need to specify the time intervals. The rest is a LEFT JOIN/GROUP BY or correlated subquery:
with dates as (
select convert(datetime, '2019-01-01 07:00:00') as dt
union all
select dateadd(hour, 1, dt)
from dates
where dt < '2019-01-01 12:00:00'
)
select dates.dt, count(t.id)
from dates left join
t
on dates.dt < t.time_end and
dates.dt >= dateadd(hour, 1, t.time_start)
group by dates.dt
order by dates.dt;
If you have lots of data and lots of time periods, you might find that this has poor performance. If this is the case, ask a new question, with more information about sizing and performance.

Group time series by time intervals (e.g. days) with aggregate of duration

I have a table containing a time series with following information. Each record represents the event of "changing the mode".
Timestamp | Mode
------------------+------
2018-01-01 12:00 | 1
2018-01-01 18:00 | 2
2018-01-02 01:00 | 1
2018-01-02 02:00 | 2
2018-01-04 04:00 | 1
By using the LEAD function, I can create a query with the following result. Now each record contains the information, when and how long the "mode was active".
Please check the 2nd and the 4th record. They "belong" to multiple days.
StartDT | EndDT | Mode | Duration
------------------+------------------+------+----------
2018-01-01 12:00 | 2018-01-01 18:00 | 1 | 6:00
2018-01-01 18:00 | 2018-01-02 01:00 | 2 | 7:00
2018-01-02 01:00 | 2018-01-02 02:00 | 1 | 1:00
2018-01-02 02:00 | 2018-01-04 04:00 | 2 | 50:00
2018-01-04 04:00 | (NULL) | 1 | (NULL)
Now I would like to have a query that groups the data by day and mode and aggregates the duration.
This result table is needed:
Date | Mode | Total
------------+------+-------
2018-01-01 | 1 | 6:00
2018-01-01 | 2 | 6:00
2018-01-02 | 1 | 1:00
2018-01-02 | 2 | 23:00
2018-01-03 | 2 | 24:00
2018-01-04 | 2 | 04:00
I didn't known how to handle the records that "belongs" to multiple days. Any ideas?
create table ChangeMode ( ModeStart datetime2(7), Mode int )
insert into ChangeMode ( ModeStart, Mode ) values
( '2018-11-15T21:00:00.0000000', 1 ),
( '2018-11-16T17:18:19.1231234', 2 ),
( '2018-11-16T18:00:00.5555555', 1 ),
( '2018-11-16T18:00:01.1234567', 2 ),
( '2018-11-16T19:02:22.8888888', 1 ),
( '2018-11-16T20:00:00.9876543', 2 ),
( '2018-11-17T09:00:00.0000000', 1 ),
( '2018-11-17T23:23:23.0230450', 2 ),
( '2018-11-19T17:00:00.0172839', 1 ),
( '2018-11-20T03:07:00.7033077', 2 )
;
with
-- Determine the earliest and latest dates.
-- Cast to date to remove the time portion.
-- Cast results back to datetime because we're going to add hours later.
MinMaxDates
as
(select cast(min(cast(ModeStart as date))as datetime) as MinDate,
cast(max(cast(ModeStart as date))as datetime) as MaxDate from ChangeMode),
-- How many days have passed during that period
Dur
as
(select datediff(day,MinDate,MaxDate) as Duration from MinMaxDates),
-- Create a list of numbers.
-- These will be added to MinDate to get a list of dates.
NumList
as
( select 0 as Num
union all
select Num+1 from NumList,Dur where Num<Duration ),
-- Create a list of dates by adding those numbers to MinDate
DayList
as
( select dateadd(day,Num,MinDate)as ModeDate from NumList, MinMaxDates ),
-- Create a list of day periods
PeriodList
as
( select ModeDate as StartTime,
dateadd(day,1,ModeDate) as EndTime
from DayList ),
-- Use LEAD to get periods for each record
-- Final record would return NULL for ModeEnd
-- We replace that with end of last day
ModePeriodList
as
( select ModeStart,
coalesce( lead(ModeStart)over(order by ModeStart),
dateadd(day,1,MaxDate) ) as ModeEnd,
Mode from ChangeMode, MinMaxDates ),
ModeDayList
as
( select * from ModePeriodList, PeriodList
where ModeStart<=EndTime and ModeEnd>=StartTime
),
-- Keep the later of the mode start time, and the day start time
-- Keep the earlier of the mode end time, and the day end time
ModeDayPeriod
as
( select case when ModeStart>=StartTime then ModeStart else StartTime end as StartTime,
case when ModeEnd<=EndTime then ModeEnd else EndTime end as EndTime,
Mode from ModeDayList ),
SumDurations
as
( select cast(StartTime as date) as ModeDate,
Mode,
DateDiff_Big(nanosecond,StartTime,EndTime)
/3600000000000
as DurationHours from ModeDayPeriod )
-- List the results in order
-- Use MaxRecursion option in case there are more than 100 days
select ModeDate as [Date], Mode, sum(DurationHours) as [Total Duration Hours]
from SumDurations
group by ModeDate, Mode
order by ModeDate, Mode
option (maxrecursion 0)
Result is:
Date Mode Total Duration Hours
---------- ----------- ---------------------------------------
2018-11-15 1 3.00000000000000
2018-11-16 1 18.26605271947221
2018-11-16 2 5.73394728052777
2018-11-17 1 14.38972862361111
2018-11-17 2 9.61027137638888
2018-11-18 2 24.00000000000000
2018-11-19 1 6.99999519891666
2018-11-19 2 17.00000480108333
2018-11-20 1 3.11686202991666
2018-11-20 2 20.88313797008333
you could use a CTE to create a table of days then join the time slots to it
DECLARE #MAX as datetime2 = (SELECT MAX(CAST(Timestamp as date)) MX FROM process);
WITH StartEnd AS (select p1.Timestamp StartDT,
P2.Timestamp EndDT ,
p1.mode
from process p1
outer apply
(SELECT TOP 1 pOP.* FROM
process pOP
where pOP.Timestamp > p1.Timestamp
order by pOP.Timestamp asc) P2
),
CAL AS (SELECT (SELECT MIN(cast(StartDT as date)) MN FROM StartEnd) DT
UNION ALL
SELECT DATEADD(day,1,DT) DT FROM CAL WHERE CAL.DT < #MAX
),
TMS AS
(SELECT CASE WHEN S.StartDT > C.DT THEN S.StartDT ELSE C.DT END AS STP,
CASE WHEN S.EndDT < DATEADD(day,1,C.DT) THEN S.ENDDT ELSE DATEADD(day,1,C.DT) END AS STE
FROM StartEnd S JOIN CAL C ON NOT(S.EndDT <= C.DT OR S.StartDT>= DATEADD(day,1,C.dt))
)
SELECT *,datediff(MI ,TMS.STP, TMS.ste) as x from TMS
The following uses recursive CTE to build a list of dates (a calendar or number table works equally well). It then intersect the dates with date times so that missing dates are populated with matching data. The important bit is that for each row, if start datetime belongs to previous day then it is clamped to 00:00. Likewise for end datetime.
DECLARE #t TABLE (timestamp DATETIME, mode INT);
INSERT INTO #t VALUES
('2018-01-01 12:00', 1),
('2018-01-01 18:00', 2),
('2018-01-02 01:00', 1),
('2018-01-02 02:00', 2),
('2018-01-04 04:00', 1);
WITH cte1 AS (
-- the min and max dates in your data
SELECT
CAST(MIN(timestamp) AS DATE) AS mindate,
CAST(MAX(timestamp) AS DATE) AS maxdate
FROM #t
), cte2 AS (
-- build all dates between min and max dates using recursive cte
SELECT mindate AS day_start, DATEADD(DAY, 1, mindate) AS day_end, maxdate
FROM cte1
UNION ALL
SELECT DATEADD(DAY, 1, day_start), DATEADD(DAY, 2, day_start), maxdate
FROM cte2
WHERE day_start < maxdate
), cte3 AS (
-- pull end datetime from next row into current
SELECT
timestamp AS dt_start,
LEAD(timestamp) OVER (ORDER BY timestamp) AS dt_end,
mode
FROM #t
), cte4 AS (
-- join datetime with date using date overlap query
-- then clamp start datetime to 00:00 of the date
-- and clamp end datetime to 00:00 of next date
SELECT
IIF(dt_start < day_start, day_start, dt_start) AS dt_start_fix,
IIF(dt_end > day_end, day_end, dt_end) AS dt_end_fix,
mode
FROM cte2
INNER JOIN cte3 ON day_end > dt_start AND dt_end > day_start
)
SELECT dt_start_fix, dt_end_fix, mode, datediff(minute, dt_start_fix, dt_end_fix) / 60.0 AS total
FROM cte4
DB Fiddle
Thanks everybody!
The answer from Cato put me on the right track. Here my final solution:
DECLARE #Start AS datetime;
DECLARE #End AS datetime;
DECLARE #Interval AS int;
SET #Start = '2018-01-01';
SET #End = '2018-01-05';
SET #Interval = 24 * 60 * 60;
WITH
cteDurations AS
(SELECT [Timestamp] AS StartDT,
LEAD ([Timestamp]) OVER (ORDER BY [Timestamp]) AS EndDT,
Mode
FROM tblLog
WHERE [Timestamp] BETWEEN #Start AND #End
),
cteTimeslots AS
(SELECT #Start AS StartDT,
DATEADD(SECOND, #Interval, #Start) AS EndDT
UNION ALL
SELECT EndDT,
DATEADD(SECOND, #Interval, EndDT)
FROM cteTimeSlots WHERE StartDT < #End
),
cteDurationsPerTimesplot AS
(SELECT CASE WHEN S.StartDT > C.StartDT THEN S.StartDT ELSE C.StartDT END AS StartDT,
CASE WHEN S.EndDT < C.EndDT THEN S.EndDT ELSE C.EndDT END AS EndDT,
C.StartDT AS Slot,
S.Mode
FROM cteDurations S
JOIN cteTimeslots C ON NOT(S.EndDT <= C.StartDT OR S.StartDT >= C.EndDT)
)
SELECT Slot,
Mode,
SUM(DATEDIFF(SECOND, StartDT, EndDT)) AS Duration
FROM cteDurationsPerTimesplot
GROUP BY Slot, Mode
ORDER BY Slot, Mode;
With the variable #Interval you are able to define the size of the timeslots.
The CTE cteDurations creates a subresult with the durations of all necessary entries by using the TSQL function LEAD (available in MSSQL >= 2012). This will be a lot faster than an OUTER APPLY.
The CTE cteTimeslots generates a list of timeslots with start time and end time.
The CTE cteDurationsPerTimesplot is a subresult with a JOIN between cteDurations and cteTimeslots. This this the magic JOIN statement from Cato!
And finally the SELECT statement will do the grouping and sum calculation per Slot and Mode.
Once again: Thanks a lot to everybody! Especially to Cato! You saved my weekend!
Regards
Oliver

SQL Server Group by date and by time of day over a date range

I'm not even sure if this can/should be done is SQL but here goes.
I have a table that stores a start date and an end date like so
userPingId createdAt lastUpdatedAt
1 2017-10-17 11:31:52.160 2017-10-18 14:31:52.160
I want to return a result set that groups the results by date and if they were active between different points between the two date.
The different points are
Morning - Before 12pm
Afternoon - Between 12pm and 5pm
Evening - After 5pm
So for example I would get the following results
sessionDate morning afternoon evening
2017-10-17 1 1 1
2017-10-18 1 1 0
Here is what I have so far and I believe that it's quite close but the fact I can't get the results I need make me think that this might not be possible in SQL (btw i'm using a numbers lookup table in my query which I saw on another tutorial)
DECLARE #s DATE = '2017-01-01', #e DATE = '2018-01-01';
;WITH d(sessionDate) AS
(
SELECT TOP (DATEDIFF(DAY, #s, #e) + 1) DATEADD(DAY, n-1, #s)
FROM dbo.Numbers ORDER BY n
)
SELECT
d.sessionDate,
sum(case when
(CONVERT(DATE, createdAt) = d.sessionDate AND datepart(hour, createdAt) < 12)
OR (CONVERT(DATE, lastUpdatedAt) = d.sessionDate AND datepart(hour, lastUpdatedAt) < 12)
then 1 else 0 end) as Morning,
sum(case when
(datepart(hour, createdAt) >= 12 and datepart(hour, createdAt) < 17)
OR (datepart(hour, lastUpdatedAt) >= 12 and datepart(hour, lastUpdatedAt) < 17)
OR (datepart(hour, createdAt) < 12 and datepart(hour, lastUpdatedAt) >= 17)
then 1 else 0 end) as Afternoon,
sum(case when datepart(hour, createdAt) >= 17 OR datepart(hour, lastUpdatedAt) >= 17 then 1 else 0 end) as Evening
FROM d
LEFT OUTER JOIN MYTABLE AS s
ON s.createdAt >= #s AND s.lastUpdatedAt <= #e
AND (CONVERT(DATE, s.createdAt) = d.sessionDate OR CONVERT(DATE, s.lastUpdatedAt) = d.sessionDate)
WHERE d.sessionDate >= #s AND d.sessionDate <= #e
AND userPingId = 49
GROUP BY d.sessionDate
ORDER BY d.sessionDate;
Building on what you started with the numbers table, you can add the time ranges to your adhoc calendar table using another common table expression using cross apply()
and the table value constructor (values (...),(...)).
From there, you can use an inner join based on overlapping date ranges along with conditional aggregation to pivot the results:
declare #s datetime = '2017-01-01', #e datetime = '2018-01-01';
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, d as ( /* adhoc date/numbers table */
select top (datediff(day, #s, #e)+1)
SessionDate=convert(datetime,dateadd(day,row_number() over(order by (select 1))-1,#s))
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by SessionDate
)
, h as ( /* add time ranges to date table */
select
SessionDate
, StartDateTime = dateadd(hour,v.s,SessionDate)
, EndDateTime = dateadd(hour,v.e,SessionDate)
, v.point
from d
cross apply (values
(0,12,'morning')
,(12,17,'afternoon')
,(17,24,'evening')
) v (s,e,point)
)
select
t.userPingId
, h.SessionDate
, morning = count(case when point = 'morning' then 1 end)
, afternoon = count(case when point = 'afternoon' then 1 end)
, evening = count(case when point = 'evening' then 1 end)
from t
inner join h
on t.lastupdatedat >= h.startdatetime
and h.enddatetime > t.createdat
group by t.userPingId, h.SessionDate
rextester demo: http://rextester.com/MVB77123
returns:
+------------+-------------+---------+-----------+---------+
| userPingId | SessionDate | morning | afternoon | evening |
+------------+-------------+---------+-----------+---------+
| 1 | 2017-10-17 | 1 | 1 | 1 |
| 1 | 2017-10-18 | 1 | 1 | 0 |
+------------+-------------+---------+-----------+---------+
Alternately, you could use pivot() instead of conditional aggregation in the final select:
select UserPingId, SessionDate, Morning, Afternoon, Evening
from (
select
t.userPingId
, h.SessionDate
, h.point
from t
inner join h
on t.lastupdatedat >= h.startdatetime
and h.enddatetime > t.createdat
) t
pivot (count(point) for point in ([Morning], [Afternoon], [Evening])) p
rextester demo: http://rextester.com/SKLRG63092
You can using PIVOT on CTE's to derive solution to this problem.
Below is the test table
select * from ping
Below is the sql query
;with details as
(
select userPingId, createdAt as presenceDate , convert(date, createdAt) as
onlyDate,
datepart(hour, createdAt) as onlyHour
from ping
union all
select userPingId, lastUpdatedAt as presenceDate , convert(date,
lastUpdatedAt) as onlyDate,
datepart(hour, lastUpdatedAt) as onlyHour
from ping
)
, cte as
(
select onlyDate,count(*) as count,
case
when onlyHour between 0 and 12 then 'morning'
when onlyHour between 12 and 17 then 'afternoon'
when onlyHour>17 then 'evening'
end as 'period'
from details
group by onlyDate,onlyHour
)
select onlyDate, coalesce(morning,0) as morning,
coalesce(afternoon,0) as afternoon , coalesce(evening,0) as evening from
(
select onlyDate, count,period
from cte ) src
pivot
(
sum(count)
for period in ([morning],[afternoon],[evening])
) p
Below is the final result
This is a fairly similar answer to the one already posted, I just wanted the practice with PIVOT :)
I use a separate table with the time sections in it. this is then cross joined with the number table to create a date and time range for bucketing. i join this to the data and then pivot it (example: https://data.stackexchange.com/stackoverflow/query/750496/bucketing-data-into-date-am-pm-evening-and-pivoting-results)
SELECT
*
FROM (
SELECT
[userPingId],
dt,
[desc]
FROM (
SELECT
DATEADD(D, number, #s) AS dt,
CAST(DATEADD(D, number, #s) AS datetime) + CAST(s AS datetime) AS s,
CAST(DATEADD(D, number, #s) AS datetime) + CAST(e AS datetime) AS e,
[desc]
FROM #numbers
CROSS JOIN #times
WHERE number < DATEDIFF(D, #s, #e)
) ts
INNER JOIN #mytable AS m
ON m.createdat < ts.e
AND m.[lastUpdatedAt] >= ts.s
) src
PIVOT
(
COUNT([userPingId])
FOR [desc] IN ([am], [pm], [ev])
) piv;
the #times table is just:
s e desc
00:00:00.0000000 12:00:00.0000000 am
12:00:00.0000000 17:00:00.0000000 pm
17:00:00.0000000 23:59:59.0000000 ev

Count data for last 10 days including days without any entries

I want to count all data for each of last 10 days where some column have specific value. I manage to do this:
declare #start datetime = CAST(getdate() as date)
declare #end datetime = dateadd(day, -10, #start)
;with amonth(day) as
(
select #end as day
union all
select day + 1
from amonth where day < CAST(#start as date)
)
select CAST(amonth.day as date) as dat, count(vrijeme) as c
from amonth
left join Dnevnik on CAST(vrijeme as date) = CAST(amonth.day as date)
group by CAST(amonth.day as date) order by dat
It gives me a result like this:
dat c
2017-01-21 0 <--- day without any record
2017-01-22 1
2017-01-23 1
2017-01-24 1
2017-01-25 1
2017-01-26 0 <--- day without any record
2017-01-27 27
2017-01-28 125
2017-01-29 190
2017-01-30 127
2017-01-31 319
But I don't want to count all data for each day, I want to count data where one column has specific value. So I thought it might be something like this (I added where clause):
declare #start datetime = CAST(getdate() as date)
declare #end datetime = dateadd(day, -10, #start)
;with amonth(day) as
(
select #end as day
union all
select day + 1
from amonth
where day < CAST(#start as date)
)
select CAST(amonth.day as date) as dat, count(vrijeme) as c
from amonth
left join Dnevnik on CAST(vrijeme as date) = CAST(amonth.day as date)
where tipZapisa = 6
group by CAST(amonth.day as date) order by dat
but this code gives me results without those empty days (where count is 0)
dat c
2017-01-22 1
2017-01-27 9
2017-01-28 67
2017-01-29 33
2017-01-30 46
2017-01-31 37
In the last result, I just want to include days which doesn't have any records.
Thanks in advance
Keep your join LEFT , stop using LEFT columns in NULL -prohibiting predicates in WHERE. Include it in ON
with amonth(day) as
(
select #end as day
union all
select day + 1
from amonth
where day < CAST(#start as date)
)
select CAST(amonth.day as date) as dat, count(vrijeme) as c
from amonth
left join Dnevnik on CAST(vrijeme as date) = CAST(amonth.day as date)
and tipZapisa = 6
group by CAST(amonth.day as date) order by dat
pre populate #START with the date of the 10th matching record, then feed it into your second query
SELECT #START = MIN(DQ.dat) FROM (select DISTINCT TOP 10 CAST(amonth.day as date) as dat
from Dnevnik WHERE tipZapisa = 6 ORDER BY amonth.day as date DESC) DQ
then you should be able to return the actual 10 days with the matching condition