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

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.

Related

How to Convert a Date Span to Monthly Records using SQL

I have multiple date spans for the user over a period of few months, I would like to split each span to multiple rows by month and year(default to first day of the month) for which user has been active during the span period. Active user will have future end date records to be split up until the current month and year
Existing Data
ID
Start date
end date
1234
2019-01-01
2019-03-31
1234
2019-09-18
2020-01-31
1234
2022-11-15
2025-01-31
Tried to place the below date month query into the spans
Select Top 500 mmdd=cast (dateadd(Month,-1+Row_Number() Over (Order By (Select NULL)),'2019-01-01') as date)
From master..spt_values n1
order by 1 asc
EXPECTED OUTPUT
ID
active month
1234
2019-01-01
1234
2019-02-01
1234
2019-03-01
1234
2019-09-01
1234
2019-10-01
1234
2019-11-01
1234
2019-12-01
1234
2020-01-01
1234
2022-11-01
1234
2022-12-01
1234
2023-01-01
Larnu is on the right track. One of the easiest ways I've found is to create a calendar table or a function (which can effectively do the same thing).
Try this:
CREATE FUNCTION [dbo].[udfCalendar]
(
#StartDate Date,
#EndDate Date
)
RETURNS #Calendar TABLE (ID int, DateValue DateTime, DayValue int, MonthValue int, YearValue int)
AS
BEGIN
WHILE #StartDate < #EndDate
BEGIN
INSERT #Calendar
SELECT --like 20190101, 1/1/2019, 1, 1, 2019
YEAR (#StartDate) * 10000 + MONTH (#StartDate) * 100 + Day (#StartDate) AS ID,
#StartDate AS DateValue,
DATEPART (dd, #StartDate) AS DayValue,
DATEPART (mm, #StartDate) AS MonthValue,
DATEPART (yy, #StartDate) AS YearValue;
SET #StartDate = DateAdd(m, 1, #StartDate);
END
RETURN;
END
Then you can join to it
Select n1.ID, cal.DateValue as ActiveMonth
From master..spt_values n1 inner join
dbo.udfCalendar('1/1/2019', '1/1/2023') cal
On cal.DateValue Between n1.StartDate and n1.EndDate
Order By DateValue

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.

Splitting dates into intervals using Start Date and End Date

I have scenario where I need to split the given date range into monthly intervals.
For example, the input is like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-02-23
2018-02-24 2018-03-31
2018-04-01 2018-08-16
2018-08-17 2018-12-31
And the expected output should be like below:
StartDate EndDate
2018-01-21 2018-01-29
2018-01-30 2018-01-31
2018-02-01 2018-02-23
2018-02-24 2018-02-28
2018-03-01 2018-03-31
2018-04-01 2018-04-30
2018-05-01 2018-05-31
2018-06-01 2018-06-30
2018-07-01 2018-07-31
2018-08-01 2018-08-16
2018-08-17 2018-08-31
2018-09-01 2018-09-30
2018-10-01 2018-10-31
2018-11-01 2018-11-30
2018-12-01 2018-12-31
Below is the sample data.
CREATE TABLE #Dates
(
StartDate DATE,
EndDate DATE
);
INSERT INTO #Dates
(
StartDate,
EndDate
)
VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
You can use a recursive CTE. The basic idea is to start with the first date 2018-01-21 and build a list of all months' start and end date upto the last date 2018-12-31. Then inner join with your data and clamp the dates if necessary.
DECLARE #Dates TABLE (StartDate DATE, EndDate DATE);
INSERT INTO #Dates (StartDate, EndDate) VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');
WITH minmax AS (
-- clamp min(start date) to 1st day of that month
SELECT DATEADD(MONTH, DATEDIFF(MONTH, CAST('00010101' AS DATE), MIN(StartDate)), CAST('00010101' AS DATE)) AS mindate, MAX(EndDate) AS maxdate
FROM #Dates
), months AS (
-- calculate first and last day of each month
-- e.g. for February 2018 it'll return 2018-02-01 and 2018-02-28
SELECT mindate AS date01, DATEADD(DAY, -1, DATEADD(MONTH, 1, mindate)) AS date31, maxdate
FROM minmax
UNION ALL
SELECT DATEADD(MONTH, 1, prev.date01), DATEADD(DAY, -1, DATEADD(MONTH, 2, prev.date01)), maxdate
FROM months AS prev
WHERE prev.date31 < maxdate
)
SELECT
-- clamp start and end date to first and last day of corresponding month
CASE WHEN StartDate < date01 THEN date01 ELSE StartDate END,
CASE WHEN EndDate > date31 THEN date31 ELSE EndDate END
FROM months
INNER JOIN #Dates ON date31 >= StartDate AND EndDate >= date01
If rCTE is not an option you can always JOIN with a table of numbers or table of dates (the idea above still applies).
You can Cross Apply with the Master..spt_values table to get a row for each month between StartDate and EndDate.
SELECT *
into #dates
FROM (values
('2018-01-21', '2018-01-29')
,('2018-01-30', '2018-02-23')
,('2018-02-24', '2018-03-31')
,('2018-04-01', '2018-08-16')
,('2018-08-17', '2018-12-31')
)d(StartDate , EndDate)
SELECT
SplitStart as StartDate
,case when enddate < SplitEnd then enddate else SplitEnd end as EndDate
FROM #dates d
cross apply (
SELECT
cast(dateadd(mm, number, dateadd(dd, (-datepart(dd, d.startdate) +1) * isnull((number / nullif(number, 0)), 0), d.startdate)) as date) as SplitStart
,cast(dateadd(dd, -datepart(dd, dateadd(mm, number+1, startdate)), dateadd(mm, number+1, startdate)) as date) as SplitEnd
FROM
master..spt_values
where type = 'p'
and number between 0 and (((year(enddate) - year(startdate)) * 12) + month(enddate) - month(startdate))
) s
drop table #dates
The following should also work
First i put startdates and enddates into a single column in the cte-block data.
In the block som_eom, i create the start_of_month and end_of_month for all 12 months.
I union steps 1 and 2 into curated_set
I create curated_set which is ordered by the date column
Finally i reject the unwanted records, in my filter clause not in('som','StartDate')
with data
as (select *
from dates
unpivot(x for y in(startdate,enddate))t
)
,som_eom
as (select top 12
cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date) as som
,dateadd(dd
,-1
,dateadd(mm
,1
,cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date)
)
) as eom
from information_schema.tables
)
,curated_set
as(select *
from data
union all
select *
from som_eom
unpivot(x for y in(som,eom))t
)
,curated_data
as(select x
,y
,lag(x) over(order by x) as prev_val
from curated_set
)
select prev_val as st_dt,x as end_dt
,y
from curated_Data
where y not in('som','StartDate')
Start with the initial StartDate and calculate the end of month or simply use the EndDate if it's within the same month.
Use the newly calculated EndDate+1 as StartDate for recursion and repeat the calculation.
WITH cte AS
( SELECT StartDate, -- initial start date
CASE WHEN EndDate < DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
END AS newEnd, -- LEAST(end of current month, EndDate)
EndDate
FROM #Dates
UNION ALL
SELECT dateadd(DAY,1,newEnd), -- previous end + 1 day, i.e. 1st of current month
CASE WHEN EndDate <= DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
THEN EndDate
ELSE DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0))
END, -- LEAST(end of next month, EndDate)
EndDate
FROM cte
WHERE newEnd < EndDate
)
SELECT StartDate, newEnd
FROM cte

SQL spread month value into weeks

I have a table where I have values by month and I want to spread these values by week, taking into account that weeks that spread into two month need to take part of the value of each of the month and weight on the number of days that correspond to each month.
For example I have the table with a different price of steel by month
Product Month Price
------------------------------------
Steel 1/Jan/2014 100
Steel 1/Feb/2014 200
Steel 1/Mar/2014 300
I need to convert it into weeks as follows
Product Week Price
-------------------------------------------
Steel 06-Jan-14 100
Steel 13-Jan-14 100
Steel 20-Jan-14 100
Steel 27-Jan-14 128.57
Steel 03-Feb-14 200
Steel 10-Feb-14 200
Steel 17-Feb-14 200
As you see above, the week that overlaps between Jan and Feb needs to be calculated as follows
(100*5/7)+(200*2/7)
This takes into account tha the week of the 27th has 5 days that fall into Jan and 2 into Feb.
Is there any possible way to create a query in SQL that would achieve this?
I tried the following
First attempt:
select
WD.week,
PM.PRICE,
DATEADD(m,1,PM.Month),
SUM(PM.PRICE/7) * COUNT(*)
from
( select '2014-1-1' as Month, 100 as PRICE
union
select '2014-2-1' as Month, 200 as PRICE
)PM
join
( select '2014-1-20' as week
union
select '2014-1-27' as week
union
select '2014-2-3' as week
)WD
ON WD.week>=PM.Month
AND WD.week < DATEADD(m,1,PM.Month)
group by
WD.week,PM.PRICE, DATEADD(m,1,PM.Month)
This gives me the following
week PRICE
2014-1-20 100 2014-02-01 00:00:00.000 14
2014-1-27 100 2014-02-01 00:00:00.000 14
2014-2-3 200 2014-03-01 00:00:00.000 28
I tried also the following
;with x as (
select price,
datepart(week,dateadd(day, n.n-2, t1.month)) wk,
dateadd(day, n.n-1, t1.month) dt
from
(select '2014-1-1' as Month, 100 as PRICE
union
select '2014-2-1' as Month, 200 as PRICE) t1
cross apply (
select datediff(day, t.month, dateadd(month, 1, t.month)) nd
from
(select '2014-1-1' as Month, 100 as PRICE
union
select '2014-2-1' as Month, 200 as PRICE)
t
where t1.month = t.month) ndm
inner join
(SELECT (a.Number * 256) + b.Number AS N FROM
(SELECT number FROM master..spt_values WHERE type = 'P' AND number <= 255) a (Number),
(SELECT number FROM master..spt_values WHERE type = 'P' AND number <= 255) b (Number)) n --numbers
on n.n <= ndm.nd
)
select min(dt) as week, cast(sum(price)/count(*) as decimal(9,2)) as price
from x
group by wk
having count(*) = 7
order by wk
This gimes me the following
week price
2014-01-07 00:00:00.000 100.00
2014-01-14 00:00:00.000 100.00
2014-01-21 00:00:00.000 100.00
2014-02-04 00:00:00.000 200.00
2014-02-11 00:00:00.000 200.00
2014-02-18 00:00:00.000 200.00
Thanks
If you have a calendar table it's a simple join:
SELECT
product,
calendar_date - (day_of_week-1) AS week,
SUM(price/7) * COUNT(*)
FROM prices AS p
JOIN calendar AS c
ON c.calendar_date >= month
AND c.calendar_date < DATEADD(m,1,month)
GROUP BY product,
calendar_date - (day_of_week-1)
This could be further simplified to join only to mondays and then do some more date arithmetic in a CASE to get 7 or less days.
Edit:
Your last query returned jan 31st two times, you need to remove the =from on n.n < ndm.nd. And as you seem to work with ISO weeks you better change the DATEPART to avoid problems with different DATEFIRST settings.
Based on your last query I created a fiddle.
;with x as (
select price,
datepart(isowk,dateadd(day, n.n, t1.month)) wk,
dateadd(day, n.n-1, t1.month) dt
from
(select '2014-1-1' as Month, 100.00 as PRICE
union
select '2014-2-1' as Month, 200.00 as PRICE) t1
cross apply (
select datediff(day, t.month, dateadd(month, 1, t.month)) nd
from
(select '2014-1-1' as Month, 100.00 as PRICE
union
select '2014-2-1' as Month, 200.00 as PRICE)
t
where t1.month = t.month) ndm
inner join
(SELECT (a.Number * 256) + b.Number AS N FROM
(SELECT number FROM master..spt_values WHERE type = 'P' AND number <= 255) a (Number),
(SELECT number FROM master..spt_values WHERE type = 'P' AND number <= 255) b (Number)) n --numbers
on n.n < ndm.nd
) select min(dt) as week, cast(sum(price)/count(*) as decimal(9,2)) as price
from x
group by wk
having count(*) = 7
order by wk
Of course the dates might be from multiple years, so you need to GROUP BY by the year, too.
Actually, you need to spred it over days, and then get the averages by week. To get the days we'll use the Numbers table.
;with x as (
select product, price,
datepart(week,dateadd(day, n.n-2, t1.month)) wk,
dateadd(day, n.n-1, t1.month) dt
from #t t1
cross apply (
select datediff(day, t.month, dateadd(month, 1, t.month)) nd
from #t t
where t1.month = t.month and t1.product = t.product) ndm
inner join numbers n on n.n <= ndm.nd
)
select product, min(dt) as week, cast(sum(price)/count(*) as decimal(9,2)) as price
from x
group by product, wk
having count(*) = 7
order by product, wk
The result of datepart(week,dateadd(day, n.n-2, t1.month)) expression depends on SET DATEFIRST so you might need to adjust accordingly.

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