SQL check if contracts are active in each month of the quarter (year). Sum values/(duration in mths) - sql

I am working with an excel file that contains data and a vba to filter the data via ADO using an SQL string.
My database contains:
Surname; Start_date (of the contract), Dur_mths (contract duration in months), Value (Total value of the contract).
A contract may have the duration of 2-3 years. Therefore I would like to divide the total value of each contract by duration(Value/m), check if the contract is active in the specific period and then sum the results for each Surname. Each Surname may have several contracts active in a given period. I would like to see the sum of all contract values divided by their durations for each Surname for a given period.
My current working code for one month (January 2012) looks like this:
SELECT [Surname],
Sum (iif([Start_date] <= '2012-1-1'
AND DateAdd ("m",[Dur_mths], [Start_date]) >= DateSerial(2012,1+1, 0),[Value]/[Dur_mths],0))
AS [SumValue] FROM [Data$]
GROUP BY [Surname]
Now, I would like to make it work for a quarter (or a year).
I understand this still requires checking for each month in a quarter if the contract is active betw. start of contract and end of contract (start plus duration in months).
Here comes my question:
Is there an easy way to develop the code to do the same for a quarter (a year, any period)? Ie. to check if the contract is active for several months in a given period (quarter, year)? And then sum the values of active contracts(/duration in months) for each customer?
Thank you very much for all the help.
Jacek
EDIT! : In the case of one quarter, for one customer or surname, the calculation should test: in how many months in the quarter (or in the year, or in the specified period) the contract is active.
m1 m2 m3
contr1 50 50 0 (contract ended in m2)
contr2 0 0 20 (contract starts in m3)
contr3 10 10 10 (contract started 1y ego and will continue well into 2015) etc.
For this single surname, the sum for the quarter should give 150.
EDIT2:
I am now thinking how to perhaps use a loop to calculate how many months in a quarter (or a year, or a given period) the contract was active. Eg. I am trying to develop a loop, sopmething like (It is not actually any working code but an idea what I think needs to be done, I am not a programmer unfortunately):
(??? i do not know what to do with it yet...)
DECLARE #I INT, #N INT;
DECLARE #StartDate DATETIME #EndDate DATETIME
SET #I = 1
SET #N = 0
SET #QueryStartDate = 2012-01-01
SET #EndDate = DateAdd ("m",[Dur_mths], [Start_date])
WHILE #I <= 3
BEGIN
SET #N = #N + iif(DateSerial(year[Start_date];month[Start_date]+#I-1;1); <= #QueryStartDate AND #EndDate >= DateSerial(year(#StartDate),month(#StartDate+#I, 0),1,0))
SET #I = #I + 1
END
(??? i do not know what to do with it yet...)
It would return the number of months the contract was active - #N. I would use it to multiply my contract/(duration in months) in the initial code. I am trying now to read how to incorporate it in my initial code.

You can, perhaps, use the same DateAdd function with various parameters to get the data for a quarter or a year, as below:
AND DateAdd ("q",[Dur_quarter], [Start_date]) >= DateSerial(2012,1+1, 0),[Value]/[Dur_quarter],0))
and
AND DateAdd ("yyyy",[Dur_year], [Start_date]) >= DateSerial(2012,1+1, 0),[Value]/[Dur_year],0))
The complete details of the DATEADD function are provided on MSDN here.

For the time being I arrived at a following solution for a single quarter -by simply repeating the code for the month. The date range and region is entered to sql string from excel cells...I am still wondering if it can be made somehow universal for any period (with a loop?) In addition I added a left join
SELECT [Data$].[Surname],[DataInternal$].[Code], [reg],
Sum (iif([Start_date]
<= '2012-1-1'
AND DateAdd ("m",[Dur_mths], [Start_date])
>= DateSerial(2012,1+1, 0)
,[Value]/[Dur_mths],0)) AS [M1],
Sum (iif([Start_date]
<= '2012-2-1'
AND DateAdd ("m",[Dur_mths], [Start_date])
>= DateSerial(2012,1+2, 0)
,[Value]/[Dur_mths],0)) AS [M2],
Sum (iif([Start_date]
<= '2012-3-1'
AND DateAdd ("m",[Dur_mths], [Start_date])
>= DateSerial(2012,1+3, 0)
,[Value]/[Dur_mths],0)) AS [M3],
([M1]+[M2]+[M3]) AS [Q]
FROM [Data$]
LEFT JOIN [DataInternal$] ON [Data$].[Surname] = [DataInternal$].[Surname]
WHERE 1 = 1 AND [reg] = 'N'
GROUP BY [Data$].[Surname],[Code],[reg] ORDER BY [Data$].[Surname]
Empty lines added only for readability. The strings are first concatenated in excel to form a one SQL query string executed in vba macro. I will be grateful for all the comments and corrections. My question remains unadressed yet, if it is perhaps possible to loop such a solution in EXCEL ADO VBA.

Related

Calculate Recurring User For 12 Months with SQL

I'm trying to see if there is a better way to achieve what I am doing right now. For example, I need to know total number of users who have logged in for the past 12 months. So each user who has logged in at least once a month, for twelve months in a row would count towards the total.
The way I am doing this right now is: I query my table and get all user ids and timestamps of when they were active and return them to my c# code. Then with bunch of loops and LINQ I calculate the value (Its too much code to dump into this question and since I'm trying to get away from doing it in c# I don't believe there is a need for it).
Now this takes some time to run and I'm sure there has to be a better way to do this with SQL. I've searched but haven't found any SQL functions that let you count based on a recurring condition.
For an answer I'm hoping to either get an example or a link to a similar SO question or an article that talks about achieving this.
An example of MyUsersTable:
UserId | Timestamp
1 | '2018-12-23 00:00:00.000'
1 | '2018-11-23 00:00:00.000'
1 | '2018-10-23 00:00:00.000'
EDIT: I did thought of using SUM(CASE WHEN month = 1 and month = 2 and month = 3) but that seems also like not a great solution.
Expected Result:
Total number of users who were active at least once a month in the last 12 months.
If you need users who logged in every month in 2018:
select ut.userid
from MyUsersTable ut
where timestamp >= '2018-01-01' and timestamp < '2019-01-01'
group by ut.userid
having count(distinct month(timestamp)) = 12;
I'd count the distinct number of months a user logged in on:
SELECT userid
FROM mytable
WHERE YEAR(timestamp) = 2018
GROUP BY userid
HAVING COUNT(DISTINCT MONTH(timestamp)) = 12
To get userIDs who logged in for a specific number of consecutive months, you can use:
/* These are your input values */
DECLARE #searchDate date = '2018-12-15' ;
DECLARE #monthsToSearch int = 12 ;
/* First day of search month */
DECLARE #EndDate date = DATEADD(month, DATEDIFF(month, 0, #searchDate), 0) ;
/* First day of month to search from */
DECLARE #StartDate date = DATEADD(month, -#monthsToSearch, #EndDate) ;
SELECT userID --, #StartDate AS startDate, #EndDate AS endDate
FROM (
SELECT userID, ( (YEAR(userLoginDT)*100)+MONTH(userLoginDT) ) AS datePoint /* YYYYMM */
FROM t1
WHERE userLoginDT >= #StartDate
AND userLoginDT < #EndDate
) s1
GROUP BY userID
HAVING count(distinct(datePoint)) = #monthsToSearch
;
See the db<>fiddle here for my examples.
The fist two declared variables are your input variables. You feed it the date your are running the report on and then telling it how many months you want to go back to. So you can search any number of months. After that, it's pretty much date manipulation and math.
#EndDate essentially takes your declared date and calculates the first day of the month you are currently searching in. You will search for any dates before this date.
#StartDate counts back from your #EndDate to calculate the number of months you want to search.
(YEAR(userLoginDT)*100)+MONTH(userLoginDT) in your sub-select creates an integer variable that you can GROUP BY to get a distinct count of months you're searching over. This part could be sped up with the Calendar Table.
Then you just use the HAVING to pick out how many distinct records your want for #monthsToSearch.
NOTE: As many here can attest, I'm a huge fan of working with Calendar Tables when dealing with date calculations and large amounts of search data. Something like that would likely speed the query up a bit.

Counting working days between two dates Oracle

I'm trying to write a query which can count the number of working days between a payment being received and being processed, I started playing around with this for payments received in December 2017;
select unique trunc(date_received),
(case when trunc(date_received) in ('25-DEC-17','26-DEC-17') Then 0 when
to_char(date_received,'D') <6 Then 1 else 0 end) Working_day
from payments
where date_received between '01-DEC-17' and '31-dec-17'
order by trunc(date_received)
But to be honest, I'm at a loss as to how to take it further and add in date_processed and count the number of working days between date_processed and date_received... Any help would be much appreciated...
Maybe not the most optimal, but it works quite nicely, and it's easy to incorporate more complicated checks, like holidays. This query first generates all dates between the two dates, and then lets you filter out all the days that 'don't count'.
In this implementation I filtered out only weekend days, but it's quite easy to add checks for holidays and such.
with
-- YourQuery: I used a stub, but you can use your actual query here, which
-- returns a from date and to date. If you have multiple rows, you can also
-- output some id here, which can be used for grouping in the last step.
YourQuery as
(
select
trunc(sysdate - 7) as FromDate,
trunc(sysdate) as ToDate
from dual),
-- DaysBetween. This returns all the dates from the start date up to and
-- including the end date.
DaysBetween as
(
select
FromDate,
FromDate + level - 1 as DayBetween,
ToDate
from
YourQuery
connect by
FromDate + level - 1 <= ToDate)
-- As a last step, you can filter out all the days you want.
-- This default query only filters out Saturdays and Sundays, but you
-- could add a 'not exists' check that checks against a table with known
-- holidays.
select
count(*)
from
DaysBetween d
where
trim(to_char(DAYINBETWEEN, 'DAY', 'NLS_DATE_LANGUAGE=AMERICAN'))
not in ('SATURDAY', 'SUNDAY');

SSRS. Workday Function

I'm trying to convert the below Excel formula into SSRS but having looked around I cannot seem to find a solution. I can calculate the number of working days between two dates but what I'm trying to do is add on a number of working days to a date. Essentially I don't have the 2nd date.
I guess it would be something along the lines of the DATEADD function?
=WORKDAY($A1,B$1)
Hope someone can help
Many thanks
Here is a tsql solution to add X Business Days to a date.
declare #calendar as table (theDate date, dayOfWeek varchar (10));
declare #startDate as date = '20170704';
declare #businessDaysToAdd as integer = 10;
insert into #calendar
select theDate
, datename(dw, theDate) dow
from
dbo.dateTable('20170701', '20170720') ;
with temp as (
select theDate
, dayOfWeek
, rank() over (order by theDate) theRank
from #calendar
where theDate > #startDate
and dayOfWeek not in ('Saturday', 'Sunday')
)
select * from temp
where theRank = #businessDaysToAdd;
Notes
dbo.DateTable is a table valued function that just happens to exist in the database I was using. In real life, you might have an actual calendar table of some sort.
This example does not include holidays.
This is only the start of the answer to the posted question. It only solves the problem of Essentially I don't have the 2nd date.
Type this into the expression for the textbox. (From SSRS 2008 Datediff for Working Days)
=(DateDiff(DateInterval.day,Parameters!STARTDATE.Value,Parameters!ENDDATE.Value)+1)
-(DateDiff(DateInterval.WeekOfYear,Parameters!STARTDATE.Value,Parameters!ENDDATE.Value)*2)
-(iif(Weekday(Parameters!STARTDATE.Value) = 7,1,0)
-(iif(Weekday(Parameters!ENDDATE.Value) = 6,1,0))-1)
Ok after much perseverance I managed to get what I wanted in both TSQL and SSRS. My objective was to measure Agent productivity so I didn’t want to count the weekend and this would be unfair. If a date fell on a weekend then I wanted it to jump to a Monday. Likewise if adding number of days onto a date went over a weekend in the future then I needed the incremented date to reflect this. For the end user (In SSRS) I wanted a leading edge (Like an Upside down triangle) so that if the date + number working days was in the future then set to NULL, showing a zero would look like no productivity which is incorrect.
First TSQL - My base query started with the following SO thread but after trying many of the options I was finding when the date fell on a Saturday or Sunday the solution did not work for me (I was unable to create functions due to permissions). However tweaking the below got me there and I dealt with Sunday specifically
Add business days to date in SQL without loops
SELECT
,DATEADD(WEEKDAY, (/*Your Working Days*//5)*7+(/*Your Working Days*/ % 5) +
(CASE WHEN DATEPART(WEEKDAY,/*Your Date*/) <>7 AND DATEPART(WEEKDAY,/*Your Date*/) + (/*Your Working Days*/ % 5) >5 THEN 2
WHEN DATEPART(WEEKDAY,/*Your Date*/) = 7 AND DATEPART(WEEKDAY,/*Your Date*/) + (/*Your Working Days*/ % 5) >5 THEN 1 ELSE 0 END), /*Your Date*/) AS [IncrementedDate]
FROM /*YourTable*/
Then for SSRS - The 2 key points here is that TSQL will divide as an integer if the source number is an integer so this needs to be handled in SSRS and secondly you need to set the first week day to Monday as part of the expression.
I put this expression into a Matrix with Date Created being my Row Group and Contact Working Days being my Column Group.
=DATEADD("W",(INT(ReportItems!ContactWorkingDays.Value/5))*7+(ReportItems!ContactWorkingDays.Value MOD 5) + IIF(DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) <> 7 AND (DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) + (ReportItems!ContactWorkingDays.Value MOD 5) >5),2,IIF(DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) = 7 AND (DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) + (ReportItems!ContactWorkingDays.Value MOD 5) >5),1,0)),ReportItems!DateCreated.Value)
This does not include holidays - I'm not too bothered at this stage and that is for a rainy day! :)

Createing a report using financial periods

I have created a report for management that will total everything up by month with in a date range. Management has now decided that rather than by month they would like to go by period. We have 13 periods in a year each is 28 days except the last one is 29 or 30 depending on if its a leap year. The beginning of the first period is always 1-1-YYYY. So now I will need to figure out what the beginning and end of each period is and total up each period. I am not really sure how to do this since every year the dates will change and they may want to look at periods from the previous year through the current period. The code and results I am currently using are enclosed
SELECT
DATEADD(MONTH, DATEDIFF(MONTH, 0, finspecteddate), 0) AS 'Date'
,COUNT(*) AS Lots
,sum(flotSize) as 'Lot Size'
,sum(LReject) 'Lots Rejected'
,sum(fnumreject) as Rejected
,sum(fsampleSize) as 'Sample Size'
,sum(BDueDate) as 'Before Due Date'
FROM
ReportData
WHERE
finspecteddate >= '01-01-2014'
AND finspecteddate <= '10-15-2014'
GROUP BY
DATEADD(MONTH, DATEDIFF(MONTH, 0, finspecteddate), 0)
ORDER BY
date
Modify the following queries to suit your needs:
;WITH Period AS (
SELECT 1 AS ReportingPeriod,
CAST('2013-01-01' AS datetime) AS PeriodStartDate,
CAST('2013-01-28' AS datetime) AS PeriodEndDate
UNION ALL
SELECT CASE
WHEN p.ReportingPeriod = 13 THEN 1
ELSE p.ReportingPeriod + 1
END,
CASE
WHEN p.ReportingPeriod = 13 THEN DATEADD(YEAR,YEAR(p.PeriodStartDate)-1899,'1900-01-01')
ELSE DATEADD(DAY,28,p.PeriodStartDate)
END,
CASE
WHEN p.ReportingPeriod = 12 THEN DATEADD(YEAR,YEAR(p.PeriodStartDate)-1900,'1900-12-31')
ELSE DATEADD(DAY,28,p.PeriodEndDate)
END
FROM Period p
WHERE p.PeriodStartDate < '2017-12-03'
)
SELECT
P.PeriodStartDate
,P.PeriodEndDate
,COUNT(*) AS Lots
,sum(flotSize) as 'Lot Size'
,sum(LReject) 'Lots Rejected'
,sum(fnumreject) as Rejected
,sum(fsampleSize) as 'Sample Size'
,sum(BDueDate) as 'Before Due Date'
FROM
ReportData R
INNER JOIN Period P ON R.finspecteddate >= P.PeriodStartDate AND R.finspecteddate <= P.PeriodEndDate
WHERE
finspecteddate >= '01-01-2014'
AND finspecteddate <= '10-15-2014'
GROUP BY
P.PeriodStartDate
,P.PeriodEndDate
ORDER BY
P.PeriodStartDate
It uses a recursive CTE to build a period table, which is then joined to ReportData to aggregate asccording to your requirements. I don't have SQL Server 2005 to test it on. It works with 2008. Post a SQL Fiddle if you need help in 2005.
If you haven't got one, create a period calendar table with a year, period number, start date and end date columns. Then when you need to refer to periods, you can refer to the table. When they change the definition of what a period is, you can change the table. When they decide that February 29 doesn't count as one of the 28 days, you can change the table. When they decide to use the first Monday instead of the first Thursday as the start of the year, you just change the table. And best of all, changing how next year works won't change how last year works.
Then you just join to the table to determine which period you're in.

Sum of values per month divided by days of month

I have a Sales table:
id_item, quantity, date_time
What I need is to sum the items sold in a month and divide them by the days of the month from a selected period of months.
Example - The user selects the dates of Oct 1 to Dec 31. I need to show the items_sold/days_of_month:
Month Items sold Days of month Items/Day
Sep 25 30 0.83333
Oct 36 31 1.16
Dec 15 31 0.4838
I have to specify by Kind of item. the kind is obtained from another table called Items. I use dateformat dd/mm/yy.
select
month(date_time),
sum(quantity) / (select(datepart(dd,getdate())))
from
sales v
join items a on v.id_item=a.id_item
where
a.kind='Kind of Item'
and cast(Convert(varchar(10), date_time, 112) as datetime)
between '01/10/2012' and '31/12/2012'
group by
month(date_time)
My problem is selecting the days of the months, how can I select x number of months and divide the sum(quantity) of each month by the days of each?
I know this part of the code only selects the days of the current month:
(select(datepart(dd,getdate())))
Try this on for size:
DECLARE
#FromDate datetime,
#ToDate date; -- inclusive
SET #FromDate = DateAdd(month, DateDiff(month, 0, '20121118'), 0);
SET #ToDate = DateAdd(month, DateDiff(month, 0, '20121220') + 1, 0);
SELECT
Year = Year(S.date_time),
Month = Month(S.date_time),
QtyPerDay =
Sum(s.quantity) * 1.0
/ DateDiff(day, M.MonthStart, DateAdd(month, 1, M.MonthStart))
FROM
dbo.Sales S
INNER JOIN dbo.Items I
ON S.id_item = I.id_item
CROSS APPLY (
SELECT MonthStart = DateAdd(month, DateDiff(month, 0, S.date_time), 0)
) M
WHERE
I.kind = 'Kind of Item'
AND S.date_time >= #FromDate
AND S.date_time < #ToDate
GROUP BY
Year(S.date_time),
Month(S.date_time),
M.MonthStart
It will select any full month that is partially enclosed by the FromDate and ToDate. The * 1.0 part is required if the quantity column is an integer, otherwise you will get an integer result instead of a decimal one.
Some stylistic notes:
Do NOT use string date conversion on a column to ensure you get whole days. This will completely prevent any index from being used, require more CPU, and furthermore is unclear (what does style 112 do again!?!?). To enclose full date periods, use what I showed in my query of DateCol >= StartDate and DateCol < OneMoreThanEndDate. Do a search for "sargable" to understand a very key concept here. A very safe and valuable general rule is to never put a column inside an expression if the condition can be rewritten to avoid it.
It is good that you're aliasing your tables, but you should use those aliases throughout the query for each column, as I did in my query. I recognize that the aliases V and A came from another language so they make sense there--just in general try to use aliases that match the table names.
Do include the schema name on your objects. Not doing so is not a huge no-no, but there are definite benefits and it is best practice.
When you ask a question it is helpful to explain all the logic so people don't have to guess or ask you--if you know (for example) that users can input mid-month dates but you need whole months then please indicate that in your question and state what needs to be done.
Giving the version of SQL server helps us zero in on the syntax required, as prior versions are less expressive. By telling us the version we can give you the best query possible.
Note: there is nothing wrong with putting the date calculation math in the query itself (instead of using SET to do it). But I figured you would be encoding this in a stored procedure and if so, using SET is just fine.