Adapting SQL to work over multiple weeks - sql-server

I have a report that determines employees that have entered less than 40 hours on their timesheets for a specific week:
With Under40s as (
Select Distinct Entry.UUID, Entry.FirstName, Entry.LastName, Team.PersonUnit, Team.TeamName
From Entry as Entry
Inner Join TeamOrganization as Team on Team.PersonUUID = Entry.UUID and #EnteredDate between Team.PeriodBeginDate and Team.PeriodEndDate
Where
(
Select sum(Entry2.Hours) From Entry as Entry2
Where Entry2.UUID = Entry.UUID and Entry2.Date Between #StartDate and #EndDate
) < 40
Or not exists
(
Select 1 From Entry as Entry2
Where Entry2.UUID = Entry.UUID and Entry2.Date Between #StartDate and #EndDate
)
)
Select distinct Under40s.UUID, Under40s.FirstName, Under40s.LastName, Under40s.PersonUnit, Under40s.TeamName, Case
When Sum(Entry.Hours) is null then 0
Else Sum(Entry.Hours)
End as TotalHours
From Under40s
Left Join Entry as Entry on Entry.UUID = Under40s.UUID and Entry.Date Between #StartDate and #EndDate
Where Under40s.PersonUnit in (#PersonUnit) and Under40s.TeamName in (#Teams)
Group By Under40s.UUID, Under40s.FirstName, Under40s.LastName, Under40s.PersonUnit, Under40s.TeamName
Order By Under40s.LastName, Under40s.FirstName
The client now wants to be able to enter a date range of more than a week. I'm thinking they want to see for each week within that date range, who's reporting less than 40 hours. I am not sure how, or if, I can modify the SQL to deliver such a report. Can anyone give me a hint?
Thanks!

Assuming every working day is 8 hours, one option would be to get the the day differences between start date and end date and then multiplying that by 8 and using the product as the criteria. For example: startdate: 20/11/2013 and enddate: 31/12/2013 is 41 days including weekends (so you also you have to filter weekends or non-working days), but just as example 41*8 = 328. So instead of less than 40 it will be less than 328. Here's a link to a working solution of excluding weekends and other specified dates: Number of days in date range, excluding weekends and other dates, in C#

Related

T-SQL results per day for 1 year between time range

I need to return count on results per day between a time range that will overlap days.
So from 8 PM to 8 AM for every day that starts on M-F return a count for that time period. And do that for the entire year.
This is what I have, and I could do a simple and between if I only wanted on day but I'm not sure how to iterate through days especially when the start is on one day and the end on the next but skip the ones that Start on a Saturday or Sunday.
SELECT TOP (50)
ClientVisit.visittype,
ClientVisit.location_id,
ClientVisit.visittype_id,
Location.location_desc,
Location.location_code,
ClientVisit.timein,
ClientVisit.timeout,
ClientVisit.visit_dateday
FROM
ClientVisit
INNER JOIN
Location ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%'
OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505'
OR Location.location_code = 'Access-hosp')
AND (ClientVisit.timein BETWEEN #param1 AND #param2)
Filtering days of week and hours of the day is easy enough. Does this group by get at what you're trying to accomplish for the counts?
SELECT CAST(ClientVisit.timein AS DATE) AS DT, COUNT(*)
FROM ClientVisit INNER JOIN Location
ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%' OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505' OR Location.location_code = 'Access-hosp')
-- Use date params rather than datetime
AND CAST(ClientVisit.timein AS DATE) BETWEEN #param1 AND #param2
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, ClientVisit.timein) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND ( DATEPART(hour, ClientVisit.timein) BETWEEN 8 AND 23
OR DATEPART(hour, ClientVisit.timein) BETWEEN 0 AND 7)
GROUP BY CAST(ClientVisit.timein AS DATE);
If you need to treat the hours from 8PM to 8AM as a single shift then you can adjust the times prior to so that times after midnight are treated as part of the preceeding day:
WITH AdjustedVisit AS (
SELECT *, DATEADD(hour, -8, timein) AS adjustedin FROM ClientVisit)
-- Use date params rather than datetime
WHERE CAST(timein AS DATE) BETWEEN #param1 AND #param2
)
SELECT CAST(v.adjustedin AS DATE) AS DT, COUNT(*)
FROM AdjustedVisit AS v INNER JOIN Location AS l
ON v.location_id = l.location_id
WHERE
(v.visittype Like '%Open Chart%' OR v.visittype LIKE '%Diag%')
AND (l.location_code = 'Access-505' OR l.location_code = 'Access-hosp')
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, v.adjustedin) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND DATEPART(hour, v.adjustedin) BETWEEN 12 AND 23
GROUP BY CAST(v.adjustedin AS DATE);

How to find out Date/Month interval between #StartDate and #EndDate passed in stored procedure parameters

I am working on a stored procedure where I am dividing the number of rows by interval of month and day repeated in the specified date range.
Interval Month and Day = 7th April and 8th October
Example
For Date range 2014/01/01 and 2014/12/31, 7th April and 8th October are repeated 2 times so I will divide my statement by 2.
For Date range 2014/01/01 and 2015/09/01, 7th April came 2 and 8th October 1 so I will divide my statement by 3.
As others have stated, the question is a bit unclear, but I believe I know what you're trying to do. You are trying to find the number of times that a set of dates (only taking Month/Day into account) happen over a range of dates (set by #StartDate and #EndDate). I think the select count(*) from TableName part of the question is a distraction, as you already know how to do that. Below is an answer on how to get the denominator, which is what you are trying to figure out how to do.
declare #StartDate date = '2014-01-01'
, #EndDate date = '2014-12-31'
, #DenVal int --Denominator Value
create table #dates_of_interest
(
month_nbr tinyint not null
, day_nbr tinyint not null
)
insert into #dates_of_interest
values (4, 7) --7th of April
, (10, 8) --8th of October
; with date_list as
(
--use a Recursive CTE to generate a list of all the dates in the given range.
select #StartDate as dt
union all
select dateadd(d,1,dt) as dt
from date_list
where 1=1
and dt < #EndDate
)
--Get the output of the Recursive CTE along with Month/Day numbes
select dt
, datepart(m,dt) as month_nbr
, datepart(d,dt) as day_nbr
into #list_of_dates
from date_list as dl
option (maxrecursion 32767) --set to max possible levels of recursion (might want to lower this number)
--Set the Denominator to the results of the sum(case/when) AKA countif
set #DenVal =
(
select sum(case when di.month_nbr is null and di.day_nbr is null then 0 else 1 end)
from #list_of_dates as ld
left join #dates_of_interest as di on ld.month_nbr = di.month_nbr
and ld.day_nbr = di.day_nbr
)
Print #DenVal
Both examples of 1/1/2014 - 12/31/2014 and 1/1/2014 - 9/1/2015 come up with the desired results of 2 and 3 respectively. There may be other ways of accomplishing this, but I thought that a Recursive CTE was the best option.

SQL Server : calculating days elapsed

I need to get the number of elapsed days between any two dates with respect to the current date. IE:
mm/dd/yyyy
Current day = 07/10/2015
07/08/2013 ... 07/11/2013 - 4 days elapsed
Current day = 07/10/2015
07/08/2015 ... 07/11/2015 - 2 days have elapsed
I've tried several combinations using DATEDIFF with day as the date part, however, I can't seem to get a clean way to get the days elapsed when the date could be past or present.
EDIT
I know the start date and the end date of a certain business process. They could be this year, last year, two years ago and so on. I need a way via SQL Server functions to figure out the days total elapsed. If it's not the current year, obviously the entire span/range would have elapsed. If it's the current year, perhaps the entire span/range hasn't elapsed and it needs to say how many days are "into the process" based on the respected start time, end time and current time.
Hopefully this makes more sense?
Please help.
I used #Sean Lange, with a small tweak:
DATEDIFF(DAY, #StartDate, case when #EndDate < GETDATE() then #EndDate + 1 else GETDATE() end)
Thanks all.
This is pretty similar to the answer provided by Stan but here is my take on this.
with Something as
(
select CAST('2013-07-08' as datetime) as StartDate
, CAST('2013-07-11' as datetime) as EndDate
union all
select '2015-07-08', '2015-07-11'
)
select *
, DATEDIFF(DAY, StartDate, case when EndDate < GETDATE() then EndDate else GETDATE() end)
from Something
How about this:
Given:
CREATE TABLE dbo.test ( ChildID INT Identity,
Start DateTime
, Finish DateTime
)
and your test data:
insert into dbo.test (start,finish) values('07/08/2013','07/11/2013')
insert into dbo.test (start,finish) values('07/08/2015','07/11/2015')
then
select start,finish
, DATEDIFF(DAY, start, CASE WHEN GETDATE() BETWEEN start and finish
THEN GETDATE() - 1 ELSE finish END) + 1 as elapsed
from dbo.test
gives the result from your example.
You might have to tweak if there are other adjustments for how the current date fits between the range.

Get record based on year in oracle

I am creating a query to give number of days between two days based on year. Actually I have below type of date range
From Date: TO_DATE('01-Jun-2011','dd-MM-yyyy')
To Date: TO_DATE('31-Dec-2013','dd-MM-yyyy')
My Result should be:
Year Number of day
------------------------------
2011 XXX
2012 XXX
2013 XXX
I've tried below query
WITH all_dates AS
(SELECT start_date + LEVEL - 1 AS a_date
FROM
(SELECT TO_DATE ('21/03/2011', 'DD/MM/YYYY') AS start_date ,
TO_DATE ('25/06/2013', 'DD/MM/YYYY') AS end_date
FROM dual
)
CONNECT BY LEVEL <= end_date + 1 - start_date
)
SELECT TO_CHAR ( TRUNC (a_date, 'YEAR') , 'YYYY' ) AS YEAR,
COUNT (*) AS num_days
FROM all_dates
WHERE a_date - TRUNC (a_date, 'IW') < 7
GROUP BY TRUNC (a_date, 'YEAR')
ORDER BY TRUNC (a_date, 'YEAR') ;
I got exact output
Year Number of day
------------------------------
2011 286
2012 366
2013 176
My question is if i use connect by then query execution takes long time as i have millions of records in table and hence i don't want to use connect by clause
connect by clause is creating virtual rows against the particular record.
Any help or suggestion would be greatly appreciated.
From your vague expected results I think you want the number of records between those dates, not the number of days; but it's rather unclear. Since you refer to a table in the question I assume you want something related to the table data, not simply days between two dates which wouldn't depend on a table at all. (I have no idea what the connect by clause reference means though). This should give you that, if it is what you want:
select extract(year from date_field), count(*)
from t42
where date_field >= to_date('01-Jun-2011', 'DD-MON-YYYY')
and date_field < to_date('31-Dec-2013') + interval '1' day
group by extract(year from date_field)
order by extract(year from date_field);
The where clause is as you'd expect between two dates; I've assumed there might be times in your date field (i.e. not all at midnight) and that you want to count all records on the last date in your range. Then it's grouping and counting based on the year for each record.
SQL Fiddle.
If you want the number of days that have records within the range, then you can just vary the count slightly:
select extract(year from date_field), count(distinct trunc(date_field))
...
SQL Fiddle.
you can use the below function to reduce the number of virtual rows by considering only the years in between.You can check the SQLFIDDLE to check the performance.
First consider only the number of days between start date and the year end of that year or
End date if it is in same year
Then consider the years in between from next year of start date to the year before the end date year
Finally consider the number of days from start of end date year to end date
Hence instead of iterating for all the days between start date and end date we need to iterate only the years
WITH all_dates AS
(SELECT (TO_CHAR(START_DATE,'yyyy') + LEVEL - 1) YEARS_BETWEEN,start_date,end_date
FROM
(SELECT TO_DATE ('21/03/2011', 'DD/MM/YYYY') AS start_date ,
TO_DATE ('25/06/2013', 'DD/MM/YYYY') AS end_date
FROM dual
)
CONNECT BY LEVEL <= (TO_CHAR(end_date,'yyyy')) - (TO_CHAR(start_date,'yyyy')-1)
)
SELECT DECODE(TO_CHAR(END_DATE,'yyyy'),YEARS_BETWEEN,END_DATE
,to_date('31-12-'||years_between,'dd-mm-yyyy'))
- DECODE(TO_CHAR(START_DATE,'yyyy'),YEARS_BETWEEN,START_DATE
,to_date('01-01-'||years_between,'dd-mm-yyyy'))+1,years_between
FROM ALL_DATES;
In Oracle you can perform Addition and Substraction to dates like this...
SELECT
TO_DATE('31-Dec-2013','dd-MM-yyyy') - TO_DATE('01-Jun-2011','dd-MM-yyyy')
DAYS FROM DUAL;
it will return day difference between two dates....
select to_date(2011, 'yyyy'), to_date(2012, 'yyyy'), to_date(2013, 'yyyy')
from dual;
TO_DATE(2011,'Y TO_DATE(2012,'Y TO_DATE(2013,'Y
--------------- --------------- ---------------
01-MAY-11 01-MAY-12 01-MAY-13
select to_char(date_field,'yyyy'), count(*)
from your_table
where date_field between to_date('01-Jun-2011', 'DD-MON-YYYY')
and to_date('31-Dec-2013 23:59:59', 'DD-MON-YYYY hh24:mi:ss')
group by to_char(date_field,'yyyy')
order by to_char(date_field,'yyyy');

Checking a range of dates in SQL

Following on from a question I put on yesterday, I need to return a range of "available" dates for a laptop rollout "booking system". I want to populate a table of possible available dates a user can book a slot on by checking for each date the total possible number of slots, and subtracting the number of slots already booked.
The logic is as follows:
A technician can build 3 laptops per day.
On any day there may be 1, 2 or 3 technicians available.
A table holds the bookings made
I don't want a table of all possible dates, I want to calculate it on the fly
Relevant tables are:
tl_sb_slotBooking
This contains the bookings already made
tl_sb_availabilityPeriods
This is used to calculate the total number of available slots on a given day
I can bring back a list of dates with a fixed maximum number (in this case 3) of slots:
DECLARE #startDate DATE
DECLARE #endDate DATE
SET #startDate = GETDATE()
SET #endDate = DATEADD(m,3,#startDate)
;
WITH dates(Date) AS
(
SELECT #startdate as Date
UNION ALL
SELECT DATEADD(d,1,[Date])
FROM dates
WHERE DATE < #enddate
)
SELECT Date
FROM dates
EXCEPT
SELECT date
FROM tl_sb_booking
GROUP BY date
HAVING COUNT(date) >= 3
However, the maximum won't always be 3, it changes for each day.
I can find the maximum possible slots for a given day:
DECLARE #myDate DATETIME = '2013-06-22'
SELECT SUM(laptopsPerDay) AS totalSlots
FROM tl_sb_technicianAvailability
WHERE startDate <= #myDate AND endDate >= #myDate
AND availabiltyStateID=3
it will bring back 6 as the total number of slots available for 2013-06-22. (The availabilityStateID field is used to store available/unavailable etc.)
So, the bit I am stuck on is combining the two.
What I want is for each possible date, if the number of slots already booked is less than the number of possible slots for that day, add it to the table being returned (otherwise don't).
Firstly, althought you are only generating a small list, using a CTE to generate a sequential list performs terribly and is best avoided.
For the sake of this I will use the system table Master..spt_values for a sequential list of numbers, but if you are worried about using undocumented system tables then there are other methods in the link above.
The first thing I would do is split the technician availability dates into a row per day, this will allow for technicians who are available for only part of the peiod required (e.g. from your sceen shot of the table if you wanted to query from 18th June to 26th June none of the technicians show would appear as available using the query you have posted):
SELECT Date = DATEADD(DAY, spt.Number, ta.StartDate),
ta.TechnicianID,
ta.LapTopsPerDay
FROM tl_sb_technicianAvailability ta
INNER JOIN Master..spt_values spt
ON spt.Type = 'P'
AND spt.Number BETWEEN 0 AND DATEDIFF(DAY, ta.startDate, ta.EndDate)
This would simply turn:
TechnicianID StartDate EndDate LapTopsPerDay
1 20130620 20130624 3
into
Date TechnicianID LapTopsPerDay
20130620 1 3
20130621 1 3
20130622 1 3
20130623 1 3
20130624 1 3
You can then limit this list to the date range required, and sum up the total laptops than can be done as this is not needed on a technicial level:
WITH ExplodedAvailability AS
( SELECT Date = DATEADD(DAY, spt.Number, ta.StartDate),
ta.TechnicianID,
ta.LapTopsPerDay
FROM tl_sb_technicianAvailability ta
INNER JOIN Master..spt_values spt
ON spt.Type = 'P'
AND spt.Number BETWEEN 0 AND DATEDIFF(DAY, ta.startDate, ta.EndDate)
)
SELECT Date, TotalLaptops = SUM(LapTopsPerDay)
FROM ExplodedAvailability
WHERE Date >= #StartDate
AND Date < #EndDate
GROUP BY Date;
Finally you can LEFT JOIN to the bookings table to get the available slots per day
WITH ExplodedAvailability AS
( SELECT Date = DATEADD(DAY, spt.Number, ta.StartDate),
ta.TechnicianID,
ta.LapTopsPerDay
FROM tl_sb_technicianAvailability ta
INNER JOIN Master..spt_values spt
ON spt.Type = 'P'
AND spt.Number BETWEEN 0 AND DATEDIFF(DAY, ta.startDate, ta.EndDate)
), Availability AS
( SELECT Date, TotalLaptops = SUM(LapTopsPerDay)
FROM ExplodedAvailability
WHERE Date >= #StartDate
AND Date < #EndDate
GROUP BY Date
), Bookings AS
( SELECT Date, SlotsBooked = COUNT(*)
FROM tl_sb_booking
GROUP BY Date
)
SELECT Availability.Date,
Availability.TotalLaptops,
RemainingSlots = Availability.TotalLaptops - ISNULL(Bookings.SlotsBooked, 0)
FROM Availability
LEFT JOIN Bookings
ON Bookings.Date = Availability.Date;
I think what you are after is to add a booking to the next available day, so the query to do this would be:
DECLARE #UserID INT = 1;
WITH ExplodedAvailability AS
( SELECT Date = DATEADD(DAY, spt.Number, ta.StartDate),
ta.TechnicianID,
ta.LapTopsPerDay
FROM tl_sb_technicianAvailability ta
INNER JOIN Master..spt_values spt
ON spt.Type = 'P'
AND spt.Number BETWEEN 0 AND DATEDIFF(DAY, ta.startDate, ta.EndDate)
), Availability AS
( SELECT Date, TotalLaptops = SUM(LapTopsPerDay)
FROM ExplodedAvailability
WHERE Date >= CAST(GETDATE() AS DATE)
GROUP BY Date
), Bookings AS
( SELECT Date, SlotsBooked = COUNT(*)
FROM tl_sb_booking
GROUP BY Date
)
INSERT tl_sb_slotBooking (UserID, Date)
SELECT #UserID, MIN(Availability.Date)
FROM Availability
LEFT JOIN Bookings
ON Bookings.Date = Availability.Date
WHERE Availability.TotalLaptops > ISNULL(Bookings.SlotsBooked, 0)
Should this be of use to anyone, this is the way I ultimately did it:
DECLARE #startDate DATE
DECLARE #endDate DATE
SET #startDate = GETDATE()
SET #endDate = DATEADD(m,3,#startDate)
;
WITH dates(currentDate) AS
(
SELECT #startdate as currentDate
UNION ALL
SELECT DATEADD(d,1,[currentDate])
FROM dates
WHERE currentDate < #enddate
)
SELECT currentDate
FROM dates
WHERE /* slots booked for date */
(
SELECT count([date])
FROM tl_sb_booking
where [date] = currentDate
)
<
/* total slots available */
(
SELECT SUM(laptopsPerDay) AS totalSlots
FROM tl_sb_technicianAvailability
WHERE startDate <= currentDate AND endDate >= currentDate
AND availabiltyStateID=3
)

Resources