Checking a range of dates in SQL - sql-server

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
)

Related

Getting individual dates from a date range using T-SQL

I have been asked to create two datasets showing 7 days of dates from a two date range.
Example: I have a date range of StartDate = 2022-12-12 and EndDate = 2022-12-25. I need a query to display the individual dates in between these two dates. I was told to use DATEADD, but cannot for the life figure this out.
Any help would be be helpful, thank you.
SELECT DATEADD(DAY, 7, StartDate) AS WeekOne
I was expecting something like this:
2022-12-12
2022-12-13
2022-12-14
2022-12-15
2022-12-16
2022-12-17
2022-12-18
DECLARE #InStartDate DATE='2022-12-12';
DECLARE #InStopDate DATE='2022-12-25';
WITH GEN AS
(
SELECT #InStartDate AS Start_dated
UNION ALL
SELECT DATEADD(DD,1,G.Start_dated)
FROM GEN AS G
WHERE G.Start_dated< #InStopDate
)
SELECT G.*
FROM GEN AS G
You can use something like this
You need to start by generating a numbers table. It needs enough rows to handle each day between your start and end dates. Something like this:
with Numbers as (
select 0 as n
union all
select n + 1
from Numbers
where n < 365
)
select n
from Numbers
option(maxrecursion 0);
Given the example range, I felt like 365 days (one year) was adequate, but it's easy to tweak that range (as we'll see).
Once you have the Numbers table, you can use DateAdd() to add that amount to the start date:
DECLARE #StartDate date = '20221212';
with Numbers as (
select 0 as n
union all
select n + 1
from Numbers
where n < 365
)
select DATEADD(day, n, #StartDate)
from Numbers
option(maxrecursion 0)
From here it's a simple matter to use the EndDate in a WHERE clause to limit the total rows:
DECLARE #StartDate date = '20221212';
DECLARE #EndDate date = '20221231';
with Numbers as (
select 0 as n
union all
select n + 1
from Numbers
where n < DATEDIFF(day, #StartDate, #EndDate)
)
select DATEADD(day, n, #StartDate)
from Numbers
option(maxrecursion 0)
For SQL Server 2022 and later you can use Generate_Series:
declare #StartDate as Date = '20221212', #EndDate as Date = '20221225';
select DateAdd( day, Value, #StartDate ) as TargetDate
from Generate_Series( 0, DateDiff( day, #StartDate, #EndDate ) );
dbfiddle.

How can I retrieve properties that have price for each date between the given dates?

I have two tables Agency_DailyPrices and Agency_DailyDiscounts.
Here is my query:
DECLARE #checkIn DATE = CAST(GETDATE() AS DATE);
DECLARE #checkOut DATE = DATEADD(DAY, 6, #checkIn);
DECLARE #currency_id INT = 3;
SELECT AP.date_, AP.property_id, price, discountPercent,
CASE WHEN discountPercent > 0 THEN (price * discountPercent/100) ELSE 0 END AS discountAmount
FROM Agency_DailyPrices AP
LEFT JOIN Agency_DailyDiscounts AD ON AD.date_= AP.date_ AND AD.property_id = AP.property_id
WHERE (AP.date_ BETWEEN #checkIn AND #checkOut) AND
currency_id = #currency_id
ORDER BY AP.property_id;
And here is the output.
I want to filter the records with property_id = 62 as there is no price for 2021-06-01.
In the other words. How can I retrieve properties that have price for each date between the given dates?
Thanks in advance
As temporary solution I counted the number of days between the given dates (in this case 7). And I return the the records if the total number of rows per property was equal to 7.

SQL-Server Get data of each day that falls between two dates

I have a table with job schedules :
job_id [unique ID]
pref_start [date]
spec_duration [time in seconds]
I can calculate the end date from the preferred start and duration. The pref_start is not fixed, and can be changed at whim by the engineers.
I need to report activity in any given week, so if I have data similar to:
jid start end
J1 01/01/yyyy 15/02/yyyy
J2 07/01/yyyy 08/02/yyyy
J3 09/02/yyyy 21/03/yyyy
How would I query "tell me the job id's that occur on each day of the week 07/02/yyyy to 12/02/yyyy"
First find the matching intervals between your jobs and your filtering interval, then the amount of days for the filter interval and the overlapping intervals must match:
DECLARE #Jobs TABLE (
ID INT IDENTITY,
StartDate DATE,
EndDate DATE)
INSERT INTO #Jobs (
StartDate,
EndDate)
VALUES
('2019-01-01', '2019-02-15'),
('2019-01-07', '2019-02-08'),
('2019-02-09', '2019-03-21')
DECLARE #FilterStartDate DATE = '2019-02-07'
DECLARE #FilterEndDate DATE = '2019-02-12'
;WITH AtLeast1DayOverlappingJobs AS
(
SELECT
J.ID,
J.StartDate,
J.EndDate,
OverlappingStartDate = CASE
WHEN J.StartDate > #FilterStartDate THEN J.StartDate ELSE #FilterStartDate END, -- Highest of 2
OverlappingEndDate = CASE
WHEN J.EndDate < #FilterEndDate THEN J.EndDate ELSE #FilterEndDate END -- Lowest of 2
FROM
#Jobs AS J
WHERE
-- They share at least 1 day
#FilterStartDate <= J.EndDate AND #FilterEndDate >= J.StartDate
)
SELECT
T.*
FROM
AtLeast1DayOverlappingJobs AS T
WHERE
-- Amount of days must match between filter and overlapping periods
DATEDIFF(DAY, #FilterStartDate, #FilterEndDate) = DATEDIFF(DAY, T.OverlappingStartDate, T.OverlappingEndDate)
Results:
ID StartDate EndDate OverlappingStartDate OverlappingEndDate
1 2019-01-01 2019-02-15 2019-02-07 2019-02-12

Count number of occurrences of a day between two dates SQL Server

In SQL Server, how many 8 day periods do I have between this two dates?
#date1 = '2016/11/08'
#date2 = '2017/02/10'
Manually I have four 8 day periods between those dates
Declare #date1 date = '2016/11/08'
Declare #date2 date = '2017/02/10'
Select count(*)
From (
Select Top (DateDiff(DD,#date1,#date2)+1) D=cast(DateAdd(DD,Row_Number() Over (Order By (Select null))-1,#date1) as Date)
From master..spt_values n1, master..spt_values n2
) A
Where DatePart(DAY,D)=8
Returns
4
Here's an alternate approach using the standard SQL Date functions:
declare #StartDate datetime = '2016-11-08'
, #EndDate datetime = '2023-10-09' --'2017-02-10'
, #dayOfInterest int = 8 --in case you decided you were interested in a different date
--if you don't care whether or not start date is before or after end date; swap their values...
/*
select #StartDate = min(d)
, #EndDate = max(d)
from (select #StartDate d union select #EndDate) x
*/
select
case when #StartDate <= #EndDate then
--if the end date is after or on the start date, we count the number of eighths in between
datediff(month,#StartDate,#EndDate) --count the number of months between the two dates
+ case when datepart(dd,#StartDate)<=#dayOfInterest then 0 else -1 end --if our range excludes the nth this month, exclude it
+ case when datepart(dd,#EndDate)<#dayOfInterest then 0 else 1 end --if our range includes the nth on the last month, include it
else
0 --if the end date is before the start date; we return 0
end
CountTheDays --give the output column a name
Caution
NB: For the 8th this will work perfectly.
For dates after 28th it isn't reliable (you'd be better off going with John Cappelletti's solution), as my logic would add 1 day per month, regardless of whether that month contained that day (i.e. if counting the number of 30ths between 1st Jan and 1st March you'd get 2 instead of 1, as my logic assumes Feb 30th to be a valid date.

GROUP BY DAY, CUMULATIVE SUM

I have a table in MSSQL with the following structure:
PersonId
StartDate
EndDate
I need to be able to show the number of distinct people in the table within a date range or at a given date.
As an example i need to show on a daily basis the totals per day, e.g. if we have 2 entries on the 1st June, 3 on the 2nd June and 1 on the 3rd June the system should show the following result:
1st June: 2
2nd June: 5
3rd June: 6
If however e.g. on of the entries on the 2nd June also has an end date that is 2nd June then the 3rd June result would show just 5.
Would someone be able to assist with this.
Thanks
UPDATE
This is what i have so far which seems to work. Is there a better solution though as my solution only gets me employed figures. I also need unemployed on another column - unemployed would mean either no entry in the table or date not between and no other entry as employed.
CREATE TABLE #Temp(CountTotal int NOT NULL, CountDate datetime NOT NULL);
DECLARE #StartDT DATETIME
SET #StartDT = '2015-01-01 00:00:00'
WHILE #StartDT < '2015-08-31 00:00:00'
BEGIN
INSERT INTO #Temp(CountTotal, CountDate)
SELECT COUNT(DISTINCT PERSON.Id) AS CountTotal, #StartDT AS CountDate FROM PERSON
INNER JOIN DATA_INPUT_CHANGE_LOG ON PERSON.DataInputTypeId = DATA_INPUT_CHANGE_LOG.DataInputTypeId AND PERSON.Id = DATA_INPUT_CHANGE_LOG.DataItemId
LEFT OUTER JOIN PERSON_EMPLOYMENT ON PERSON.Id = PERSON_EMPLOYMENT.PersonId
WHERE PERSON.Id > 0 AND DATA_INPUT_CHANGE_LOG.Hidden = '0' AND DATA_INPUT_CHANGE_LOG.Approved = '1'
AND ((PERSON_EMPLOYMENT.StartDate <= DATEADD(MONTH,1,#StartDT) AND PERSON_EMPLOYMENT.EndDate IS NULL)
OR (#StartDT BETWEEN PERSON_EMPLOYMENT.StartDate AND PERSON_EMPLOYMENT.EndDate) AND PERSON_EMPLOYMENT.EndDate IS NOT NULL)
SET #StartDT = DATEADD(MONTH,1,#StartDT)
END
select * from #Temp
drop TABLE #Temp
You can use the following query. The cte part is to generate a set of serial dates between the start date and end date.
DECLARE #ViewStartDate DATETIME
DECLARE #ViewEndDate DATETIME
SET #ViewStartDate = '2015-01-01 00:00:00.000';
SET #ViewEndDate = '2015-02-25 00:00:00.000';
;WITH Dates([Date])
AS
(
SELECT #ViewStartDate
UNION ALL
SELECT DATEADD(DAY, 1,Date)
FROM Dates
WHERE DATEADD(DAY, 1,Date) <= #ViewEndDate
)
SELECT [Date], COUNT(*)
FROM Dates
LEFT JOIN PersonData ON Dates.Date >= PersonData.StartDate
AND Dates.Date <= PersonData.EndDate
GROUP By [Date]
Replace the PersonData with your table name
If startdate and enddate columns can be null, then you need to add
addditional conditions to the join
It assumes one person has only one record in the same date range
You could do this by creating data where every start date is a +1 event and end date is -1 and then calculate a running total on top of that.
For example if your data is something like this
PersonId StartDate EndDate
1 20150101 20150201
2 20150102 20150115
3 20150101
You first create a data set that looks like this:
EventDate ChangeValue
20150101 +2
20150102 +1
20150115 -1
20150201 -1
And if you use running total, you'll get this:
EventDate Total
2015-01-01 2
2015-01-02 3
2015-01-15 2
2015-02-01 1
You can get it with something like this:
select
p.eventdate,
sum(p.changevalue) over (order by p.eventdate asc) as total
from
(
select startdate as eventdate, sum(1) as changevalue from personnel group by startdate
union all
select enddate, sum(-1) from personnel where enddate is not null group by enddate
) p
order by p.eventdate asc
Having window function with sum() requires SQL Server 2012. If you're using older version, you can check other options for running totals.
My example in SQL Fiddle
If you have dates that don't have any events and you need to show those too, then the best option is probably to create a separate table of dates for the whole range you'll ever need, for example 1.1.2000 - 31.12.2099.
-- Edit --
To get count for a specific day, it's possible use the same logic, but just sum everything up to that day:
declare #eventdate date
set #eventdate = '20150117'
select
sum(p.changevalue)
from
(
select startdate as eventdate, 1 as changevalue from personnel
where startdate <= #eventdate
union all
select enddate, -1 from personnel
where enddate < #eventdate
) p
Hopefully this is ok, can't test since SQL Fiddle seems to be unavailable.

Resources