SQL split number over date range - sql

I have a table with, for example this data
ID |start_date |end_date |amount
---|------------|-----------|-------
a1 |2013-12-01 |2014-03-31 |100
Iwant to have a query that split the dates so I have the amount splitted out over the year like this :
ID |org_start_date |org_end_date |new_start_date |new_end_date |amount
---|----------------|---------------|----------------|----------------|-------
a1 |2013-12-01 |2014-03-31 |2013-12-01 |2013-12-31 |25
a1 |2013-12-01 |2014-03-31 |2014-01-01 |2014-03-31 |75
The 25 in 2013 is because 2013 has one month and 75 in 2014 because this has 3 months
Is there a way to do this in T-SQL?
Thx in advance!

Use spt_values table to create a calendar table, then join to your table to split date range into any part you want.
If split by year and divide amount by months you could:
with dates as
(
select number,DATEADD(day,number,'20130101') as dt
from master..spt_values
where number between 0 and 1000 AND TYPE='P'
)
select
m.start_date as org_start_date,
m.end_date as org_end_date,
min(d.dt) as new_start_date,
max(d.dt) as new_end_date,
m.amount*count(distinct month(d.dt))/(datediff(month,m.start_date,m.end_date)+1) as amount
from
MonthSplit m
join
dates d
on
d.dt between m.start_date and m.end_date
group by
m.start_date, m.end_date, year(d.dt),m.amount
Here is the SQL FIDDLE DEMO.

Here is a solution using a numbers table:
SQL Fiddle Example
DECLARE #STARTYR INT = (SELECT MIN(YEAR([Start Date])) FROM Table1)
DECLARE #ENDYR INT = (SELECT MAX(YEAR([End Date])) FROM Table1)
SELECT [Id]
, #STARTYR + Number AS [Year]
, CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END AS [Start]
, CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE [End Date] END AS [End]
, DATEDIFF(MONTH,CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END
,CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE DATEADD(MONTH,DATEDIFF(MONTH,0,DATEADD(MONTH,1,[End Date])),0) END) AS [Months]
, DATEDIFF(MONTH,[Start Date],[End Date]) + 1 [Total Months]
, ([Amount] / (DATEDIFF(MONTH,[Start Date],[End Date]) + 1))
*
DATEDIFF(MONTH,CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END
,CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE DATEADD(MONTH,DATEDIFF(MONTH,0,DATEADD(MONTH,1,[End Date])),0) END) AS [Proportion]
FROM Numbers
LEFT JOIN Table1 ON YEAR([Start Date]) <= #STARTYR + Number
AND YEAR([End Date]) >= #STARTYR + Number
WHERE Number <= #ENDYR - #STARTYR

I don't have a ready made SQL for you but just a thought to solve this problem. If you have some experience with SQL it won't be hard to express it in SQL.
You could do this by defining a reference table for the months that are between your begin and end date:
ID | month | year | month start | month end | count |
-----------------------------------------------------------------------------
1001 | dec-2013 | 2013 | 1-12-2013 | 31-12-2013 | 1 |
1001 | jan-2014 | 2014 | 1-1-2014 | 31-1-2014 | 1 |
1001 | feb-2014 | 2014 | 1-2-2014 | 28-2-2014 | 1 |
1001 | mar-2014 | 2014 | 1-3-2014 | 31-3-2014 | 1 |
Maybe you already have such a time ref table in your DWH.
When you join (with a between statement) your table with the record that contains the start and end date per row, with this reference table, you'll have the split from 1 row to the number of months contained in the range correct. The count column will help you to get the split ratio per year right by grouping over the year afterwards (like : 1/4 for 2013 and 3/4 for 2014). You'll have to apply the ratio to the field 'amount' that you want to split up.

It's very similar to this question:
Split date range into one row per month in sql server
Although you are doing grouping by year, so based on that answer you can modify it to do what you want by adding MIN, MAX to your date values and grouping by the YEAR():
SQL Fiddle Demo
Schema Setup:
CREATE TABLE MonthSplit
([ID] varchar(2), [start_date] datetime, [end_date] datetime, [amount] int)
;
INSERT INTO MonthSplit
([ID], [start_date], [end_date], [amount])
VALUES
('a1', '2013-12-01 00:00:00', '2014-03-31 00:00:00', 100),
('a2', '2013-10-01 00:00:00', '2015-05-01 00:00:00', 400)
;
Recursive CTE to group by Year:
WITH cte AS
(SELECT ID
, start_date
, end_date
, start_date AS from_date
, DATEADD(day, day(start_date)* -1 + 1, start_date) AS first_of_month
FROM MonthSplit
UNION ALL
SELECT ID
, start_date
, end_date
, DATEADD(month,1,first_of_month)
, DATEADD(month,1,first_of_month)
FROM cte
WHERE DATEADD(month,1,from_date) < end_date
)
SELECT ID as ID,
min(start_date) as org_start_date,
min(end_date) as org_end_date,
min(from_date) AS new_start_date,
CASE when max(end_date) < DATEADD(month,1,max(first_of_month)) THEN
max(end_date)
ELSE
DATEADD(day, -1, DATEADD(month,1,max(first_of_month)))
END AS new_end_date
FROM cte
group by year(from_date), ID
Results:
| ID | ORG_START_DATE | ORG_END_DATE | NEW_START_DATE | NEW_END_DATE |
|----|-------------------|----------------|-------------------|-------------------|
| a1 | December, 01 2013 | March, 31 2014 | December, 01 2013 | December, 31 2013 |
| a1 | December, 01 2013 | March, 31 2014 | January, 01 2014 | March, 31 2014 |
| a2 | October, 01 2013 | May, 01 2015 | October, 01 2013 | December, 31 2013 |
| a2 | October, 01 2013 | May, 01 2015 | January, 01 2014 | December, 31 2014 |
| a2 | October, 01 2013 | May, 01 2015 | January, 01 2015 | April, 30 2015 |

Related

Check if a month is skipped then add values dynamically?

I have a set of data from a table that would only be populated if a user has data for a certain month just like this:
Month | MonthName | Value
3 | March | 136.00
4 | April | 306.00
7 | July | 476.00
12 | December | 510.48
But what I need is to check if a month is skipped then adding the value the month before so the end result would be like this:
Month | MonthName | Value
3 | March | 136.00
4 | April | 306.00
5 | May | 306.00 -- added data
6 | June | 306.00 -- added data
7 | July | 476.00
8 | August | 476.00 -- added data
9 | September | 476.00 -- added data
10 | October | 476.00 -- added data
11 | November | 476.00 -- added data
12 | December | 510.48
How can I do this dynamically on SQL Server?
One method is a recursive CTE:
with cte as (
select month, value, lead(month) over (order by month) as next_month
from t
union all
select month + 1, value, next_month
from cte
where month + 1 < next_month
)
select month, datename(month, datefromparts(2020, month, 1)) as monthname, value
from cte
order by month;
Here is a db<>fiddle.
you can use spt_values to get continuous number 1-12, and then left join your table by max(month)
select t1.month
,datename(month,datefromparts(2020, t1.month, 1)) monthname
,t2.value
from (
select top 12 number + 1 as month from master..spt_values
where type = 'p'
) t1
left join t t2 on t2.month = (select max(month) from t tmp where tmp.month < = t1.month)
where t2.month is not null
CREATE TABLE T
([Month] int, [MonthName] varchar(8), [Value] numeric)
;
INSERT INTO T
([Month], [MonthName], [Value])
VALUES
(3, 'March', 136.00),
(4, 'April', 306.00),
(7, 'July', 476.00),
(12, 'December', 510.48)
;
Demo Link SQL Server 2012 | db<>fiddle
note
if you have year column then you need to fix the script.

How to return same row multiple times with multiple conditions

My knowledge is pretty basic so your help would be highly appreciated.
I'm trying to return the same row multiple times when it meets the condition (I only have access to select query).
I have a table of more than 500000 records with Customer ID, Start Date and End Date, where end date could be null.
I am trying to add a new column called Week_No and list all rows accordingly. For example if the date range is more than one week, then the row must be returned multiple times with corresponding week number. Also I would like to count overlapping days, which will never be more than 7 (week) per row and then count unavailable days using second table.
Sample data below
t1
ID | Start_Date | End_Date
000001 | 12/12/2017 | 03/01/2018
000002 | 13/01/2018 |
000003 | 02/01/2018 | 11/01/2018
...
t2
ID | Unavailable
000002 | 14/01/2018
000003 | 03/01/2018
000003 | 04/01/2018
000003 | 08/01/2018
...
I cannot pass the stage of adding week no. I have tried using CASE and UNION ALL but keep getting errors.
declare #week01start datetime = '2018-01-01 00:00:00'
declare #week01end datetime = '2018-01-07 00:00:00'
declare #week02start datetime = '2018-01-08 00:00:00'
declare #week02end datetime = '2018-01-14 00:00:00'
...
SELECT
ID,
'01' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week01end and End_Date >= #week01start)
or (Start_Date <= #week01end and End_Date is null)
UNION ALL
SELECT
ID,
'02' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week02end and End_Date >= #week02start)
or (Start_Date <= #week02end and End_Date is null)
...
The new table should look like this
ID | Week_No | Year | Start_Date | End_Date | Overlap | Unavail_Days
000001 | 01 | 2018 | 12/12/2017 | 03/01/2018 | 3 |
000002 | 02 | 2018 | 13/01/2018 | | 2 | 1
000003 | 01 | 2018 | 02/01/2018 | 11/01/2018 | 6 | 2
000003 | 02 | 2018 | 02/01/2018 | 11/01/2018 | 4 | 1
...
business wise i cannot understand what you are trying to achieve. You can use the following code though to calculate your overlapping days etc. I did it the way you asked, but i would recommend a separate table, like a Time dimension to produce a "cleaner" solution
/*sample data set in temp table*/
select '000001' as id, '2017-12-12'as start_dt, ' 2018-01-03' as end_dt into #tmp union
select '000002' as id, '2018-01-13 'as start_dt, null as end_dt union
select '000003' as id, '2018-01-02' as start_dt, '2018-01-11' as end_dt
/*calculate week numbers and week diff according to dates*/
select *,
DATEPART(WK,start_dt) as start_weekNumber,
DATEPART(WK,end_dt) as end_weekNumber,
case
when DATEPART(WK,end_dt) - DATEPART(WK,start_dt) > 0 then (DATEPART(WK,end_dt) - DATEPART(WK,start_dt)) +1
else (52 - DATEPART(WK,start_dt)) + DATEPART(WK,end_dt)
end as WeekDiff
into #tmp1
from
(
SELECT *,DATEADD(DAY, 2 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [start_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [startdt_Week_End_Date],
DATEADD(DAY, 2 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_End_Date]
from #tmp
) s
/*cte used to create duplicates when week diff is over 1*/
;with x as
(
SELECT TOP (10) rn = ROW_NUMBER() --modify the max you want
OVER (ORDER BY [object_id])
FROM sys.all_columns
ORDER BY [object_id]
)
/*final query*/
select --*
ID,
start_weekNumber+ (r-1) as Week,
DATEPART(YY,start_dt) as [YEAR],
start_dt,
end_dt,
null as Overlap,
null as unavailable_days
from
(
select *,
ROW_NUMBER() over (partition by id order by id) r
from
(
select d.* from x
CROSS JOIN #tmp1 AS d
WHERE x.rn <= d.WeekDiff
union all
select * from #tmp1
where WeekDiff is null
) a
)a_ext
order by id,start_weekNumber
--drop table #tmp1,#tmp
The above will produce the results you want except the overlap and unavailable columns. Instead of just counting weeks, i added the number of week in the year using start_dt, but you can change that if you don't like it:
ID Week YEAR start_dt end_dt Overlap unavailable_days
000001 50 2017 2017-12-12 2018-01-03 NULL NULL
000001 51 2017 2017-12-12 2018-01-03 NULL NULL
000001 52 2017 2017-12-12 2018-01-03 NULL NULL
000002 2 2018 2018-01-13 NULL NULL NULL
000003 1 2018 2018-01-02 2018-01-11 NULL NULL
000003 2 2018 2018-01-02 2018-01-11 NULL NULL

TSQL Check if Month and Year fields are expired

i have a table with Month and Year fields as integer, eg:
Month | Year
------------
10 | 17
------------
11 | 17
------------
12 | 17
------------
1 | 18
------------
(Year 17 is for 2017 and Year 18 is for 2018)
I want add into a query a calculated field for check if the date is expired
SELECT [Year], [Month],
CASE WHEN
([Year]+2000) < DATEPART(Year, GetDate()) OR
(([Year]+2000) = DATEPART(Year, GetDate()) AND [Month] < DATEPART(Month, GetDate()))
THEN 1 ELSE 0 END AS IsExpired
FROM test
the output is
Month | Year | IsExpired
------------------------
10 | 17 | 1
------------------------
11 | 17 | 1
------------------------
12 | 17 | 1
------------------------
1 | 18 | 1
------------------------
the expected output is (because current GetDate() is 2017-11-29):
Month | Year | IsExpired
------------------------
10 | 17 | 1
------------------------
11 | 17 | 0
------------------------
12 | 17 | 0
------------------------
1 | 18 | 0
------------------------
see live on http://sqlfiddle.com/#!6/8c807/2
what i'm doing wrong?
Convert your values to dates:
WITH IntDates AS (
SELECT *
FROM (VALUES (10,17),(11,17),(12,17),(1,18)) AS D ([Month], [Year])),
Dates AS(
SELECT *,
DATEADD(YEAR, [Year], DATEADD(MONTH, [Month], '20000101')) AS DateValue
FROM IntDates)
SELECT *,
CASE WHEN DateValue < GETDATE() THEN 1 ELSE 0 END AS Expired
FROM Dates;
If you were using the date datatype this becomes a lot simpler.
create table test2
(
ExpirationDate date
)
--have to do a bunch of string manipulation to turn this into viable dates.
--and this of course is after switching the columns posted in your sql fiddle.
insert test2
select convert(char(4), [Year] + 2000) + right('0' + convert(varchar(2), [Month]), 2) + '01'
from Test
select case when ExpirationDate < dateadd(month, datediff(month, 0, getdate()), 0) --get beginning of the current month
then 1 else 0 end
, ExpirationDate
from test2

SQL Calculations for budgeting

I have a database that contains the following columns:
Vendor, Amount, StartDate, Months
I would like to be able to calculate the average monthly amount based on the Months that are entered. I would also like to see it calculate out from the start date to the end date based on the StartDate + Months calculation. The resulting table would look something like this:
Vendor1 has 2 months of 1112 starting Jan 1 while Vendor2 has 3 months of 2040 staring Feb 1
| | ANNUAL | JAN | FEB | MAR | APR |
Vendor1 | 2,224 | 1,112 | 1,112 | | |
Vendor2 | 6,120 | | 2,040 | 2,040 | 2,040 |
Any assistance or direction would be greatly appreciated.
That's a strange DB design. However, here's what you've got to try:
SELECT (Amount * Months) AS Annual, (Case #(StartDate < DATE("01.02.year")) WHEN 1 THEN Amount ELSE NULL) AS Jan FROM Table --etc for all months
Will think of modifications though, because this way is a little too straightforward.
You would use conditional aggregation. Assuming the start dates are all in the same year, the code might look like this:
select vendorid, (amount * months) as total,
(case when month(startdate) <= 1 and month(startdate) + months >= 1
then amount
end) as jan,
(case when month(startdate) <= 2 and month(startdate) + months >= 2
then amount
end) as feb,
(case when month(startdate) <= 3 and month(startdate) + months >= 3
then amount
end) as mar,
(case when month(startdate) <= 4 and month(startdate) + months >= 4
then amount
end) as apr,
from t;

Count distinct customer based on last three month sales

I need to get monthly count of distinct customers based on last three month Sales.
To show the result by adding current month customer count and adding last three month customer to the count as below:
In month of APRIL ,distinct customers count of (APRIL+MARCH+FEBRUARY)
In month of MAY,distinct customers count of (MAY+APRIL+MARCH)
In month of JUNE,distinct customers count of (JUNE+MAY+APRIL)
In month of JULY,distinct customers count of (JULY+JUNE+MAY)
Here what I tried:
SELECT MonNumber = MONTH(h.Invoicedate) ,
YearNumber = YEAR(h.Invoicedate) ,
PartyCount = ( SELECT COUNT(DISTINCT s.CustomerID)
FROM salesdata s
WHERE s.Invoicedate BETWEEN DATEADD(month, -6,
h.Invoicedate)
AND h.Invoicedate
)
FROM salesdata h
GROUP BY MONTH(h.Invoicedate) ,
YEAR(h.Invoicedate)
ORDER BY YEAR(h.Invoicedate) ,
MONTH(h.Invoicedate)
| Year | Month | COUNT |
|-----------|----------|-------------|
| 2014 | Jan | 6 |
| 2014 | Feb | 6 |
| 2014 | Mar | 6 |
| 2014 | Apr | 4 |
| 2014 | May | 6 |
| 2014 | Jun | 6 |
View the table on SQL Fiddle
Here it is.
WITH dt AS (
-- set invoice to BOM to get one row per month
SELECT DISTINCT DATEADD(mm,DATEDIFF(mm,0,InvoiceDate),0) AS InvoiceDate
FROM salesdata
)
SELECT YEAR(dt.InvoiceDate) AS YEAR
, MONTH(dt.InvoiceDate) AS MONTH
, COUNT(DISTINCT CustomerId) AS PARTYCOUNT
FROM salesdata s
INNER JOIN dt
-- Define your window
ON s.InvoiceDate >= DATEADD(MM, -2, dt.InvoiceDate)
AND s.InvoiceDate < DATEADD(MM, 1, dt.InvoiceDate)
GROUP BY YEAR(dt.InvoiceDate)
, MONTH(dt.InvoiceDate)
ORDER BY 1, 2
You need to apply some aggregate function to invoicedate.
This should work: fiddle
SELECT
-- get the first of the current month and substract two months
dateadd(month, -2, DATEADD(day, -day(h.Invoicedate) + 1, h.Invoicedate)) as first_of_month,
PartyCount = ( SELECT COUNT(DISTINCT s.CustomerID)
FROM salesdata s
WHERE s.Invoicedate >= dateadd(month, -2, DATEADD(day, -day(h.Invoicedate) + 1, h.Invoicedate))
AND s.Invoicedate < min(dateadd(month, 1, DATEADD(day, -day(h.Invoicedate) + 1, h.Invoicedate)))
)
FROM salesdata h
group by
dateadd(month, -2, DATEADD(day, -day(h.Invoicedate) + 1, h.Invoicedate))
order by 1
I would prefer to create a table with the month names and date ranges first and then simply use this instead.