Append data to split rows - sql

I want to know how many people weren't available in months historically, for that I have an historicTable which contains data from 2012 to 2018 and each row contains how much time an employee wasn't available (vacations, sickness, etc.) this is one example:
idUser startDate endDate daysUn reason nameEmp
--------------------------------------------------------
123 25/01/2018 09/02/2018 12 Sickness John Doe
This is what I need for every row
idUser startDate endDate daysUn reason nameEmp
--------------------------------------------------------
123 25/01/2018 31/01/2018 5 Sickness John Doe
123 01/01/2018 09/02/2018 7 Sickness John Doe
I know this been asked hundred of times here but I'm having trouble doing this for an entire table, for what I've tried in different answers all process work for specific given startdate and enddate columns, and what I need it's to append ALL data to this table and save it as-is so the analyst will be able to study specific cases and specific employees. This is what I get with my current code:
original_INI original_FIN new_INI new_FIN
----------------------- ----------------------- ----------------------- -----------------------
2017-10-15 00:00:00.000 2018-01-06 00:00:00.000 2017-10-15 00:00:00.000 2017-10-31 00:00:00.000
2017-10-15 00:00:00.000 2018-01-06 00:00:00.000 2017-11-01 00:00:00.000 2017-11-30 00:00:00.000
2017-10-15 00:00:00.000 2018-01-06 00:00:00.000 2017-12-01 00:00:00.000 2017-12-31 00:00:00.000
2017-10-15 00:00:00.000 2018-01-06 00:00:00.000 2018-01-01 00:00:00.000 2018-01-06 00:00:00.000
This is the code, original dates are ok as I can sort data more globally but it could print and save the rest of the data so it's more readable:
;WITH n(n) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY [object_id])-1 FROM sys.all_columns
),
d(n,f,t,md,bp,ep) AS
(
SELECT n.n, d.INI, d.FIN,
DATEDIFF(MONTH, d.INI, d.FIN),
DATEADD(MONTH, n.n, DATEADD(DAY, 1-DAY(INI), INI)),
DATEADD(DAY, -1, DATEADD(MONTH, 1, DATEADD(MONTH, n.n,
DATEADD(DAY, 1-DAY(INI), INI))))
FROM n INNER JOIN archivoFuente AS d
ON d.FIN >= DATEADD(MONTH, n.n-1, d.INI)
)
SELECT original_INI = f, original_FIN = t,
new_INI = CASE n WHEN 0 THEN f ELSE bp END,
new_FIN = CASE n WHEN md THEN t ELSE ep END
FROM d WHERE md >= n
ORDER BY original_INI, new_INI;
Any help with the query it's appreciated.

It's pretty easy actually, I used the same code for my requirements, you need to call each column in each select statement so it exist when you split the rows, check this code:
;WITH n(n) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY [object_id])-1 FROM sys.all_columns
),
d(n,f,t,md,bp,ep,
--CALL YOUR COLUMNS HERE EG: name, id, bla, ble
) AS
(
SELECT n.n,d.INI, d.FIN,
DATEDIFF(MONTH, d.INI, d.FIN),
DATEADD(MONTH, n.n, DATEADD(DAY, 1-DAY(INI), INI)),
DATEADD(DAY, -1, DATEADD(MONTH, 1, DATEADD(MONTH, n.n,
DATEADD(DAY, 1-DAY(INI), INI)))),
--CALL YOUR COLUMNS HERE AGAIN, PAY ATTENTION TO NAMES AND COMMAS
d.id_hr,d.Tipo,d.ID_tip,d.Nom_inc,d.RUT,d.Nombre,d.ID_emp,d.Nom_pos,d.Dias_durac,d.Num_lic,d.ID_usu_ap,d.ult_act
FROM n INNER JOIN archivoFuente AS d
ON d.FIN >= DATEADD(MONTH, n.n-1, d.INI)
)
SELECT --PUT ONCE AGAIN YOUR COLUMNS HERE, THIS WILL WORK FOR THE DISPLAYED RESULT
original_INI = f, original_FIN = t,
new_INI = CASE n WHEN 0 THEN f ELSE bp END,
new_FIN = CASE n WHEN md THEN t ELSE ep END
FROM d
WHERE md >= n
ORDER BY original_INI, new_INI;
Now, to save the table, I'd recommend using an INSERT statement to a new table, how will you do it, I don't know, I'am in the same spot as you. Hope someone check this question.

Related

Create a row for each date in a range, and add 1 for each day within a date range for a record in SQL

Suppose I have a date range, #StartDate = 2022-01-01 and #EndDate = 2022-02-01, and this is a reporting period.
In addition, I also have customer records, where each customer has a LIVE Date and a ServiceEndDate (or ServiceEndDate = NULL as they are an ongoing customer)
Some customers may have their Live Date and Service end date range extend outside of the reporting period range. I would only want to report for days that they were a customer in the period.
Name
LiveDate
ServiceEndDate
Tom
2021-10-11
2022-01-13
Mark
2022-11-13
2022-02-15
Andy
2022-01-02
2022-02-10
Rob
2022-01-09
2022-01-14
I would like to create a table where column A is the Date (iterating between every date in the reporting period) and column B is a sum of the number of customers that were a customer on that date.
Something like this
Date
NumberOfCustomers
2022-01-01
2
2022-01-02
3
2022-01-03
3
2022-01-04
3
2022-01-05
3
2022-01-06
3
2022-01-07
3
2022-01-08
3
2022-01-09
4
2022-01-10
4
2022-01-11
4
2022-01-12
4
2022-01-13
4
2022-01-14
3
2022-01-15
3
And so on until the end the #EndDate
Any help would be much appreciated, thanks.
You can join your table to a calendar table containing all the dates you need:
with calendar as
(select cast('2022-01-01' as datetime) as d
union all select dateadd(day, 1, d)
from calendar
where d < '2022-02-01')
select d as "Date", count(*) as NumberOfCustomers
from calendar inner join table_name
on d between LiveDate and coalesce(ServiceEndDate, '9999-12-31')
group by d;
Fiddle
I would personally suggest using a Tally, rather than an rCTE, as a Tally is significantly more performant.
SELECT *
INTO dbo.YourTable
FROM (VALUES('Tom ',CONVERT(date,'2021-10-11 '),CONVERT(date,'2022-01-13')),
('Mark',CONVERT(date,' 2022-11-13'),CONVERT(date,' 2022-02-15')),
('Andy',CONVERT(date,' 2022-01-02'),CONVERT(date,' 2022-02-10')),
('Rob ',CONVERT(date,'2022-01-09 '),CONVERT(date,'2022-01-14')))V(Name,LiveDate,ServiceEndDate);
GO
SELECT *
FROM dbo.YourTable;
GO
DECLARE #StartDate date = '20220101',
#EndDate date = '20220201';
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, #StartDate, #EndDate))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --up to 1,000 days
Dates AS(
SELECT DATEADD(DAY, T.I, #StartDate) AS Date
FROM Tally T)
SELECT D.Date,
COUNT(YT.[Name]) AS NumberOfCustomers
FROM Dates D
LEFT JOIN dbo.YourTable YT ON D.[Date] >= YT.LiveDate
AND (D.[Date] <= YT.ServiceEndDate
OR YT.ServiceEndDate IS NULL)
GROUP BY D.[Date]
ORDER BY D.[Date];
GO
DROP TABLE dbo.YourTable;
Note that then results don't reflect your expected results, I suspect your expected results are wrong. For example you have 2 people live on 2022-01-01, however, there is only 1 person who is live on that date: Tom.
This solution will also never have Mark as "live" (the rCTE method in the other answer won't either) as their end date is before their Live date. If someone can have their service end before it started, I would suggest you have a data quality issue, and you should be adding a CHECK CONSTRAINT to the table to ensure that value of ServiceEndDate is >= LiveDate.

DATEPART fix days

CASE WHEN DAY % 2 = 0 AND POL = 'SUUA'
THEN CONVERT(VARCHAR(15),DATEPART(DAY,5)) + ' TO ' + CONVERT(VARCHAR(15),DATEPART(DAY,3),103)
I set datefirst 1 but i can't how to improve this.
I'm sorry for less information. I'm using SQL Server.
I uploaded the .img to exemplify.
CASE WHEN DAY % 2 = 0 AND POL = 'SUUA'
THEN CONVERT(VARCHAR(15),DATEPART(DAY,5)) + ' TO ' + CONVERT(VARCHAR(15),DATEPART(DAY,3),103)
I want to show somwthing like this
PORT COLLECT DEPARTURE
MANAUS 07/12 TO 12/12 15/12/2018(ODD DAY)
I highly recommend using a Calendar Table for this. A calendar table holds date values with additional information, so it's easier to find particular days (like business days or weekdays, as for this example).
The following solution uses a calendar table and 2 CROSS APPLY operators to get the previous collection dates.
This is how you can create a calendar table (recursive CTE):
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
-- Create a Calendar Table
IF OBJECT_ID('tempdb..#CalendarTable') IS NOT NULL
DROP TABLE #CalendarTable
;WITH CalendarTable AS
(
SELECT
Date = CONVERT(DATE, '2016-01-01'),
Weekday = DATEPART(WEEKDAY, '2016-01-01')
UNION ALL
SELECT
Date = DATEADD(DAY, 1, C.Date),
Weekday = DATEPART(WEEKDAY, DATEADD(DAY, 1, C.Date))
FROM
CalendarTable AS C
WHERE
C.Date <= '2020-01-01'
)
SELECT
C.Date,
C.Weekday
INTO
#CalendarTable
FROM
CalendarTable AS C
OPTION
(MAXRECURSION 0)
The table is like the following:
SELECT * FROM #CalendarTable ORDER BY Date DESC
Date Weekday
2020-01-02 4
2020-01-01 3
2019-12-31 2
2019-12-30 1
2019-12-29 7
2019-12-28 6
2019-12-27 5
2019-12-26 4
2019-12-25 3
2019-12-24 2
2019-12-23 1
2019-12-22 7
2019-12-21 6
2019-12-20 5
2019-12-19 4
2019-12-18 3
2019-12-17 2
2019-12-16 1
2019-12-15 7
2019-12-14 6
2019-12-13 5
2019-12-12 4
2019-12-11 3
We will use this to find the closest Wednesday and Monday just before a particular Departure date. We find this using a CROSS APPLY having the DepartureDate as a higher limit, then searching for the particular weekday (1 for monday, 3 for wednesday). Then use TOP 1 with ORDER BY Date DESC to get the highest Monday/Wednesday just before that departure date.
-- Build your Collect periods
;WITH SampleData AS
(
SELECT
V.Departure
FROM
(VALUES
('2018-12-01'),
('2018-12-09'),
('2018-12-25'),
('2018-12-29'),
('2019-01-02'),
('2019-01-07'),
('2019-01-10')) AS V(Departure)
)
SELECT
V.Departure,
-- Friday to Wednesday
ClosestWednesdayBeforeDeparture = W.Date,
PreviousFridayOfThatWednesday = DATEADD(DAY, -5, W.Date),
-- Wednesday to Monday
ClosestMondayBeforeDeparture = M.Date,
PreviousWednesdayOfThatMonday = DATEADD(DAY, -5, M.Date),
-- Check for odd/even
IsOdd = CASE WHEN DATEPART(DAY, V.Departure) % 2 = 1 THEN 1 ELSE 0 END,
-- Use previous expressions to build your collect periods
Collect = CASE
WHEN
DATEPART(DAY, V.Departure) % 2 = 1 -- IsOdd
THEN
CONVERT(VARCHAR(100), DATEADD(DAY, -5, W.Date), 120) -- PreviousFridayOfThatWednesday
+ ' TO '
+ CONVERT(VARCHAR(100), W.Date, 120) -- ClosestWednesdayBeforeDeparture
ELSE -- IsEven
CONVERT(VARCHAR(100), DATEADD(DAY, -5, M.Date), 120) -- PreviousWednesdayOfThatMonday
+ ' TO '
+ CONVERT(VARCHAR(100), M.Date, 120) -- ClosestMondayBeforeDeparture
END
FROM
SampleData AS V
CROSS APPLY (
SELECT TOP 1
C.Date
FROM
#CalendarTable AS C
WHERE
C.Date < V.Departure AND
C.Weekday = 3 -- 3: Wednesday
ORDER BY
C.Date DESC) AS W
CROSS APPLY (
SELECT TOP 1
C.Date
FROM
#CalendarTable AS C
WHERE
C.Date < V.Departure AND
C.Weekday = 1 -- 1: Monday
ORDER BY
C.Date DESC) AS M
ORDER BY
V.Departure
Finding the previous Friday from a Wednesday is as simple as moving backwards 5 days, the same goes from Monday to Wednesday.
Results:
Departure IsOdd Collect ClosestWednesdayBeforeDeparture PreviousFridayOfThatWednesday ClosestMondayBeforeDeparture PreviousWednesdayOfThatMonday
2018-12-01 1 2018-11-23 TO 2018-11-28 2018-11-28 2018-11-23 2018-11-26 2018-11-21
2018-12-09 1 2018-11-30 TO 2018-12-05 2018-12-05 2018-11-30 2018-12-03 2018-11-28
2018-12-25 1 2018-12-14 TO 2018-12-19 2018-12-19 2018-12-14 2018-12-24 2018-12-19
2018-12-29 1 2018-12-21 TO 2018-12-26 2018-12-26 2018-12-21 2018-12-24 2018-12-19
2019-01-02 0 2018-12-26 TO 2018-12-31 2018-12-26 2018-12-21 2018-12-31 2018-12-26
2019-01-07 1 2018-12-28 TO 2019-01-02 2019-01-02 2018-12-28 2018-12-31 2018-12-26
2019-01-10 0 2019-01-02 TO 2019-01-07 2019-01-09 2019-01-04 2019-01-07 2019-01-02
This was a good SQL exercise.
Really thanks #Ezlo, thats was excellent. It's a good SQL exercise. I think this will suit me well on my job. Sorry to look boring, I've some questions:
Edit:. I noticed that the dates of V(Departure) is fix. I want to make the dates dinamic. Because when I set the days on
CROSS APPLY(SELECT TOP 1
C.DATE
FROM
#CALENDARTABLE C, SAMPLEDATA V
WHERE
C.DATE < V.DEPARTURE AND
C.WEEKDAY = 1
ORDER BY
C.DATE DESC) AS W
CROSS APPLY (SELECT TOP 1
C.DATE
FROM
#CALENDARTABLE C,SAMPLEDATA V
WHERE
C.DATE < V.DEPARTURE AND
C.WEEKDAY = 7
ORDER BY
C.DATE DESC) AS M
The dates of collect as fix for all Ports.
Sorry if i boring. You guys help a lot.

Summing Records within a Moving Date Range, Date Distances

I have complex calculation requirement for a user logging system. I need to locate the most frequently active users based on their number of logins within a 180 day window. Once two login dates are 181 days apart, they do not count towards a total but could count towards a total when grouped with other dates.
For example here is Jim's login history:
Jim 2018-01-01
Jim 2018-04-01
Jim 2018-05-01
Jim 2018-06-01
Jim 2018-07-01
Jim 2018-08-01
Jim 2018-09-01
Jim 2018-12-01
Using 6 months, instead of 180 days, for simplicity, and only looking 6 months in one direction, Jim had the following totals:
Logins: 5 (2018-01-01 + 6 months)
Logins: 6 (2018-04-01 + 6 months)
Logins: 5 (2018-05-01 + 6 months)
Logins: 5 (2018-06-01 + 6 months)
Logins: 4 (2018-07-01 + 6 months)
Logins: 3 (2018-08-01 + 6 months)
Logins: 2 (2018-09-01 + 6 months)
Logins: 1 (2018-12-01 + 6 months)
So my system would report back 6 because it only wants the maximum total.
Other than brute force calculation, I'm lost on how to construct this system. Yes I can denormalize data to any degree, speed is most important.
Try this:
declare #tbl table(name char(3), dt date);
insert into #tbl values
('Jim', '2018-01-01'),
('Jim', '2018-04-01'),
('Jim', '2018-05-01'),
('Jim', '2018-06-01'),
('Jim', '2018-07-01'),
('Jim', '2018-08-01'),
('Jim', '2018-09-01'),
('Jim', '2018-12-01');
;with cte as (
select name, dt, DATEADD(day, 181, dt) upperDt from #tbl
), cte2 as (
select name,
(select COUNT(*) from cte where dt between c.dt and c.upperDt and name = c.name) cnt
from cte c
)
select name, MAX(cnt) [max]
from cte2
group by name
Try this, using a Common Table Expression to Calculate the EndDate Window and CROSS APPLY to calculate the total number of logins
DECLARE #t TABLE (UserName NVARCHAR(10), LoginDate DATETIME)
INSERT INTO #t
(UserName,LoginDate) VALUES
('Jim','2018-01-01'),
('Jim','2018-04-01'),
('Jim','2018-05-01'),
('Jim','2018-06-01'),
('Jim','2018-07-01'),
('Jim','2018-08-01'),
('Jim','2018-09-01'),
('Jim','2018-12-01')
; WITH CteDateRange
AS(
SELECT
T.UserName
,T.LoginDate
--,EndDateRange = DATEADD(DAY, 181, LoginDate)
,EndDateRange = DATEADD(MONTH, 6, LoginDate)
FROM #t T
)
SELECT
DR.UserName
,DR.LoginDate
,DR.EndDateRange
,T.Total
FROM CteDateRange DR
CROSS APPLY ( SELECT Total = COUNT(D.LoginDate)
FROM CteDateRange D
WHERE D.LoginDate >= DR.LoginDate
AND D.LoginDate <= DR.EndDateRange
AND D.UserName = DR.UserName
) T
Output
UserName LoginDate EndDateRange Total
Jim 2018-01-01 00:00:00.000 2018-07-01 00:00:00.000 5
Jim 2018-04-01 00:00:00.000 2018-10-01 00:00:00.000 6
Jim 2018-05-01 00:00:00.000 2018-11-01 00:00:00.000 5
Jim 2018-06-01 00:00:00.000 2018-12-01 00:00:00.000 5
Jim 2018-07-01 00:00:00.000 2019-01-01 00:00:00.000 4
Jim 2018-08-01 00:00:00.000 2019-02-01 00:00:00.000 3
Jim 2018-09-01 00:00:00.000 2019-03-01 00:00:00.000 2
Jim 2018-12-01 00:00:00.000 2019-06-01 00:00:00.000 1
One basic solution uses a join:
select l.*
from (select l.name, count(*) as cnt,
row_number() over (partition by name order by count(*) desc) as seqnum
from logins l join
logins l2
on l.name = l2.name and
l2.date >= l.date and l2.date < dateadd(day, 181, l.date)
group by l.name
) l
where seqnum = 1;
This might have acceptable performance with an index on logins(name, date).

SQL Server : Gap / Island, datetime, contiguous block 365 day block

I have a table that looks like this:-
tblMeterReadings
id meter period_start period_end amount
1 1 2014-01-01 00:00 2014-01-01 00:29:59 100.3
2 1 2014-01-01 00:30 2014-01-01 00:59:59 50.5
3 1 2014-01-01 01:00 2014-01-01 01:29:59 70.7
4 1 2014-01-01 01:30 2014-01-01 01:59:59 900.1
5 1 2014-01-01 02:00 2014-01-01 02:29:59 400.0
6 1 2014-01-01 02:30 2014-01-01 02:59:59 200.3
7 1 2014-01-01 03:00 2014-01-01 03:29:59 100.8
8 1 2014-01-01 03:30 2014-01-01 03:59:59 140.3
This is a tiny "contiguous block" from '2014-01-01 00:00' to '2014-01-01 3:59:59'.
In the real table there are "contiguous blocks" of years in length.
I need to find the the period_start and period_end of the most recent CONTINUOUS 365 COMPLETE DAYs (fileterd by meter column).
When I say COMPLETE DAYs I mean a day that has entries spanning 00:00 to 23:59.
When I say CONTINUOUS I mean there must be no days missing.
I would like to select all the rows that make up this block of CONTINUOUS COMPLETE DAYs.
I also need an output like:
block_start block_end total_amount_for_block
2013-02-26 00:00 2014-02-26 23:59:59 1034234.5
This is beyond me, so if someone can solve... I will be very impressed.
Since your granularity is 1 second, you need to expand your periods into all the date/times between the start and end at 1 second intervals. To do this you need to cross join with a numbers table (The numbers table is generated on the fly by ranking object ids from an arbitrary system view, I have limited it to TOP 86400 since this is the number of seconds in a day, and you have stated your time periods never span more than one day):
WITH Numbers AS
( SELECT TOP (86400)
Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
ORDER BY a.object_id
)
SELECT r.ID, r.meter, dt.[DateTime]
FROM tblMeterReadings r
CROSS JOIN Numbers n
OUTER APPLY
( SELECT [DateTime] = DATEADD(SECOND, n.Number, r.period_start)
) dt
WHERE dt.[DateTime] <= r.Period_End;
You then have your continuous range in which to perform the normal gaps and islands grouping:
WITH Numbers AS
( SELECT TOP (86400)
Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
ORDER BY a.object_id
), Grouped AS
( SELECT r.meter,
Amount = CASE WHEN Number = 1 THEN r.Amount ELSE 0 END,
dt.[DateTime],
GroupingSet = DATEADD(SECOND,
-DENSE_RANK() OVER(PARTITION BY r.Meter
ORDER BY dt.[DateTime]),
dt.[DateTime])
FROM tblMeterReadings r
CROSS JOIN Numbers n
OUTER APPLY
( SELECT [DateTime] = DATEADD(SECOND, n.Number, r.period_start)
) dt
WHERE dt.[DateTime] <= r.Period_End
)
SELECT meter,
PeriodStart = MIN([DateTime]),
PeriodEnd = MAX([DateTime]),
Amount = SUM(Amount)
FROM Grouped
GROUP BY meter, GroupingSet
HAVING DATEADD(YEAR, 1, MIN([DateTime])) < MAX([DateTime]);
N.B. Since the join to Number causes amounts to be duplicated, it is necessary to set all duplicates to 0 using CASE WHEN Number = 1 THEN r.Amount ELSE 0 END, i.e only include the amount for the first row for each ID
Removing the Having clause for your sample data will give:
meter | PeriodStart | PeriodEnd | Amount
------+---------------------+---------------------+----------
1 | 2014-01-01 00:00:00 | 2014-01-01 03:59:59 | 1963
Example on SQL Fiddle
You could try this:
Select MIN(period_start) as "block start"
, MAX(period_end) as "block end"
, SUM(amount) as "total amount"
FROM YourTable
GROUP BY datepart(year, period_start)
, datepart(month, period_start)
, datepart(day, period_start)
, datepart(year, period_end)
, datepart(month, period_end)
, datepart(day, period_end)
Having datepart(year, period_start) = datepart(year, period_end)
AND datepart(month, period_start) = datepart(month, period_end)
AND datepart(day, period_start) = datepart(day, period_end)
AND datepart(hour, MIN(period_start)) = 0
AND datepart(minute,MIN(period_start)) = 0
AND datepart(hour, MAX(period_end)) = 23
AND datepart(minute,MIN(period_end)) = 59

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.