I have table of products and their sales quantity in months.
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-05-01 5
B 2018-08-01 10
B 2018-10-01 12
...
I'd like to first fill in the data gap between each product's min and max dates like below:
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-03-01 0
A 2018-04-01 0
A 2018-05-01 5
B 2018-08-01 10
B 2018-09-01 0
B 2018-10-01 12
...
Then I would need to perform an accumulation of each product's sales quantity by month.
Product Month total_Qty
A 2018-01-01 5
A 2018-02-01 8
A 2018-03-01 8
A 2018-04-01 8
A 2018-05-01 13
B 2018-08-01 10
B 2018-09-01 10
B 2018-10-01 22
...
I fumbled over the "cross join" clause, however it seems to generate some unexpected results for me. Could someone help to give a hint how I can achieve this in SQL?
Thanks a lot in advance.
I think a recursive CTE is a simple way to do this. The code is just:
with cte as (
select product, min(mon) as mon, max(mon) as end_mon
from t
group by product
union all
select product, dateadd(month, 1, mon), end_mon
from cte
where mon < end_mon
)
select cte.product, cte.mon, coalesce(qty, 0) as qty
from cte left join
t
on t.product = cte.product and t.mon = cte.mon;
Here is a db<>fiddle.
Hi i think this example can help you and perform what you excepted :
CREATE TABLE #MyTable
(Product varchar(10),
ProductMonth DATETIME,
Qty int
);
GO
CREATE TABLE #MyTableTempDate
(
FullMonth DATETIME
);
GO
INSERT INTO #MyTable
SELECT 'A', '2019-01-01', 214
UNION
SELECT 'A', '2019-02-01', 4
UNION
SELECT 'A', '2019-03-01', 50
UNION
SELECT 'B', '2019-01-01', 214
UNION
SELECT 'B', '2019-02-01', 10
UNION
SELECT 'C', '2019-04-01', 150
INSERT INTO #MyTableTempDate
SELECT '2019-01-01'
UNION
SELECT '2019-02-01'
UNION
SELECT '2019-03-01'
UNION
SELECT '2019-04-01'
UNION
SELECT '2019-05-01'
UNION
SELECT '2019-06-01'
UNION
SELECT '2019-07-01';
------------- FOR NEWER SQL SERVER VERSION > 2005
WITH MyCTE AS
(
SELECT T.Product, T.ProductMonth AS 'MMonth', T.Qty
FROM #MyTable T
UNION
SELECT T.Product, TD.FullMonth AS 'MMonth', 0 AS 'Qty'
FROM #MyTable T, #MyTableTempDate TD
WHERE NOT EXISTS (SELECT 1 FROM #MyTable TT WHERE TT.Product = T.Product AND TD.FullMonth = TT.ProductMonth)
)
-- SELECT * FROM MyCTE;
SELECT Product, MMonth, Qty, SUM( Qty) OVER(PARTITION BY Product ORDER BY Product
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as 'TotalQty'
FROM MyCTE
ORDER BY Product, MMonth ASC;
DROP TABLE #MyTable
DROP TABLE #MyTableTempDate
I have other way to perform this in lower SQL Server Version (like 2005 and lower)
It's a SELECT on SELECT if it's your case let me know and i provide some other example.
You can create the months with a recursive CTE
DECLARE #MyTable TABLE
(
ProductID CHAR(1),
Date DATE,
Amount INT
)
INSERT INTO #MyTable
VALUES
('A','2018-01-01', 5),
('A','2018-02-01', 3),
('A','2018-05-01', 5),
('B','2018-08-01', 10),
('B','2018-10-01', 12)
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SELECT #StartDate = MIN(Date), #EndDate = MAX(Date) FROM #MyTable
;WITH dates AS (
SELECT #StartDate AS Date
UNION ALL
SELECT DATEADD(Month, 1, Date)
FROM dates
WHERE Date < #EndDate
)
SELECT A.ProductID, d.Date, COALESCE(Amount,0) AS Amount, COALESCE(SUM(Amount) OVER(PARTITION BY A.ProductID ORDER BY A.ProductID, d.Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),0) AS Total
FROM
(
SELECT ProductID, MIN(date) as DateStart, MAX(date) as DateEnd
FROM #MyTable
GROUP BY ProductID -- As I read in your comments that you need different min and max dates per product
) A
JOIN dates d ON d.Date >= A.DateStart AND d.Date <= A.DateEnd
LEFT JOIN #MyTable T ON A.ProductID = T.ProductID AND T.Date = d.Date
ORDER BY A.ProductID, d.Date
Try this below
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
;WITH CTE(Product,[Month],Qty)
AS
(
SELECT 'A','2018-01-01', 5 UNION ALL
SELECT 'A','2018-02-01', 3 UNION ALL
SELECT 'A','2018-05-01', 5 UNION ALL
SELECT 'B','2018-08-01', 10 UNION ALL
SELECT 'D','2018-10-01', 12
)
SELECT ct.Product,[MonthDays],ct.Qty
INTO #Temp
FROM
(
SELECT c.Product,[Month],
ISNULL(Qty,0) AS Qty
FROM CTE c
)ct
RIGHT JOIN
(
SELECT -- This code is to get month data
CONVERT(VARCHAR(10),'2018-'+ RIGHT('00'+CAST(MONTH(DATEADD(MM, s.number, CONVERT(DATETIME, 0)))AS VARCHAR),2) +'-01',120) AS [MonthDays]
FROM master.dbo.spt_values s
WHERE [type] = 'P' AND s.number BETWEEN 0 AND 11
)DT
ON dt.[MonthDays] = ct.[Month]
SELECT
MAX(Product)OVER(ORDER BY [MonthDays])AS Product,
[MonthDays],
ISNULL(Qty,0) Qty,
SUM(ISNULL(Qty,0))OVER(ORDER BY [MonthDays]) As SumQty
FROM #Temp
Result
Product MonthDays Qty SumQty
------------------------------
A 2018-01-01 5 5
A 2018-02-01 3 8
A 2018-03-01 0 8
A 2018-04-01 0 8
A 2018-05-01 5 13
A 2018-06-01 0 13
A 2018-07-01 0 13
B 2018-08-01 10 23
B 2018-09-01 0 23
D 2018-10-01 12 35
D 2018-11-01 0 35
D 2018-12-01 0 35
First of all, i would divide month and year to get easier with statistics.
I will give you an example query, not based on your table but still helpful.
--here i create the table that will be used as calendar
Create Table MA_MonthYears (
Month int not null ,
year int not null
PRIMARY KEY ( month, year) )
--/////////////////
-- here i'm creating a procedure to fill the ma_monthyears table
declare #month as int
declare #year as int
set #month = 1
set #year = 2015
while ( #year != 2099 )
begin
insert into MA_MonthYears(Month, year)
select #month, #year
if #month < 12
set #month=#month+1
else
set #month=1
if #month = 1
set #year = #year + 1
end
--/////////////////
--here you are the possible result you are looking for
select SUM(Ma_saledocdetail.taxableamount) as Sold, MA_MonthYears.month , MA_MonthYears.year , item
from MA_MonthYears left outer join MA_SaleDocDetail on year(MA_SaleDocDetail.DocumentDate) = MA_MonthYears.year
and Month(ma_saledocdetail.documentdate) = MA_MonthYears.Month
group by MA_SaleDocDetail.Item, MA_MonthYears.year , MA_MonthYears.month
order by MA_MonthYears.year , MA_MonthYears.month
Related
Have a table with this
Id
StartDate
NoOfMonths
1
2021-09-01
2
2
2021-09-01
3
And want a query to return this
Id
Date
1
2021-09-01
1
2021-10-01
2
2021-09-01
2
2021-10-01
2
2021-11-01
How can I make this happen?
Here is an example without an additional table:
DECLARE #t TABLE(
ID int
, StartDate date
, NoOfMonths int
)
INSERT INTO #t VALUES
(1, '2021-09-01', 2)
,(2, '2021-09-01', 3);
WITH cte AS(
SELECT ID, StartDate, NoOfMonths
FROM #t
UNION ALL
SELECT ID, DATEADD(MONTH, 1, StartDate), NoOfMonths-1
FROM cte
WHERE NoOfMonths > 1
)
SELECT ID, StartDate
FROM cte
ORDER BY ID, StartDate
This could be solved by having an additional calendar table, which would be populated and maintained by you. The content of the table could be just dates (first days of months). Then you would join records from that calendar table with your original table using DATEADD() function if it's MS SQL server. So something like:
select DateMonth
from CalendarTable ct
inner join YourTable yt
on ct.DateMonth between yt.StartDate and DATEADD (MONTH, yt.NoOfMonths, yt.StartDate)
My data looks something like this
ProductNumber | YearMonth | Number
1 201803 1
1 201804 3
1 201810 6
2 201807 -3
2 201809 5
Now what I want to have is add an additional entry "6MSum" which is the sum of the last 6 months per ProductNumber (not the last 6 entries).
Please be aware the YearMonth data is not complete, for every ProductNumber there are gaps in between so I cant just use the last 6 entries for the sum. The final result should look something like this.
ProductNumber | YearMonth | Number | 6MSum
1 201803 1 1
1 201804 3 4
1 201810 6 9
2 201807 -3 -3
2 201809 5 2
Additionally I don't want to insert the sum to the table but instead use it in a query like:
SELECT [ProductNumber],[YearMonth],[Number],
6MSum = CONVERT(INT,SUM...)
FROM ...
I found a lot off solutions that use a "sum over period" but only for the last X entries and not for the actual conditional statement of "YearMonth within last 6 months".
Any help would be much appreciated!
Its a SQL Database
EDIT/Answer
It seems to be the case that the gaps within the months have to be filled with data, afterwards something like
sum(Number) OVER (PARTITION BY category
ORDER BY year, week
ROWS 6 PRECEDING) AS 6MSum
Should work.
Reference to the solution : https://dba.stackexchange.com/questions/181773/sum-of-previous-n-number-of-columns-based-on-some-category
You could go the OUTER APPLY route. The following produces your required results exactly:
-- prep data
SELECT
ProductNumber , YearMonth , Number
into #t
FROM ( values
(1, 201803 , 1 ),
(1, 201804 , 3 ),
(1, 201810 , 6 ),
(2, 201807 , -3 ),
(2, 201809 , 5 )
) s (ProductNumber , YearMonth , Number)
-- output
SELECT
ProductNumber
,YearMonth
,Number
,[6MSum]
FROM #t t
outer apply (
SELECT
sum(number) as [6MSum]
FROM #t it
where
it.ProductNumber = t.ProductNumber
and it.yearmonth <= t.yearmonth
and t.yearmonth - it.yearmonth between 0 and 6
) tt
drop table #t
Use outer apply and convert yearmonth to a date, something like this:
with t as (
select t.*,
convert(date, convert(varchar(255), yearmonth) + '01')) as ymd
from yourtable t
)
select t.*, t2.sum_6m
from t outer apply
(select sum(t2.number) as sum_6m
from t t2
where t2.productnumber = t.productnumber and
t2.ymd <= t.ymd and
t2.ymd > dateadd(month, -6, ymd)
) t2;
Just to provide one more option. You can use DATEFROMPARTS to build valid dates from the YearMonth value and then search for values within date ranges.
Testable here: https://rextester.com/APJJ99843
SELECT
ProductNumber , YearMonth , Number
INTO #t
FROM ( values
(1, 201803 , 1 ),
(1, 201804 , 3 ),
(1, 201810 , 6 ),
(2, 201807 , -3 ),
(2, 201809 , 5 )
) s (ProductNumber , YearMonth , Number)
SELECT *
,[6MSum] = (SELECT SUM(number) FROM #t WHERE
ProductNumber = t.ProductNumber
AND DATEFROMPARTS(LEFT(YearMonth,4),RIGHT(YearMonth,2),1) --Build a valid start of month date
BETWEEN
DATEADD(MONTH,-6,DATEFROMPARTS(LEFT(t.YearMonth,4),RIGHT(t.YearMonth,2),1)) --Build a valid start of month date 6 months back
AND DATEFROMPARTS(LEFT(t.YearMonth,4),RIGHT(t.YearMonth,2),1)) --Build a valid end of month date
FROM #t t
DROP TABLE #t
So a working query (provided by a colleauge of mine) can look like this
SELECT [YearMonth]
,[Number]
,[ProductNumber]
, (Select Sum(Number) from [...] DPDS_1 where DPDS.ProductNumber =
DPDS_1.ProductNumber and DPDS_1.YearMonth <= DPDS.YearMonth and DPDS_1.YearMonth >=
convert (int, left (convert (varchar, dateadd(mm, -6, DPDS.YearMonth + '01'), 112),
6)))FROM [...] DPDS
I need to create a report and I am struggling with the SQL script.
The table I want to query is a company_status_history table which has entries like the following (the ones that I can't figure out)
Table company_status_history
Columns:
| id | company_id | status_id | effective_date |
Data:
| 1 | 10 | 1 | 2016-12-30 00:00:00.000 |
| 2 | 10 | 5 | 2017-02-04 00:00:00.000 |
| 3 | 11 | 5 | 2017-06-05 00:00:00.000 |
| 4 | 11 | 1 | 2018-04-30 00:00:00.000 |
I want to answer to the question "Get all companies that have been at least for some point in status 1 inside the time period 01/01/2017 - 31/12/2017"
Above are the cases that I don't know how to handle since I need to add some logic of type :
"If this row is status 1 and it's date is before the date range check the next row if it has a date inside the date range."
"If this row is status 1 and it's date is after the date range check the row before if it has a date inside the date range."
I think this can be handled as a gaps and islands problem. Consider the following input data: (same as sample data of OP plus two additional rows)
id company_id status_id effective_date
-------------------------------------------
1 10 1 2016-12-15
2 10 1 2016-12-30
3 10 5 2017-02-04
4 10 4 2017-02-08
5 11 5 2017-06-05
6 11 1 2018-04-30
You can use the following query:
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
ORDER BY company_id, effective_date
to get:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 0
2 10 1 2016-12-30 1
3 10 5 2017-02-04 2
4 10 4 2017-02-08 2
5 11 5 2017-06-05 0
6 11 1 2018-04-30 0
Now you can identify status = 1 islands using:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
)
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
Output:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 1
2 10 1 2016-12-30 1
3 10 5 2017-02-04 1
4 10 4 2017-02-08 2
5 11 5 2017-06-05 1
6 11 1 2018-04-30 2
Calculated field grp will help us identify those islands:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
), CTE2 AS
(
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
)
SELECT company_id,
MIN(effective_date) AS start_date,
CASE
WHEN COUNT(*) > 1 THEN DATEADD(DAY, -1, MAX(effective_date))
ELSE MIN(effective_date)
END AS end_date
FROM CTE2
GROUP BY company_id, grp
HAVING COUNT(CASE WHEN status_id = 1 THEN 1 END) > 0
Output:
company_id start_date end_date
-----------------------------------
10 2016-12-15 2017-02-03
11 2018-04-30 2018-04-30
All you want know is those records from above that overlap with the specified interval.
Demo here with somewhat more complicated use case.
Maybe this is what you are looking for? For these kind of questions, you need to join two instance of your table, in this case I am just joining with next record by Id, which probably is not totally correct. To do it better, you can create a new Id using a windowed function like row_number, ordering the table by your requirement criteria
If this row is status 1 and it's date is before the date range check
the next row if it has a date inside the date range
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
else NULL
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
Implementing second criteria:
"If this row is status 1 and it's date is after the date range check
the row before if it has a date inside the date range."
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
when csh1.status_id=1 and csh1.effective_date>#range_en
then
case
when csh3.effective_date between #range_st and #range_en then true
else false
end
else null -- ¿?
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
left join company_status_history csh3
on csh1.id=csh3.id-1
I would suggest the use of a cte and the window functions ROW_NUMBER. With this you can find the desired records. An example:
DECLARE #t TABLE(
id INT
,company_id INT
,status_id INT
,effective_date DATETIME
)
INSERT INTO #t VALUES
(1, 10, 1, '2016-12-30 00:00:00.000')
,(2, 10, 5, '2017-02-04 00:00:00.000')
,(3, 11, 5, '2017-06-05 00:00:00.000')
,(4, 11, 1, '2018-04-30 00:00:00.000')
DECLARE #StartDate DATETIME = '2017-01-01';
DECLARE #EndDate DATETIME = '2017-12-31';
WITH cte AS(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) AS rn
FROM #t
),
cteLeadLag AS(
SELECT c.*, ISNULL(c2.effective_date, c.effective_date) LagEffective, ISNULL(c3.effective_date, c.effective_date)LeadEffective
FROM cte c
LEFT JOIN cte c2 ON c2.company_id = c.company_id AND c2.rn = c.rn-1
LEFT JOIN cte c3 ON c3.company_id = c.company_id AND c3.rn = c.rn+1
)
SELECT 'Included' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Following' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date > #EndDate
AND LagEffective BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Trailing' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date < #EndDate
AND LeadEffective BETWEEN #StartDate AND #EndDate
I first select all records with their leading and lagging Dates and then I perform your checks on the inclusion in the desired timespan.
Try with this, self-explanatory. Responds to this part of your question:
I want to answer to the question "Get all companies that have been at
least for some point in status 1 inside the time period 01/01/2017 -
31/12/2017"
Case that you want to find those id's that have been in any moment in status 1 and have records in the period requested:
SELECT *
FROM company_status_history
WHERE id IN
( SELECT Id
FROM company_status_history
WHERE status_id=1 )
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'
Case that you want to find id's in status 1 and inside the period:
SELECT *
FROM company_status_history
WHERE status_id=1
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'
Code:
DECLARE #CurrentDate DATE = '2018-01-02' ; -- Can be any date. This is just a date I used as an anchor point.
DECLARE #BillingDayOfMonth INT = 31 ; -- The day of the billing every month (can be between 1st and 31st of the month).
;WITH [TT1] ( [N] ) AS
(
SELECT 1
UNION ALL
SELECT 1
)
, [TT2] ( [N] ) AS
(
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
)
, [TT] ( [N] ) AS
(
SELECT ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ) )
FROM [TT1] [T1]
CROSS JOIN [TT2] [T2]
)
SELECT [N]
FROM [TT] ; -- This gives me the count of 12 cycles (can vary).
Goal:
To return current and X number of previous billing cycle dates (12 cycles total in the above example).
If the BillingDayOfMonth doesn't exists in a month (i.e. 29th to 31st depending on the month), choose the last day of the month as the BillingDayOfMonth. So if BillingDayOfMonth = 31st, this day doesn't exist in Nov. So The Cycle will start #11/30 instead of 11/31. The above code is incomplete and need your help figuring out an efficient solution. Thank you
Desired Output:
CycleStartDate CycleEndDate
2017-12-31 2018-01-30 -- The most current cycle
2017-11-30 2017-12-30
2017-10-31 2017-11-29
2017-09-30 2017-10-30
2017-08-31 2017-09-29
2017-07-31 2017-08-30
2017-06-30 2017-07-30
2017-05-31 2017-06-29
2017-04-30 2017-05-30 -- Notice the EndDate
2017-03-31 2017-04-29 -- Notice the EndDate
2017-02-28 2017-03-30 -- Notice the StartDate
2017-01-31 2017-02-27 -- Notice the EndDate
So based on your numbers table approach and your rules, you can use a query like below.
I tested this for #BillingDayOfMonth values like 25, 28, 30 and found results reasonably true.
See live demo
DECLARE #CurrentDate DATE = '2018-01-02' ; -- Can be any date. This is just a date I used as an anchor point.
DECLARE #BillingDayOfMonth INT = 31 ; -- The day of the billing every month (can be between 1st and 31st of the month).
DECLARE #StartDate DATE
SET #StartDate= CASE WHEN
DAY(EOMONTH(#CurrentDate)) < #BillingDayOfMonth
THEN
EOMONTH(#CurrentDate)
ELSE
DATEFROMPARTS(YEAR(#CurrentDate),MONTH(#CurrentDate),#BillingDayOfMonth)
END
;WITH [TT1] ( [N] ) AS
(
SELECT 1
UNION ALL
SELECT 1
)
, [TT2] ( [N] ) AS
(
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
UNION ALL
SELECT 1
)
, [TT] ( [N] ) AS
(
SELECT
ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ) )
FROM [TT1] [T1]
CROSS JOIN [TT2] [T2]
)
SELECT CycleStartDate =
DATEADD(M,-N,#StartDate)
,
CycleEndDate =
DATEADD(D,-1,DATEADD(M,1-N,#StartDate))
FROM [TT] ;
I havea table as table_A . table_A includes these columns
-CountryName
-Min_Date
-Max_Date
-Number
I want to duplicate data with seperating by months. For example
Argentina | 2015-01-04 | 2015-04-07 | 100
England | 2015-02-08 | 2015-03-11 | 90
I want to see a table as this (Monthly seperated)
Argentina | 01-2015 | 27 //(days to end of the min_date's month)
Argentina | 02-2015 | 29 //(days full month)
Argentina | 03-2015 | 31 //(days full month)
Argentina | 04-2015 | 7 //(days from start of the max_date's month)
England | 02-2015 | 21 //(days)
England | 03-2015 | 11 //(days)
I tried too much thing to made this for each records. But now my brain is so confusing and my project is delaying.
Does anybody know how can i solve this. I tried to duplicate each rows with datediff count but it is not working
WITH cte AS (
SELECT CountryName, ISNULL(DATEDIFF(M,Min_Date ,Max_Date )+1,1) as count FROM table_A
UNION ALL
SELECT CountryName, count-1 FROM cte WHERE count>1
)
SELECT CountryName,count FROM cte
-Generate all the dates between min and max dates for each country.
-Then get the month start and month end dates for each country,year,month.
-Finally get the date differences of the month start and month end.
WITH cte AS (
SELECT Country, min_date dt,min_date,max_date FROM t
UNION ALL
SELECT Country, dateadd(dd,1,dt),min_date,max_date FROM cte WHERE dt < max_date
)
,monthends as (
SELECT country,year(dt) yr,month(dt) mth,max(dt) monthend,min(dt) monthstart
FROM cte
GROUP BY country,year(dt),month(dt))
select country
,cast(mth as varchar(2))+'-'+cast(yr as varchar(4)) yr_month
,datediff(dd,monthstart,monthend)+1 days_diff
from monthends
Sample Demo
EDIT: Another option would be to generate all the dates once (the example shown here generates 51 years of dates from 2000 to 2050) and then joining it to the table to get the days by month.
WITH cte AS (
SELECT cast('2000-01-01' as date) dt,cast('2050-12-31' as date) maxdt
UNION ALL
SELECT dateadd(dd,1,dt),maxdt FROM cte WHERE dt < maxdt
)
SELECT country,year(dt) yr,month(dt) mth, datediff(dd,min(dt),max(dt))+1 days_diff
FROM cte c
JOIN t on c.dt BETWEEN t.min_date and t.max_date
GROUP BY country,year(dt),month(dt)
OPTION (MAXRECURSION 0)
I think you have the right idea. But you need to construct the months:
WITH cte AS (
SELECT CountryName, Min_Date as dte, Min_Date, Max_Date
FROM table_A
UNION ALL
SELECT CountryName, DATEADD(month, 1, dte), Min_Date, Max_Date
FROM cte
WHERE dte < Max_date
)
SELECT CountryName, dte
FROM cte;
Getting the number of days in the month is a bit more complicated. That requires some thought.
Oh, I forgot about EOMONTH():
select countryName, dte,
(case when dte = min_date
then datediff(day, min_date, eomonth(dte)) + 1
when dte = max_date
then day(dte)
else day(eomonth(dte))
end) as days
from cte;
Using a Calendar Table makes this stuff pretty easy. RexTester: http://rextester.com/EBTIMG23993
begin
create table #enderaric (
CountryName varchar(16)
, Min_Date date
, Max_Date date
, Number int
)
insert into #enderaric values
('Argentina' ,'2015-01-04' ,'2015-04-07' ,'100')
, ('England' ,'2015-02-08' ,'2015-03-11' ,'90')
end;
-- select * from #enderaric
--*/"
declare #FromDate date;
declare #ThruDate date;
set #FromDate = '2015-01-01';
set #ThruDate = '2015-12-31';
with x as (
select top (cast(sqrt(datediff(day, #FromDate, #ThruDate)) as int) + 1)
[number]
from [master]..spt_values v
)
/* Date Range CTE */
,cal as (
select top (1+datediff(day, #FromDate, #ThruDate))
DateValue = convert(date,dateadd(day,
row_number() over (order by x.number)-1,#FromDate)
)
from x cross join x as y
order by DateValue
)
select
e.CountryName
, YearMonth = convert(char(7),left(convert(varchar(10),DateValue),7))
, [Days]=count(c.DateValue)
from #enderaric as e
inner join cal c on c.DateValue >= e.min_date
and c.DateValue <= e.max_date
group by
e.CountryName
, e.Min_Date
, e.Max_Date
, e.Number
, convert(char(7),left(convert(varchar(10),DateValue),7))
results in:
CountryName YearMonth Days
---------------- --------- -----------
Argentina 2015-01 28
Argentina 2015-02 28
Argentina 2015-03 31
Argentina 2015-04 7
England 2015-02 21
England 2015-03 11
More about calendar tables:
Aaron Bertrand - Generate a set or sequence without loops
generate-a-set-1
generate-a-set-2
generate-a-set-3
David Stein - Creating a Date Table/Dimension on SQL 2008
Michael Valentine Jones - F_TABLE_DATE