How can i split dates into 15 days intervals in a month - sql

I have sample data set like this :
CREATE TABLE Data
(
Start DATETIME,
EndDt DATETIME,
Amount INT
)
GO
--Insert Data
INSERT INTO Data(Start,EndDt,Amount)
VALUES
('14-Apr-14','13-May-14',200),
('15-May-14','16-Jun-14',320)
Sample Input :
Start End Amount
14-Apr-14 13-May-14 200
15-May-14 16-Jun-14 320
Sample Output :
Start End Amount
14-Apr-14 30-Apr-14 100
01-May-14 13-May-14 100
15-May-14 31-May-14 160
01-Jun-14 16-Jun-14 160
can anyone suggest on this?

This solution works, but assumes you have an ID column on your table. I create a Tally table to be able to generate the date ranges (which in in sets of 16 days, not 15), and then finally get a count in the group and divide Amount by it:
CREATE TABLE dbo.[Data] (ID int IDENTITY,
[Start] date,
EndDt date,
Amount int);
GO
INSERT INTO dbo.[Data] ([Start],
EndDt,
Amount)
VALUES ('14-Apr-14', '13-May-14', 200),
('15-May-14', '16-Jun-14', 320);
GO
--Create a Tally
WITH N AS
(SELECT N
FROM (VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N (N) ),
Tally AS
(SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1,
N N2,
N N3), --1000 rows, more than enough
--Create the dates
Dates AS(
SELECT D.ID,
DATEADD(DAY, (T.I - 1) * 16, D.[Start]) AS [Start],
DATEADD(DAY, T.I * 16, D.[Start]) AS Enddt,
D.EndDt AS FinalDate,
D.Amount
FROM dbo.[Data] D
CROSS JOIN Tally T
WHERE DATEADD(DAY, (T.I - 1) * 16, D.[Start]) < D.EndDt)
--Final result set
SELECT D.ID,
LAG(DATEADD(DAY,1,D.Enddt),1,D.[Start]) OVER (PARTITION BY D.ID ORDER BY D.[Start]) AS [Start],
CASE WHEN D.Enddt > D.FinalDate THEN D.FinalDate ELSE D.Enddt END AS EndDt,
D.Amount / COUNT(ID) OVER (PARTITION BY ID) AS Amount
FROM Dates D
ORDER BY ID, [Start];
DB<>fiddle

Try this. I have tried this code on your table and it is giving desired output
select start as dd,'start' as type,amount/2 as amount from data
union all
select enddt as dd,'end' as type,amount/2 as amount from data
select case when type = 'start' then dd--DATEADD(mm, DATEDIFF(mm, 0, dd), 0)
else DATEADD(mm, DATEDIFF(mm, 0, dd), 0) end
as start,case when type = 'start' then DATEADD (dd, -1, DATEADD(mm, DATEDIFF(mm, 0, dd) + 1, 0))
else dd end as [end],amount
from (select start as dd,'start' as type,amount/2 as amount from data
union all
select enddt as dd,'end' as type,amount/2 as amount from data) a

You can use this query:
select Start a, last_day(Start) b, amount/2 c from Data
union
select trunc(EndDt , 'MM') a, EndDt b, amount/2 c from Data;
Output

Related

SQL Server - Breakdown date period

I want to create a query that breakdowns a date period into 10 days sub-periods
So a period of 2022-04-15 to 2022-05-01 should be broken into
2022-04-15 2022-04-24
2022-04-25 2022-05-01
The period could be one day (2022-04-15 to 2022-04-15) or even years
Any help appreciated
Thank you
A Tally would be a much more performant approach:
DECLARE #Start date = '20220415',
#End date = '20220501',
#Days int = 10;
WITH N AS (
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY,#Start,#End)/#Days)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM N N1, N N2, N N3, N N4) --Up to 1,000 rows. Add more cross joins for more rows
SELECT DATEADD(DAY, T.I*#Days,#Start),
CASE WHEN DATEADD(DAY, ((T.I+1)*#Days)-1,#Start) > #End THEN #END ELSE DATEADD(DAY, ((T.I+1)*#Days)-1,#Start) END
FROM Tally T;
You can use a recursive cte. A rough outline is as follows:
create table #test (
id int identity primary key,
date1 date,
date2 date
);
insert into #test (date1, date2) values
('2022-04-15', '2022-05-01'),
('2022-04-15', '2022-04-15');
with rcte as (
select id, date1 as date1_, dateadd(day, 10, date1) as date2_, date2 as enddate
from #test
union all
select id, date2_, dateadd(day, 10, date2_), enddate
from rcte
where date2_ <= enddate
)
select id, date1_, dateadd(day, -1, date2_)
from rcte
order by 1, 2
DB<>Fiddle
Does this help?
declare #fromdate date=cast('2022-04-15' as date);
declare #todate date=cast('2022-05-01' as date);
WITH cte_dates(tendays)
AS (
SELECT
#fromdate
UNION ALL
SELECT
case when dateadd(d,10,tendays) > #todate then #todate else dateadd(d,10,tendays) end
FROM
cte_dates
WHERE tendays < dateadd(d,-9,#todate)
)
SELECT
tendays,case when dateadd(d,9,tendays) > #todate then #todate else dateadd(d,9,tendays) end
FROM
cte_dates;
DB<>Fiddle

Generate Dates recursively in SQL Server

I have some dates I want to calculate which is currently done over several subqueries. Each subsequent subquery uses the result (a date) of the previous query in its calculation. E.g.
DECLARE #Date DATE = '20170101'
SELECT #foo1 = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #Date
ORDER BY DateField DESC)
SELECT #foo2 = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #foo1
ORDER BY DateField DESC)
....
SELECT #fooN = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #fooNMinus1
ORDER BY DateField DESC)
Is it possible (perhaps using CTE) to make a recursive query to achieve this for a specified number of times?
Weeks are almost always 7 days, so you can get the first one and then just add seven days. If so:
WITH dates as (
SELECT MAX(dbo.DateFunction(DateField)) as dte, 1 as counter
FROM [DateTable]
WHERE DateField <= #Date
UNION ALL
SELECT DATEADD(DAY, 7, dte), counter + 1
FROM dates
WHERE counter < #n
)
SELECT dte
FROM dates;
You can use small tally table as below
Declare #d1 date = '2017-01-01'
Declare #d2 date = '2017-12-31'
select top (datediff(day, #d1, #d2)+1) dt = DateAdd(day, Row_Number() over (order by (Select NULL))-1, #d1)
from master..spt_values s1, master..spt_values s2
Or custom tally tables
;with num as
( select * from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) v(n) )
, n1 as (select n1.* from num n1, num n2, num n3, num n4) --numbers generation
select top (datediff(day, #d1, #d2)+1) dt = DateAdd(day, Row_Number() over (order by (Select NULL))-1, #d1)
from n1
Yes, you can use a recursive query. Since top and aggregates are not allowed in the recursive part, you can use the row_number() function instead.
Declare #date date = cast(getdate() as date), #n int = 10
declare #DateTable table (DateField date)
insert into #DateTable values ('2017-05-01'),('2017-05-02'),('2017-05-03'),('2017-05-04'),('2017-05-05'),('2017-05-06'),('2017-05-07'),('2017-05-08'),('2017-05-09'),('2017-05-10'),
('2017-05-11'),('2017-05-12'),('2017-05-13'),('2017-05-14'),('2017-05-15'),('2017-05-16'),('2017-05-17'),('2017-05-18'),('2017-05-19'),('2017-05-20')
;with date_rte as (
select top 1 dbo.DateFunction(DateField) datefield, 0 recursions, cast(1 as bigint) rn
from #dateTable
where datefield <= #date
order by datefield desc
union all
select dbo.DateFunction(DateField), recursions+1, ROW_NUMBER() over (order by d.datefield desc)
from #datetable d
join date_rte r on d.DateField <= r.datefield
where recursions < #n and rn = 1
)
select datefield
from date_rte
where rn=1 and recursions = #n

Is that possible to write the below query using CTE recursion?

create table #temp (date date)
declare #X date
set #X = '2016-7-01'
declare #Y date
set #Y = cast (getdate() as date)
while(#X<=#Y)
begin
if (datename(WEEKDAY,#X) = 'Sunday')
insert into #temp values (#X)
set #X = cast(((cast(#X as datetime))+1)as date)
continue
end
select * from #temp
drop table #temp
Is that possible to write the above query using CTE recursion?
You can use a CTE to create a numbers table. You can then use the numbers table to get your dates like so:
Declare #Startdate Datetime = '2016-07-01'
Declare #EndDate Datetime = '2016-08-29'
;with
N0 as (SELECT 1 as n UNION ALL SELECT 1)
,N1 as (SELECT 1 as n FROM N0 t1, N0 t2)
,N2 as (SELECT 1 as n FROM N1 t1, N1 t2)
,N3 as (SELECT 1 as n FROM N2 t1, N2 t2)
,N4 as (SELECT 1 as n FROM N3 t1, N3 t2)
,nums as (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) as num FROM N4)
SELECT DATEADD(day,num-1,#startdate) as thedate
FROM nums
WHERE num <= DATEDIFF(day,#startdate,#enddate) + 1
and datename(WEEKDAY, DATEADD(day,num-1,#startdate)) = 'Sunday'
Each table (N0 to nums) effectively multiplies the number of rows in the previous 'table', so you end up with 65,536 rows of numbers in nums (you can do less or more by adding or removing table NX as required). Then, use the numbers table to add days to your start date(SELECT DATEADD(day,num-1,#startdate) as thedate) , where the dates returned are in your date range, and the weekday is Sunday.
Also, because the numbers in nums start at 1, we use nums-1 in our select, so as to avoid skipping over the first date in our series, effectively giving us DATEADD(day, 0, #startdate) in our first row.
You can try something like explained here: http://blog.sqlauthority.com/2009/12/29/sql-server-get-date-of-all-weekdays-or-weekends-of-the-year/
DECLARE #StartDate DATETIME
DECALRE #EndDate DATETIME
SET #StartDate = '2016-07-01'
SET #EndDate = GETDATE()
;WITH cte AS (
SELECT
1 AS DayID,
#StartDate AS FromDate,
DATENAME(dw, #StartDate) AS Dayname
UNION ALL
SELECT
cte.DayID + 1 AS DayID,
DATEADD(d, 1, cte.FromDate),
DATENAME(dw, DATEADD(d, 1, cte.FromDate)) AS Dayname
FROM cte
WHERE DATEADD(d, 1, cte.FromDate) < #EndDate
)
SELECT FromDate AS Date, Dayname
FROM cte
WHERE Dayname IN ('Sunday')
I'm thinking there's be a more efficient way to do this by using a numbers table and a factor of 7 (depending on how many dates you need to get, possible pre-calc the first sunday from your start date, then join all numbers that are factors of 7 from a numbers table), but the above works well enough also.
;with cte
as
(
select getdate() as datee
union all
select dateadd(day,1,datee)
from cte
where datediff(day,getdate(),datee)<100
)
select * from cte
where datename(WEEKDAY,datee) = 'Sunday'
You will hit max recursion limit as well ,to avoid..that use something like below..
option ( MaxRecursion 0 )
I would solve this using numbers table if i am not constrained by the need to use recursive cte ,which is also way faster than Recursive cte..
select
dateadd(day,n,getdate()) as datee
from numbers
where n<100 and datename(weekday,dateadd(day,n,getdate()))='sunday'
To learn why you need numbers table,check this link..https://dba.stackexchange.com/questions/11506/why-are-numbers-tables-invaluable

How to get the summation of days in specific month of year in range

If i have Vacation table with the following structure :
emp_num start_date end_date
234 8-2-2015 8-5-2015
234 6-28-2015 7-1-2015
234 8-29-2015 9-2-2015
115 6-7-2015 6-7-2015
115 8-7-2015 8-10-2015
considering date format is: m/dd/yyyy
How could i get the summation of vacations for every employee during specific month .
Say i want to get the vacations in 8Aug-2015
I want the result like this
emp_num sum
234 7
115 4
7 = all days between 8-2-2015 and 8-5-2015 plus all days between 8-29-2015 AND 8-31-2015 the end of the month
i hope this will help you
declare #temp table
(emp_num int, startdate date, enddate date)
insert into #temp values (234,'8-2-2015','8-5-2015')
insert into #temp values (234,'6-28-2015','7-1-2015')
insert into #temp values (234,'8-29-2015','9-2-2015')
insert into #temp values (115,'6-7-2015','6-7-2015')
insert into #temp values (115,'8-7-2015','8-10-2015')
-- i am passing 8 as month number in your case is August
select emp_num,
SUM(
DATEDIFF (DAY , startdate,
case when MONTH(enddate) = 8
then enddate
else DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,startdate)+1,0))--end date of month
end
)+1) AS Vacation from #temp
where (month(startdate) = 8 OR month(enddate) = 8) AND (Year(enddate)=2015 AND Year(enddate)=2015)
group by emp_num
UPDATE after valid comment: This will fail with these dates: 2015-07-01, 2015-09-30 –#t-clausen.dk
i was assumed OP wants for month only which he will pass
declare #temp table
(emp_num int, startdate date, enddate date)
insert into #temp values (234,'8-2-2015','8-5-2015')
insert into #temp values (234,'6-28-2015','7-1-2015')
insert into #temp values (234,'8-29-2015','9-2-2015')
insert into #temp values (115,'6-7-2015','6-7-2015')
insert into #temp values (115,'8-7-2015','8-10-2015')
insert into #temp values (116,'07-01-2015','9-30-2015')
select emp_num,
SUM(
DATEDIFF (DAY , startdate,
case when MONTH(enddate) = 8
then enddate
else DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,startdate)+1,0))
end
)+1) AS Vacation from #temp
where (Year(enddate)=2015 AND Year(enddate)=2015)
AND 8 between MONTH(startdate) AND MONTH(enddate)
group by emp_num
This will work for sqlserver 2012+
DECLARE #t table
(emp_num int, start_date date, end_date date)
INSERT #t values
( 234, '8-2-2015' , '8-5-2015'),
( 234, '6-28-2015', '7-1-2015'),
( 234, '8-29-2015', '9-2-2015'),
( 115, '6-7-2015' , '6-7-2015'),
( 115, '8-7-2015' , '8-10-2015')
DECLARE #date date = '2015-08-01'
SELECT
emp_num,
SUM(DATEDIFF(day,
CASE WHEN #date > start_date THEN #date ELSE start_date END,
CASE WHEN EOMONTH(#date) < end_date
THEN EOMONTH(#date)
ELSE end_date END)+1) [sum]
FROM #t
WHERE
start_date <= EOMONTH(#date)
and end_date >= #date
GROUP BY emp_num
Using a Tally Table:
SQL Fiddle
DECLARE #month INT,
#year INT
SELECT #month = 8, #year = 2015
--SELECT
-- DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0)) AS start_day,
-- DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0)) AS end_d
;WITH CteVacation AS(
SELECT
emp_num,
start_date = CONVERT(DATE, start_date, 101),
end_date = CONVERT(DATE, end_date, 101)
FROM vacation
)
,E1(N) AS(
SELECT * FROM(VALUES
(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
)t(N)
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b),
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b),
Tally(N) AS(
SELECT TOP(SELECT MAX(DATEDIFF(DAY, start_date, end_date)) FROM vacation)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM E4
)
SELECT
v.emp_num,
COUNT(*)
FROM CteVacation v
CROSS JOIN Tally t
WHERE
DATEADD(DAY, t.N - 1, v.start_date) <= v.end_date
AND DATEADD(DAY, t.N - 1, v.start_date) >= DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0))
AND DATEADD(DAY, t.N - 1, v.start_date) < DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0))
GROUP BY v.emp_num
First, you want to use the correct data type to ease your calculation. In my solution, I used a CTE to format your data type. Then build a tally table from 1 up to the max duration of the all the vacations. Using that tally table, do a CROSS JOIN on the vacation table to generate all vacation dates from its start_date up to end_date.
After that, add a WHERE clause to filter dates that falls on the passed month-year parameter.
Here, #month and #year is declared as INT. What you want is to get all dates from the first day of the month-year up to its last day. The formula for first day of the month is:
DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0))
And for the last day of the month, add one month to the above and just use <:
DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0))
Some common date routines.
More explanation on tally table.
Select(emp_name,start_date,end_date) AS sum_day from table_Name Group by emp_num,start_date,end_date
Try this
with cte(
Select emp_num,DATEDIFF(day,start_date,end_date) AS sum_day from table_Name
Group by emp_num,start_date,end_date
)
Select emp_num,sum(sum_day) as sum_day from cte group by emp_num

Get every hour for a time range

So what I am trying to is generate all the hours that are inside a specific time range.
So given the range 11 AM to 2:00 PM, I would get:
11:00 AM
12:00 PM
1:00 PM
2:00 PM
I am trying to avoid having to store every specific hour a store might be open and just store the range (I need to compare the hours against other times)
Thanks
No loops, recursive CTEs or numbers table required.
DECLARE
#start TIME(0) = '11:00 AM',
#end TIME(0) = '2:00 PM';
WITH x(n) AS
(
SELECT TOP (DATEDIFF(HOUR, #start, #end) + 1)
rn = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns ORDER BY [object_id]
)
SELECT t = DATEADD(HOUR, n-1, #start) FROM x ORDER BY t;
You could use a recursive CTE. This would generate the hours between 11 and 14:
;with Hours as
(
select 11 as hr
union all
select hr + 1
from Hours
where hr < 14
)
select *
from Hours
Live example at SQL Fiddle.
If you have a numbers table (click the link to create one if you don't)...
create table test(
startTime time
, endTime time
)
insert into test
select '11:00', '14:00'
select
dateadd(hh, n.n, t.startTime) as times
from test t
inner join Numbers n
-- assuming your numbers start at 1 rather than 0
on n.n-1 <= datediff(hh, t.startTime, t.endTime)
If this is specialized, you can create an hours table with just 24 values.
create table HoursInADay(
[hours] time not null
, constraint PK_HoursInADay primary key ([hours])
)
-- insert
insert into HoursInADay select '1:00'
insert into HoursInADay select '2:00'
insert into HoursInADay select '3:00'
insert into HoursInADay select '4:00'
insert into HoursInADay select '5:00'
insert into HoursInADay select '6:00'
insert into HoursInADay select '7:00'
...
select
h.[hours]
from test t
inner join HoursInADay h
on h.[hours] between t.startTime and t.endTime
The easiest way I can think of to do this is to have only 1 permanent table with a list of all hours; 24 entries total.
Create table dbo.Hours (Hourly_Time Time NOT NULL)
Insert into dbo.Hours ...
Then, given times A & B:
select * from dbo.Hours where Hourly_Time<=A and Hourly_Time>=B
#Andomar Thanks a lot, you helped me, there is my add above your code.
*----------------------------
create view vw_hoursalot as
with Hours as
(
select DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , -5 , getDate()
)
)
) as dtHr
union all
select DATEADD (minute , 30 , dtHr )
from Hours
where dtHr < DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , +5 , getDate()
)
)
)
)
select * from Hours
----------------------------
select * from vw_hoursalot option (maxrecursion 0)
----------------------------*