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

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

Related

Datetime values generation MS 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 |

Finding all dates after a date for a variable number of days

I have a list of dates in a table. For this examples the 1st day of each month. Let's call it table timeperiod with column endTime
endTime
1-1-2019
2-1-2019
3-1-2019
4-1-2019
I want to find all dates x number of days after each date in a list. Lets say x = 4. Then the list should be:
1-1-2019
1-2-2019
1-3-2019
1-4-2019
2-1-2019
2-2-2019
2-3-2019
2-4-2019
3-1-2019
3-2-2019
3-3-2019
3-4-2019
4-1-2019
4-2-2019
4-3-2019
4-4-2019
I have found solutions to find all dates between dates but I keep getting "Subquery returned more than 1 value" error when I try to use it with a list of dates.
Here is an example of something I tried but doesn't work
declare #days DECIMAL = 4
declare #StartDate date = (select convert(varchar, DATEADD(Day, +0, endTime),101) from timeperiod
declare #EndDate date = (select convert(varchar, DATEADD(Day, +#days, endTime),101) from timeperiod;
;WITH cte AS (
SELECT #StartDate AS myDate
UNION ALL
SELECT DATEADD(day,1,myDate) as myDate
FROM cte
WHERE DATEADD(day,1,myDate) <= #EndDate
)
SELECT myDate
FROM cte
OPTION (MAXRECURSION 0)
Here is a row generator that generates 5 rows, 0 to 4:
WITH rg AS (
SELECT 0 AS rn
UNION ALL
SELECT rg.rn + 1
FROM rg
WHERE rn < 4
)
Here we join it with your existing table that has firsts of the month and use DATEADD to add rn numbers of days (between 0 and 4) to the endPeriod. CROSS JOINing it caused the rows in timePeriod to repeat 5 times each:
SELECT
DATEADD(DAY, rg.rn, timePeriod.endTime) as fakeEndTime
FROM
rg CROSS JOIN timePeriod
I wasn't really clear when you say "days X days after the date, say x = 4" - to me if there is a day that is 1-Jan-2000, then the date 4 days after this is 5-Jan-2000
If you only want the 1,2,3 and 4 of Jan make the row generator < 3 instead of < 4
Already +1'd on Caius Jard's recursive cte.
Here is yet another option using an ad-hoc tally table in concert with a CROSS JOIN
Example
Declare #YourTable Table ([endTime] date)
Insert Into #YourTable Values
('1-1-2019')
,('2-1-2019')
,('3-1-2019')
,('4-1-2019')
Select NewDate = dateadd(DAY,N-1,EndTime)
From #YourTable A
Cross Join (
Select Top (4) N=row_number() over (order by (select null))
From master..spt_values N1
) B
Returns
NewDate
2019-01-01
2019-01-02
2019-01-03
2019-01-04
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-03-01
2019-03-02
2019-03-03
2019-03-04
2019-04-01
2019-04-02
2019-04-03
2019-04-04

split the date range by every 6 months dynamically. Start date can be any thing, but add up to month range

Please help to split the date range by every 6 moths and the start date could be anything but using the start date we need to add up to 09-30 only and the next day which is 10/01 should become start date. I tried using recursive cte but still not getting the exact result
startdate enddate
06-22-2018 09-30-2022
output
startdate enddate
06-22-2018 09-30-2018
10-01-2018 03-31-2019
04-01-2019 09-30-2019
10-01-2019 03-31-2020
04-01-2020 09-30-2020
Here is another option which uses an ad-hoc tally table
Example
Declare #YourTable table (startdate date, enddate date)
Insert Into #YourTable values
('06/22/2018','09/30/2022')
;with cte as (
Select *
,Grp = sum( case when day(D)=1 and month(D) in (4,10) then 1 else 0 end) over (order by d)
From #YourTable A
Cross Apply (
Select Top (DateDiff(DAY,startdate,enddate)+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),startdate)
From master..spt_values n1,master..spt_values n2
) B
)
Select StartDate = min(D)
,EndDate = max(D)
From cte
Group by Grp
Order By min(D)
Returns
StartDate EndDate
2018-06-22 2018-09-30
2018-10-01 2019-03-31
2019-04-01 2019-09-30
2019-10-01 2020-03-31
2020-04-01 2020-09-30
2020-10-01 2021-03-31
2021-04-01 2021-09-30
2021-10-01 2022-03-31
2022-04-01 2022-09-30
Option where we JOIN to an ad-hoc calendar table (note the TOP 10000 and base date of 2000-01-01)
Declare #YourTable table (id int,startdate date, enddate date)
Insert Into #YourTable values
(1,'06/22/2018','09/30/2022')
;with cte as (
Select A.*
,B.D
,Grp = sum( case when day(D)=1 and month(D) in (4,10) then 1 else 0 end) over (order by d)
From #YourTable A
Join (
Select Top 10000 D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),'2000-01-01')
From master..spt_values n1,master..spt_values n2
) B on D between startDate and EndDate
and (D in (startdate,EndDate)
or ( day(D) in (1,day(eomonth(d))) and month(D) in (3,4,9,10))
)
)
Select ID
,StartDate = min(D)
,EndDate = max(D)
From cte
Group by ID,Grp
Order By ID,min(D)
Returns
ID StartDate EndDate
1 2018-06-22 2018-09-30
1 2018-10-01 2019-03-31
1 2019-04-01 2019-09-30
1 2019-10-01 2020-03-31
1 2020-04-01 2020-09-30
1 2020-10-01 2021-03-31
1 2021-04-01 2021-09-30
1 2021-10-01 2022-03-31
1 2022-04-01 2022-09-30
You can use a recursive CTE:
with cte as (
select startdate, eomonth(datefromparts(year(startdate), 9, 1)) as enddate, enddate as orig_enddate
from t
union all
select dateadd(day, 1, enddate), eomonth(dateadd(month, 5, dateadd(day, 1, enddate))) as enddate, orig_enddate
from cte
where enddate < orig_enddate
)
select *
from cte;
Here is a db<>fiddle.
It is unclear what year you want for the first row. As per your question, this uses Sep 30th of the year of the startdate.
If you need more than 100 dates, then add option max(recursion 0).

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

Breaking out yearly payments into monthly payments with month name in a 3 year period

I was wondering where to go from my initial idea. I used the query below to get the month beginning dates for each of the three years:
DECLARE #STARTDATE DATETIME,
#ENDDATE DATETIME;
SELECT #STARTDATE='2013-01-01 00:00:00.000',
#ENDDATE='2015-12-31 00:00:00.000';
WITH [3YearDateMonth]
AS
(
SELECT TOP (DATEDIFF(mm,#STARTDATE,#ENDDATE) + 1)
MonthDate = (DATEADD(mm,DATEDIFF(mm,0,#STARTDATE) + (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1),0))
FROM sys.all_columns ac1
)
SELECT MonthDate
FROM [3YearDateMonth]
I am not sure if I should DATENAME(Month, Monthdate) it later for the month names or just do it in the cte; any suggestions would be great.
My data looks like this:
BeginDate EndDate Payment
2013-01-01 00:00:00.000 2013-12-31 00:00:00.000 3207.70
2014-01-01 00:00:00.000 2014-12-31 00:00:00.000 3303.93
2015-01-01 00:00:00.000 2015-12-31 00:00:00.000 3403.05
Since the payment is yearly I can use payment/12 to get an average monthly amount. I want my data to look like this:
BeginDate EndDate Month MonthlyAmount
2013-01-01 00:00:00.000 2013-01-31 00:00:00.000 January 267.3083
2013-02-01 00:00:00.000 2013-02-31 00:00:00.000 February 267.3083
...
2014-01-01 00:00:00.000 2014-01-31 00:00:00.000 January 275.3275
2014-02-01 00:00:00.000 2014-02-31 00:00:00.000 February 275.3275
...
2015-01-01 00:00:00.000 2015-01-31 00:00:00.000 January 283.5875
2015-02-01 00:00:00.000 2015-02-31 00:00:00.000 February 283.5875
All the way through December for each yearly pay period.
I will be pivoting the Month column later to put the monthly amounts under the corresponding month they belong in.
Is this doable because I feel lost at this point?
Starting with your three data rows, you can use the following query to get your desired results:
with months as
(
select BeginDate
, EndDate
, Payment = Payment / 12.0
from MyTable
union all
select BeginDate = dateadd(mm, 1, BeginDate)
, EndDate
, Payment
from months
where dateadd(mm, 1, BeginDate) < EndDate
)
select BeginDate
, EndDate = dateadd(dd, -1, dateadd(mm, 1, BeginDate))
, Month = datename(mm, BeginDate)
, MonthlyAmount = Payment
from months
order by BeginDate
SQL Fiddle with demo.
Here's a query for you:
WITH L1 (N) AS (SELECT 1 UNION ALL SELECT 1),
L2 (N) AS (SELECT 1 FROM L1, L1 B),
L3 (N) AS (SELECT 1 FROM L2, L2 B),
Num (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM L3)
SELECT
P.BeginDate,
P.EndDate,
M.MonthlyPayDate,
MonthlyAmount =
CASE
WHEN N.N = C.MonthCount
THEN P.Payment - Round(P.Payment / C.MonthCount, 2) * (C.MonthCount - 1)
ELSE Round(P.Payment / C.MonthCount, 2)
END
FROM
dbo.Payment P
CROSS APPLY (
SELECT DateDiff(month, BeginDate, EndDate) + 1
) C (MonthCount)
INNER JOIN Num N
ON C.MonthCount >= N.N
CROSS APPLY (
SELECT DateAdd(month, N.N - 1, BeginDate)
) M (MonthlyPayDate)
ORDER BY
P.BeginDate,
M.MonthlyPayDate
;
See a Live Demo at SQL Fiddle
Pluses:
Doesn't assume 12 months--it will work with any date range.
Properly rounds all non-ultimate months, then assigns the remainder to the last month so that the total sum is accurate. For example, for 2013, the normal monthly payment is 267.31, but December's month's payment is 267.29.
Minuses:
Assumes all dates entirely enclose full months, starting on the 1st and ending on the last day of the month.
If you provide more detail about further requirements regarding pro-rating, I can improve the query for you.