Adding Missing Month in T-SQL - sql

I have a data something like this:
LoanId PaymentDate PaymentMonth PaymentAmount
L1 12-01-2008 01 100
L2 15-02-2008 02 300
L3 01-04-2008 04 500
L3 01-10-2008 10 500
I want to add missing PaymentMonth's for each loanId's, like this:
LoanId PaymentYear PaymentMonth PaymentAmount
L1 2008 01 100
L1 2008 02 0
L1 2008 03 0
.. .. .. ..
L1 2008 12 0
L2 2008 01 0
L2 2008 02 300
L2 2008 03 0
.. .. .. ..
L3 2008 01 0
L3 2008 02 0
L3 2008 03 0
L3 2008 04 500
.. .. .. ..
L3 2008 10 500
.. .. .. ..
L3 2008 12 0
Was doing it manually, but now got more than 100k LoanId's from 2008-20012

Try to do this:
use db_test;
go
create table dbo.test1
(
loanId varchar(2),
paymentDate date,
paymentMonth varchar(2),
paymentAmount float
);
set dateformat dmy;
insert into dbo.test1
values
('L1', '12-01-2008', '01', 100),
('L2', '15-02-2008', '02', 300),
('L3', '01-04-2008', '04', 500),
('L3', '01-10-2008', '10', 500);
set dateformat ymd;
with cte as (
select cast('2008-01-31' as date) as month_dt, 1 as month_nm, format(1, 'd2') as paymentMonth
union all
select eomonth(dateadd(month, 1, month_dt)), month_nm + 1, format(month(month_dt) % 12 + 1, 'd2')
from cte
where month_dt < '2012-12-31'
), cte2 as (
select
t.loanId,
x.month_dt,
x.paymentMonth
from (
select distinct loanId from dbo.test1
) t
join cte x
on 1 = 1
)
select
a.loanId, year(a.month_dt) as paymentYear, a.paymentMonth, coalesce(b.sm, 0) as paymentAmount
from
cte2 a
left join (
select loanId, eomonth(paymentDate) as paymentDate, paymentMonth, sum(paymentAmount) as sm
from dbo.test1
group by loanId, eomonth(paymentDate), paymentMonth
) b
on a.month_dt = b.paymentDate
and a.loanId = b.loanId
order by
paymentYear asc,
loanId asc,
paymentMonth;

you could try like:
1.) Getting your MIN & MAX PaymentDate (as i asume those are the your ranges)
2.) Creating all months within this range - in my example with a common table expression).
3.) Finally selecting your data and joining with those month-dates and grouping the result
DECLARE #StartDate DATETIME,
#EndDate DATETIME;
SET #StartDate = SELECT MIN(PaymentDate)
FROM yourtable
SET #EndDate = SELECT MAX(PaymentDate)
FROM yourtable
;WITH CTE AS (
SELECT DATEADD(MONTH, x.number, #StartDate) as Months
FROM master.dbo.spt_values x
WHERE x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate)
)
SELECT yourtable.LoanID
,yourtable.PaymentYear
,yourtable.PaymentMonth
,SUM(ISNULL(PaymentAmount,0)) as PaymentAmount
FROM CTE
INNER JOIN yourtable
ON yourtable.PaymentYear = CONVERT(VARCHAR(4),DATEPART(YEAR, Months))
AND yourtable.PaymentMonth = RIGHT('0' + CONVERT(VARCHAR(2),DATEPART(MONTH, Months)),2)
GROUP BY yourtable.LoanID
,yourtable.PaymentYear
,yourtable.PaymentMonth

This is one way of doing it, pretty straightforward. Necessary comments are in the code.
declare #LoanData table (
ID char(2),
PaymentDate date,
PaymentAmount int
)
insert into #LoanData values
('L1', '01-12-2008',100),
('L2', '02-15-2008',300),
('L3', '04-01-2008',500),
('L3', '10-01-2008',500)
declare #TableID table(id char(2))
--list of IDs
insert into #TableID select distinct ID from #LoanData
declare #PaymentMonth table(
LoanID char(2),
PaymentYear int,
PaymentMonth int,
PaymentAmount int
)
declare #month int, #year int, #i int, #id char(2)
select #i = count(*) from #TableID
--first get the table which has recotrd for every month for every id (default value in PaymentAmount is 0)
while #i > 0
begin
select top 1 #id = id from #TableID
set #year=2008
while #year <= 2012
begin
set #month=1
while #month <= 12
begin
insert into #PaymentMonth values (#id, #year, #month, 0)
set #month = #month + 1
end
set #year = #year + 1
end
delete from #TableID where id = #id
set #i = #i - 1
end
--update table based on your initial data
update #PaymentMonth
set PaymentAmount = A.PaymentAmount from #LoanData as A
where LoanID = A.ID and PaymentYear = datepart(YEAR, A.PaymentDate) and PaymentMonth = datepart(MONTH, A.PaymentDate)
select * from #PaymentMonth

create table temp_loantable (
loanid bigint,
paymentdate date,
paymentmonth varchar(2),
paymentamount numeric(10,2))
Having a date range ( year & month) and then doing a left outer join should get the desired output.
select * from
(select years.n yearval , months.n monthval
from (values(2008), (2009), (2010), (2011), (2012)) years(n),
(values(1),(2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12)) months(n)) a
left outer join temp_loantable l
on year(paymentdate) = a.yearval
and month(paymentdate) = a.monthval

Related

Return 0 with dates having empty results [duplicate]

I want to show all dates between two dates when there is any date data missing then its should show zero in val column .
declare #temp table (
id int identity(1,1) not null,
CDate smalldatetime ,
val int
)
INSERT STATEMENT FOR DATA TO CHECK
insert into #temp select '10/2/2012',1
insert into #temp select '10/3/2012',1
insert into #temp select '10/5/2012',1
insert into #temp select '10/7/2012',2
insert into #temp select '10/9/2012',2
insert into #temp select '10/10/2012',2
insert into #temp select '10/13/2012',2
insert into #temp select '10/15/2012',2
Retrieve records between first day of month and today
select * from #temp where CDate between '10/01/2012' AND '10/15/2012'
As i run this query its show me all data between these two dates but i want to also include missing dates with val=0
SQL FIDDLE WITH SAMPLE DATA
;with d(date) as (
select cast('10/01/2012' as datetime)
union all
select date+1
from d
where date < '10/15/2012'
)
select t.ID, d.date CDate, isnull(t.val, 0) val
from d
left join temp t
on t.CDate = d.date
order by d.date
OPTION (MAXRECURSION 0) -- use this if your dates are >99 days apart
You need to make up the dates, so I've use a recursive common table expression here.
SQL Fiddle
MAXRECURSION number
Specifies the maximum number of recursions allowed for this query. number is a nonnegative
integer between 0 and 32767. When 0 is specified, no limit is applied. If this option is
not specified, the default limit for the server is 100.
When the specified or default number for MAXRECURSION limit is reached during query
execution, the query is ended and an error is returned.
This will work as long as there are less than 2047 days between from and to dates
declare #from smalldatetime = '10/01/2012'
declare #to smalldatetime = '10/15/2012'
select t.id, dateadd(day, number,#from), isnull(val, 0) val from #temp t
right join master..spt_values s
on dateadd(d, s.number, #from) = t.CDate
where
datediff(day, #from, #to ) > s.number
and s.type = 'P'
I think the best way to do this is to create your own table with dates (you can also use master.dbo.spt_values, but I personally don't like that solution)
declare #Temp_Dates table (CDate datetime)
declare #Date datetime
select #Date = (select min(CDate) from temp)
while #Date <= (select max(CDate) from temp)
begin
insert into #Temp_Dates (CDate)
select #Date
select #Date = dateadd(dd, 1, #Date)
end
select D.CDate, isnull(T.id, 0) as id
from #Temp_Dates as D
left outer join temp as T on T.CDate = D.CDate
you can also use recursive solution with CTE
DECLARE #min DATETIME,
#max DATETIME,
#val INT
SELECT #min = Min(CDATE),
#max = Max(CDATE)
FROM TEMP
DECLARE #temp TABLE
(
CDATE SMALLDATETIME,
VAL INT
)
WHILE #min < #max
BEGIN
SELECT #val = VAL
FROM TEMP
WHERE CDATE = #min
INSERT #temp
VALUES (#min,
#val)
SET #min = Dateadd(D, 1, #min)
SET #val = 0
END
SELECT *
FROM #temp
Declare #temp Table(id int identity(1,1) not null,CDate smalldatetime ,val int)
insert into #temp select '10/2/2012',1
insert into #temp select '10/3/2012',1
insert into #temp select '10/5/2012',1
insert into #temp select '10/7/2012',2
insert into #temp select '10/9/2012',2
insert into #temp select '10/10/2012',2
insert into #temp select '10/13/2012',2
insert into #temp select '10/15/2012',2
DECLARE #startDate DATE= '10/01/2012'
DECLARE #endDate DATE= '10/15/2012'
SELECT t.Id, X.[Date],Val = COALESCE(t.val,0)
FROM
(SELECT [Date] = DATEADD(Day,Number,#startDate)
FROM master..spt_values
WHERE Type='P'
AND DATEADD(day,Number,#startDate) <= #endDate)X
LEFT JOIN #temp t
ON X.[Date] = t.CDate
using a recursive cte with min and max
declare #T table (id int identity(1,1) primary key, dt date not null, val int not null);
insert into #T (dt, val) values
('10/2/2012',1)
, ('10/3/2012',1)
, ('10/5/2012',1)
, ('10/7/2012',2)
, ('10/9/2012',2)
, ('10/10/2012',2)
, ('10/13/2012',2)
, ('10/15/2012',2);
--select * from #T;
with cte as
( select min(dt) as dt, max(dt) as mx
from #T
union all
select dateadd(dd, 1, dt), mx
from CTE
where dt < mx
)
select c.dt, isnull(t.val, 0) as val
from cte c
left join #T t
on c.dt = t.dt
order by c.dt
option (maxrecursion 0);
dt val
---------- -----------
2012-10-02 1
2012-10-03 1
2012-10-04 0
2012-10-05 1
2012-10-06 0
2012-10-07 2
2012-10-08 0
2012-10-09 2
2012-10-10 2
2012-10-11 0
2012-10-12 0
2012-10-13 2
2012-10-14 0
2012-10-15 2

DateDiff for each month

My current query give my this as a result;
Address PK StartDate EndDate Rent Cost NoDays
1 water lane 3435 01/04/2018 12/02/2020 500 11210.95 682
7 get road 5456 14/06/2019 01/02/2020 700 5339.18 232
I want to outline how many days per month/ or how much per month spent.
this is what i want to see after NoDays or even on a new query result.
04/2018 05/2018 06/2018 07/2018 so on ....
30 31 30 31 so on ....
0 0 0 0 so on ....
or
04/2018 05/2018 06/2018 07/2018 so on ....
500 500 500 500 so on ....
0 0 0 0 so on ....
Here a solution which generates a calendar using recursion
then adding columns and updating them using cursor and dynamic script
set dateformat dmy
declare #table as table(pk int, startdate date,enddate date,rent int,cost float)
insert into #table values(3435,'01/04/2018','12/02/2020',500,11210.95),(5456,'14/06/2019','01/02/2020',700,5339.18)
declare #table2 as table(pk int)
insert into #table2 select distinct(pk) from #table
declare #calendar as table (date date)
declare #mindate as date
declare #maxdate as date
select #mindate=min(startdate) from #table
select #maxdate=max(enddate) from #table;
with cte as(select #mindate as mydate union all select dateadd(day,1,mydate) from cte
where mydate < #maxdate)
insert into #calendar select * from cte
option(maxrecursion 0);
declare #tabresultsrows as table(pk int,MO varchar(7),N int,M int,Y int);
declare #tabmonths as table(Mo varchar(7),M int,Y int);
with cal as(
select t2.pk,c.date ,t.startdate,t.enddate ,month(date) M, year(date) y ,concat(RIGHT('00' + CONVERT(NVARCHAR(2), month(date)), 2),'/', year(date)) Mo,
case when c.date >= t.startdate and c.date <=t.enddate then 1 else 0 end N from #calendar c
cross join #table2 t2
inner join #table t on t2.pk=t.pk),
caltab as(select pk,Mo,sum(N) N ,Y,M from cal group by pk,Y,M,Mo )
insert into #tabresultsrows select pk,MO,N,M,Y from caltab order by pk,Y,M
insert into #tabmonths select distinct(MO),M,Y from #tabresultsrows
IF OBJECT_ID('tempdb..#tabresultscolumns') IS NOT NULL DROP TABLE #tabresultscolumns
select * into #tabresultscolumns from #table
declare #script as varchar(max)
declare mycursor cursor for select mo from #tabmonths order by Y,M
declare #mo as varchar(7)
open mycursor
fetch mycursor into #mo
while ##fetch_status=0
begin
set #script='alter table #tabresultscolumns add ['+#mo+'] int'
print #script
exec(#script)
fetch mycursor into #mo
end
close mycursor
deallocate mycursor
declare secondcursor cursor for select pk,Mo,N from #tabresultsrows
declare #PK AS INT
declare #n as int
open secondcursor
fetch secondcursor into #pk,#mo,#n
while ##fetch_status=0
begin
set #script=concat('update #tabresultscolumns set ['+#mo+']=',#n,' where pk=',#pk )
print #script
exec(#script)
fetch secondcursor into #pk,#mo,#n
end
close secondcursor
deallocate secondcursor
select * from #tabresultscolumns
Try something like this:
-- You can set the variables to get as an input the StartDate, EndDate from your table
DECLARE #StartDate DATE = '20200101'
, #EndDate DATE = '20200331'
;with datecreator as (
SELECT DATEADD(DAY, nbr - 1, #StartDate) as dates
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.PK ) AS Nbr
FROM Test c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
)
,CTE AS
(
select distinct Month(dates) rnk,Convert(char(3), dates, 0) MM from datecreator
)
,CTE3 AS
(
SELECT T.* , rnk
FROM Test T INNER JOIN CTE c ON C.Mm = T.Months
)
,CTE4 AS
(
SELECT Years,[1] Jan ,[2] Feb ,[3] Mar FROM CTE3
PIVOT
(SUM(Rent) FOR rnk IN ([1],[2],[3])) p
)
SELECT Years , SUM(Jan) Jan , SUM(Feb) Feb , SUM(Mar) Mar FROM CTE4 GROUP BY Years
You'll get the result :
Year1 Jan Feb Mar
2013 3000 3000 3000
2014 3500 3500 3500
As you can see above I used only the first Quarter (First 3 Months) but you can use the whole year.
Please let me know if you have any questions/Feedbacks :)

Convert Procedural Approach into Set Based Approach in Sql-Server

We are using procedural approach (while loop) for inserting records into a particular table. the insert syntax is like below,
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WHILE (#CNT <7)
BEGIN
SET #WEEK = DATEADD(D,#CNT,#WEEK )
IF EXISTS
(SELECT 1
FROM YEARRANGE
WHERE #WEEK BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
)
BEGIN
SET #FLAG =1
END
ELSE
BEGIN
SET #FLAG = 0
END
INSERT INTO #Tmpdata
(
officeId,id,weekDate,startsOn,endsOn,flag
)
VALUES
(
5134,#lvCounter,#week,'09:00 AM','05:00 PM',#flag
);
SET #cnt=#cnt+1;
end
(NOTE : TaxSeason is from january to august).
Is it possible to re-write the above logic in set based approach?
This is making a number of assumption because you didn't post ddl or any consumable sample data. Also, there is a variable #lvCounter not defined in your code. This is perfect opportunity to use a tally or numbers table instead of a loop.
declare #lvCounter int = 42;
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n))
, cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E1
)
select 5134 as officeId
, #lvCounter as Id
, DATEADD(DAY, N - 1, #WEEK) as weekDate
, '09:00 AM' as startsOn
, '05:00 PM' as EndOn
, Flag
from cteTally t
cross apply
(
select CAST(count(*) as bit) as Flag
from YearRange
where DATEADD(Day, t.N, #WEEK) > CONVERT(DATE,taxseasonbegin)
AND DATEADD(Day, t.N, #WEEK) <= CONVERT (DATE,taxSeasonEnd)
) y
where t.N <= 7;
Please can you provide sample data?
You can do something like:
SELECT DateIncrement = SUM(DATEADD(D,#CNT,#WEEK)) OVER (ORDER BY officeID)
FROM...
This gets an incremented date value for each record which you can then check against your start and end dates.
You could try some Kind of this one. This gives you the data I think you Need for your insert. I do not have a table named YEARRANGE so I couldn't test it completely
DECLARE #CNT INT = 0, #WEEK DATE = '2015-11-01', #FLAG INT;
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WITH CTE AS
(
SELECT num AS cnt,
DATEADD(D, SUM(num) OVER(ORDER BY num ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
, #WEEK) AS [week]
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY nl) -1 AS num
FROM
(SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
) AS ni
) AS no
)
INSERT INTO #Tmpdata (officeId,id,weekDate,startsOn,endsOn,flag)
SELECT 5134 AS officeID, cnt AS id, [week],'09:00 AM' AS startsOn,'05:00 PM' AS endsOn, COALESCE(A1.flag,0) AS flag
FROM CTE
OUTER APPLY (SELECT 1
FROM YEARRANGE
WHERE [week] BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
) AS A1(flag);

Add a year If the second row month is smaller than First row month

I have the following Query
declare #tempMonth table(ID int IDENTITY(1,1) PRIMARY KEY,Monthid int)
declare #tempDay table(ID int IDENTITY(1,1) PRIMARY KEY,Day int)
declare #AddedDate table(ID int IDENTITY(1,1) PRIMARY KEY,Datee date)
declare #StartEndDate table(ID int IDENTITY(1,1) PRIMARY KEY,StartDate date,Enddate date)
insert into #tempMonth select words Months from dbo.SplitString1('5|8|1|3','|')
insert into #tempDay select words Days from dbo.SplitString1('10|11|12|13','|')
insert into #AddedDate
select convert(varchar,(convert(varchar,Year)+'-'+convert(varchar,Month)+'-'+convert(varchar,Day))) Datee from(
select td.ID,td.Day,tm.Monthid Month, YEAR( getdate()) Year --convert(date,convert(varchar,((Year(getdate()))+'-'+tm.Monthid+'-'+td.Day)))Datee
from #tempDay td
join #tempMonth tm on tm.ID=td.ID
) x
declare #t1 table(ID int IDENTITY(1,1) PRIMARY KEY,StartDate date)
declare #t2 table(ID int IDENTITY(0,1) PRIMARY KEY,StartDate date)
insert into #t1 select Datee StartDate from #AddedDate
insert into #t2 select Datee StartDate from #AddedDate
select t1.ID,t1.StartDate ,
case when t1.ID=4 then DATEADD(day,-1, (SELECT top 1 t3.StartDate FROM #t1 t3 ORDER BY t3.ID)) else
DATEADD(day,-1,t2.StartDate) end EndDate from #t1 t1
left join #t2 t2 on t2.ID=t1.ID
This will produce the following table
ID StartDate EndDate
1 2015-05-10 2015-08-10
2 2015-08-11 2015-01-11
3 2015-01-12 2015-03-12
4 2015-03-13 2015-05-09
I want to increase the year If the second column month value is higher than
first column value.
I have try to change the last select query like below
---
----
insert into #t1 select Datee StartDate from #AddedDate
insert into #t2 select Datee StartDate from #AddedDate
declare #Flag int=0;
select t1.ID,
case when #Flag=0 then t1.StartDate else DATEADD(year,1,t1.StartDate) end StartDate,
case when (month(t1.StartDate)>month(t2.StartDate)) then #Flag=1 end,
case when #Flag=0 then t2.StartDate else(DATEADD(YEAR,1,t2.StartDate)) end EndDate
from #t1 t1
left join #t2 t2 on t2.ID=t1.ID
But error occur, But I need the following output
ID StartDate EndDate
1 2015-05-10 2015-08-10
2 2015-08-11 2016-01-11
3 2016-01-12 2016-03-12
4 2016-03-13 2016-05-09
SplitString1 Function Definition is
CREATE FUNCTION [dbo].[SplitString1]( #StringValue varchar( 2000
) ,
#Delimiter char( 1
)
)
RETURNS #resulttable TABLE( words varchar( 2000
)
)
AS
BEGIN
DECLARE
#index int;
DECLARE
#sliceOfStringValue varchar( 2000
);
SET #index = 1;
IF LEN( #StringValue
)
<
1
OR #StringValue IS NULL
BEGIN
RETURN
END;
WHILE #index != 0
BEGIN
SET #index = CHARINDEX( #Delimiter , #StringValue
);
IF #index != 0
BEGIN
SET #sliceOfStringValue = LEFT( #StringValue , #index - 1
);
END
ELSE
BEGIN
SET #sliceOfStringValue = #StringValue;
END;
IF LEN( #sliceOfStringValue
)
>
0
BEGIN
INSERT INTO #resulttable( words
)
VALUES( #sliceOfStringValue
)
END;
SET #StringValue = RIGHT( #StringValue , LEN( #StringValue
) - #index
);
IF LEN( #StringValue
)
=
0
BEGIN BREAK
END;
END;
RETURN;
END;
How about building on your query. The following adds a year whenever the end date is before the start date:
with t as (
select t1.ID, t1.StartDate ,
(case when t1.ID=4 then DATEADD(day,-1, (SELECT top 1 t3.StartDate FROM #t1 t3 ORDER BY t3.ID))
else DATEADD(day,-1,t2.StartDate)
end) as EndDate
from #t1 t1 left join
#t2 t2
on t2.ID = t1.ID
)
select id, StartDate,
(case when EndDate < StartDate then dateadd(year, 1, EndDate)
else EndDate
end) as EndDate
from t;
If you only want to focus on the month:
select id, StartDate,
(case when month(EndDate) < month(StartDate)
then dateadd(year, 1, EndDate)
else EndDate
end) as EndDate
from t;

Summing and grouping the number of records in a month

I have the following table which has employees' absence:
RecordId EmpID ActivityCode DateFrom DateTo
---------------------------------------------------------------
666542 1511 AB 29/01/2011 02/02/2011
666986 1511 AB 11/11/2011 11/11/2011
666996 1511 EL 13/11/2011 17/11/2011
755485 1787 SL 01/11/2011 14/11/2011
758545 1787 SL 15/11/2011 03/12/2011
796956 1954 AB 09/11/2011 09/11/2011
799656 1367 AB 09/11/2011 09/11/2011
808845 1527 EL 16/11/2011 16/11/2011
823323 1527 EL 17/11/2011 17/11/2011
823669 1527 EL 18/11/2011 18/11/2011
899555 1123 AB 09/11/2011 09/12/2011
990990 1511 AB 12/11/2011 12/11/2011
Now I want a report generated by a stored proc to sum all the absence days for a specific absence code for each month in a given year, for example If i want to know the totals of absence in 2011 from the previous table I will get something similar to:
Month TotalDays
---------------------------------
JAN 2011 201
FEB 2011 36
MAR 2011 67
APR 2011 91
....
The stored proc will have two params (#Year INT, #AbsCode NVARCHAR(3)).
Please Note, Sometimes a record overlaps another month (like the first row in the example table) and that should be counted separately for each month. I have tried using loops but with no luck. I am so weak in TSQL.
UPDATE
Right now I am using a scalar value user function and a stored procedure to do the job, Its ugly and hard to trace. Here it is any way:
The User function:
ALTER FUNCTION [dbo].[GetActivityTotalDaysInMonth]
(
#ActivityCode CHAR(3)
,#Year INT
,#Month INT
)
RETURNS INT
AS
BEGIN
DECLARE #FirstDayOfMonth DATETIME
DECLARE #LastDayOfMonth DATETIME
SET #FirstDayOfMonth = CAST(CAST(#Year AS varchar) + '-' + CAST(#Month AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
SET #LastDayOfMonth = DATEADD(s, -1, DATEADD(M, 1, #FirstDayOfMonth))
DECLARE #TotalDays INT
SELECT #TotalDays =
SUM(DATEDIFF(DAY,
(CASE WHEN ActivityDateFrom < #FirstDayOfMonth THEN #FirstDayOfMonth ELSE ActivityDateFrom END)
, (CASE WHEN ActivityDateTo > #LastDayOfMonth THEN #LastDayOfMonth ELSE ActivityDateTo END))+1)
FROM Activities
WHERE
ActivityCode=#ActivityCode
AND ((ActivityDateFrom < #FirstDayOfMonth AND ActivityDateTo >= #FirstDayOfMonth)
OR (ActivityDateFrom >= #FirstDayOfMonth AND ActivityDateTo <= #LastDayOfMonth)
OR (ActivityDateFrom <= #LastDayOfMonth AND ActivityDateTo > #LastDayOfMonth))
RETURN #TotalDays
END
Now, I call this function inside a loop in a stored procedure:
ALTER PROCEDURE GetAnnualActivityTotalDays
(
#ActivityCode CHAR(3)
,#Year INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Stats TABLE
([Month] NVARCHAR(50), TotalDays INT)
DECLARE #MonthNo INT
DECLARE #Month DATETIME
SET #MonthNo = 1
WHILE #MonthNo <= 12
BEGIN
SET #Month = CAST(CAST(#Year AS varchar) + '-' + CAST(#MonthNo AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
INSERT INTO #Stats ([Month], TotalDays)
SELECT UPPER(SUBSTRING(DATENAME(mm, #Month), 1, 3)) + ', ' + CAST(#Year AS NVARCHAR),
dbo.GetActivityTotalDaysInMonth(#ActivityCode
,#Year
,#MonthNo
,#Base)
SET #MonthNo = #MonthNo + 1
END
SELECT * FROM #Stats
END
As you can see, this is ugly code which I believe it can be done in an easier way.. Any suggestions?
You'd need to create a calendar table which will allow you to easily count the days for each month that the start and end dates encompass. For example recordid = 666542 has 3 days in January and 2 days in February. You'd be able to get that number by a query like
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
If you wrap that in a common table expression you can perform aggregation queries afterwards on the CTE.
with mycte as (
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
)
select calyear, calmonth, count(caldate)
from mycte
group by calyear, calmonth
order by calyear, calmonth
To generate the calendar table you can use code similar to
create table calendar (calyear, calmonth, caldate)
declare #numdays int --number of days to generate in the calendar
declare #datestart datetime --the date to begin from in the calendar
set #numdays = 365
set #datestart = 'jan 1 2011';
with num as (
select 0 number
union
select 1 number
union
select 2 number
union
select 3 number
union
select 4 number
union
select 5 number
union
select 6 number
union
select 7 number
union
select 8 number
union
select 9 number
),
numberlist as (
select ((hundreds.number * 100) + (tens.number * 10) + ones.number) n
from num hundreds
cross join num tens
cross join num ones
where ((hundreds.number * 100) + (tens.number * 10) + ones.number) < #numdays
)
insert into calendar (calyear, calmonth, caldate)
select
datepart(yy,dateadd(dd,n,#datestart)) calyear,
datepart(mm,dateadd(dd,n,#datestart)) calmonth,
dateadd(dd,n,#datestart)caldate
from numberlist