Checking availability on a booking table in SQL - tsql

I'm writing a job vacancy database for a bit of fun (and to try and learn T-SQL/SQL Server and this is what I have in my applications table so far.
application_id name interviewer location_id from to
-----------------------------------------------------------------------------------------------------------
1 Joe Bloggs Sarah Saunders 100 2008-12-25 00:00:00 2008-12-26 00:00:00
2 Barry White Issac Hayes 100 2008-12-29 00:00:00 2008-12-30 00:00:00
It's easy enough to find out what bookings have been made for these dates; a simple select statement would find these out easily enough.
The only problem I have now is how to figure out what days DON'T contain bookings. I'd like to do a search on the following table to see what dates are available in the room with location_id 100 between "2008-12-25 00:00:00" and "2008-12-30 00:00:00" and have it return that there is no interview being held in the room from the 27th to the 28th.
I'm sure this is painfully easy, but please lay some SQL wisdom on me.

You can generate a temporary table containing your days (either in the upper layer or with a stored function, which would be better if that is for SQL-learning purpose), then OUTER JOIN it on the booking table and filter on the record having NULL matching application_id.

One way could be to put the date range in a table variable and join.
declare #startDate datetime, #endDate datetime
SET #startDate = '2009-05-01'
SET #endDate = '2009-05-31'
declare #dates table (date datetime)
insert into #dates values (#startDate)
while #startDate < #endDate
begin
set #startDate = #startDate + 1
insert into #dates values (#startDate)
end
select d.* from applications a
left join #dates d on d.date between a.from and a.to
where a.application_id is null
Not tested, but something like that might work.

First, I would start by breaking down your query "2008-12-25 00:00:00" to "2008-12-30 00:00:00" into "time periods" of one day each. This is relatively easy with a table variable and a while loop so I won't go into it here.
Then you could loop through each of the time periods from the table variable and see if it overlaps any of the existing bookings (you would only pull out the bookings that overlap the query time period). To do that I suggest using this helper function:
CREATE FUNCTION [dbo].[fn_TimePeriodsOverlap]
(
#pStartTP1 datetime,
#pEndTP1 datetime,
#pStartTP2 datetime,
#pEndTP2 datetime
)
RETURNS bit
AS
BEGIN
DECLARE #Result bit
SET #Result = 0
IF #pStartTP1 >= #pStartTP2 AND #pStartTP1 < #pEndTP2
SET #Result = 1
ELSE IF #pEndTP1 >= #pStartTP2 AND #pEndTP1 < #pEndTP2
SET #Result = 1
ELSE IF #pStartTP2 >= #pStartTP1 AND #pStartTP2 < #pEndTP1
SET #Result = 1
ELSE IF #pEndTP2 >= #pStartTP1 AND #pEndTP2 < #pEndTP1
SET #Result = 1
RETURN #Result
END
That will return 1 if two time periods overlap, and 0 otherwise. This has the advantage of working even if the booking blocks aren't always a full day.

Related

Convert Excel formula (using Date and subtraction) into T-SQL

I am trying to write this Excel formula into T-SQL (to write a function).
Expected output is 0.71944444, but currently my output (using T-SQL) is 24.0000.
I am not sure why we have to add a day to same date and subtract the same date.
Bottom is a screenshot from Excel:
This is what I have so far in T-SQL:
CREATE FUNCTION [dbo].[fn_0921] (
#Punch_Start nvarchar(max)
)
RETURNS decimal(36, 8) AS
BEGIN
DECLARE #return_value nvarchar(max);
SET #return_value =
DATEDIFF(
MINUTE, CAST(#Punch_Start AS datetime2),
(
dateadd(
day, 1, CAST(#Punch_Start AS datetime2)
)
)
)
/ (60.0)
RETURN #return_value
END;
Thanks for help.
The Excel formula is returning the difference between the datetime in cell K4 & the start of the next day (i.e. 7/26/2021 00:00) as a fraction of a whole day. The following is the equivalent in T-SQL:
DECLARE #Punch_Start datetime2 = '7/25/2021 06:44';
SELECT DATEDIFF(
MINUTE,
#Punch_Start,
CAST(
CAST(
DATEADD(DAY, 1, #Punch_Start)
AS date) -- Add 1 day to #Punch_Start & cast as date to remove the time component - this is the start of the next day
AS datetime2) -- Cast back to datetime2 to get the difference in minutes
) / 1440.; -- Divide the difference in minutes by the number of minutes in a day (60 minutes per hour, 24 hours per day) to get the difference as a fraction of a day
This can probably help you:
DECLARE #date DATETIME2 = '2021-07-25 06:44'
DECLARE #seconds INT = DATEDIFF(second, CAST(#date AS date), #date)
DECLARE #secondsFromEnd FLOAT = 86400 - #seconds
SELECT #secondsFromEnd / 86400

How to identify invalid dates in postgres table field?

I have a table in PostgreSQL that has two date fields ( start and end ). There are many invalid dates both date fields like 0988-08-11,4987-09-11 etc.. Is there a simple query to identify them? The data type of the field is DATE. Thanks in advance.
Values in a date column ARE valid per definition. The year 0988 = 988 is a valid historic date as well as the year 4987 which is far in the future.
To filter out dates which are too historic or too far in the future you simply make this query:
SELECT
date_col
FROM
table
WHERE
date_col < /* <MINIMUM DATE> */
OR date_col > /* <MAXIMUM DATE> */
For date ranges (your minimum and maximum date) you could use the daterange functionality:
https://www.postgresql.org/docs/current/static/rangetypes.html
https://www.postgresql.org/docs/current/static/functions-range.html
Example table:
start_date end_date
2015-01-01 2017-01-01 -- valid
200-01-01 900-01-01 -- completely too early
3000-01-01 4000-01-01 -- completely too late
0200-01-01 2000-01-01 -- begin too early
2000-01-01 4000-01-01 -- end too late
200-01-01 4000-01-01 -- begin too early, end too late
Query:
SELECT
start_date,
end_date
FROM
dates
WHERE
daterange('1900-01-01', '2100-01-01') #> daterange(start_date, end_date)
Result:
start_date end_date
2015-01-01 2017-01-01
demo:db<>fiddle
Those are valid dates, but if you have business rules that state they are not valid for your purpose, you can delete them based on those rules:
For example, if you don't want any dates prior to 1900 or after 2999, this statement would delete the records with those dates:
DELETE FROM mytable
WHERE
start_date < '1900-01-01'::DATE OR
start_date >= '2999-01-01'::DATE OR
end_date < '1900-01-01'::DATE OR
end_date >= '2999-01-01'::DATE;
If you want to replace the dates with the lowest/highest acceptable dates instead of deleting the entire record, you could do something like this:
UPDATE mytable
SET
start_date = least('2999-01-01'::DATE, greatest('1900-01-01'::DATE, start_date)),
end_date = least('2999-01-01'::DATE, greatest('1900-01-01'::DATE, end_date))
WHERE
start_date < '1900-01-01'::DATE OR
start_date >= '2999-01-01'::DATE OR
end_date < '1900-01-01'::DATE OR
end_date >= '2999-01-01'::DATE;

Find total number in a specific period of time SQL

I am trying to find the total number of members in a given period. Say I have the following data:
member_id start_date end_date
1 9/1/2013 12/31/2013
2 10/1/2013 11/12/2013
3 12/1/2013 12/31/2013
4 5/1/2012 8/5/2013
5 9/1/2013 12/31/2013
6 7/1/2013 12/31/2013
7 6/6/2012 12/5/2013
8 10/1/2013 12/31/2013
9 7/8/2013 12/31/2013
10 1/1/2012 11/5/2013
In SQL I need to create a report that will list out the number of members in each month of the year. In this case something like the following:
Date Members Per Month
Jan-12 1
Feb-12 1
Mar-12 1
Apr-12 1
May-12 2
Jun-12 3
Jul-12 3
Aug-12 3
Sep-12 3
Oct-12 3
Nov-12 3
Dec-12 3
Jan-13 3
Feb-13 3
Mar-13 3
Apr-13 3
May-13 3
Jun-13 3
Jul-13 5
Aug-13 4
Sep-13 6
Oct-13 8
Nov-13 6
Dec-13 6
So there is only 1 member from Jan-12 (member id 10) until May-12 when member id 4 joins making the count 2 and so on.
The date range can be all over so I can't specify the specific dates but it is by month, meaning that even if someone ends 12-1 it is considered active for the month for Dec.
I was able to create the following stored procedure that was able to accomplish what I needed:
USE [ValueBasedSandbox]
GO
/****** Object: StoredProcedure [dbo].[sp_member_count_per_month] Script Date: 01/08/2015 12:02:37 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Create date: 2015-08-01
-- Description: Find the counts per a given date passed in
-- =============================================
CREATE PROCEDURE [dbo].[sp_member_count_per_month]
-- Add the parameters for the stored procedure here
#YEAR int
, #ENDYEAR int
AS
DECLARE #FIRSTDAYMONTH DATETIME
DECLARE #LASTDAYMONTH DATETIME
DECLARE #MONTH INT = 1;
--Drop the temporary holding table if exists
IF OBJECT_ID('tempdb.dbo.##TEMPCOUNTERTABLE', 'U') IS NOT NULL
DROP TABLE dbo.##TEMPCOUNTERTABLE
CREATE TABLE dbo.##TEMPCOUNTERTABLE (
counter INT
, start_date DATETIME2
, end_date DATETIME2
)
--Perform this loop for each year desired
WHILE #YEAR <= #ENDYEAR
BEGIN
--Perform for each month of the year
WHILE (#MONTH <= 12)
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET #FIRSTDAYMONTH = DATEADD(MONTH, #MONTH - 1, DATEADD(YEAR, #YEAR-1900, 0))
SET #LASTDAYMONTH = DATEADD(MONTH, #MONTH, DATEADD(YEAR, #YEAR-1900, 0)-1)
INSERT INTO dbo.##TEMPCOUNTERTABLE(counter, start_date, end_date)
SELECT COUNT(*) AS counter
, #FIRSTDAYMONTH AS start_date
, #LASTDAYMONTH AS end_date
FROM dbo.member_table
WHERE start_date <= #LASTDAYMONTH
AND end_date >= #FIRSTDAYMONTH
--Increment through all the months of the year
SET #MONTH = #MONTH + 1
END -- End Monthly Loop
--Reset Month counter
SET #MONTH = 1
--Increment the desired years
SET #YEAR = #YEAR + 1
END -- End Yearly Loop
--Display the results
SELECT *
FROM dbo.##TEMPCOUNTERTABLE
-- Drop the temp table
IF OBJECT_ID('tempdb.dbo.##TEMPCOUNTERTABLE', 'U') IS NOT NULL
DROP TABLE dbo.##TEMPCOUNTERTABLE
GO
This should do the trick
with datesCte(monthStart,monthEnd) as
(
select cast('20120101' as date) as monthStart, cast('20120131' as date) as monthEnd
union all
select DATEADD(MONTH, 1, d.monthStart), dateadd(day, -1, dateadd(month, 1, d.monthStart))
from datesCte as d
where d.monthStart < '20140101'
)
select *
from datesCte as d
cross apply
(
select count(*) as cnt
from dbo.MemberDates as m
where m.startDate <= d.monthEnd and m.endDate > d.monthStart
) as x
order by d.monthStart

Returning multiple months of data into one select

I have a question on SQL 2008 which is probably quite easy but I can't see the woods for the trees now.
I am trying to produce a sql based report detailing the last six months of helpdesk issue stats, per application, per office, per month which I then take into ssrs to apply prettiness :o)
Anyway - I have my script, which is fine on a month by month basis, for example;
SELECT distinct t.name_1 'Application',
(select distinct name from location where location_ref = c.location_ref) as office,
Count (t.name_1) as [Call Count],
datename(month, dateadd(month,-2,getdate()))+' '+datename(year, dateadd(month,-2,getdate())) as [Report Month]
FROM call_logging C
Inner Join problem_type t On t.ref_composite = c.ref_composite
AND c.resolve_time between onvert(datetime,convert(varchar,month(dateadd(m,-2,getdate()))) + '/01/' + convert(varchar,year(dateadd(m,-2,getdate()))))
and convert(datetime,convert(varchar,month(dateadd(m,-1,getdate()))) + '/01/' + convert(varchar,year(getdate())))
and c.resolve_group in ('48', '60')
which brings back all of May's issues.
The problem is that t.name_1 (the application in which the issue is for) is dynamic and the list grows or shrinks every month.
I basically need a layout of
APPLICATION OFFICE COUNT JUNE MAY APRIL MARCH FEB JAN
WORD LONDON 20 1 1 2 5 10 1
WORD PARIS 10 2 3 1 2 0 3
EXCEL MADRID 05 0 0 3 2 0 0
etc (if that makes sense on this layout!)
I've gone down the 6 separate reports road but it just doesn't look very nice in ssrs. I've thought about #tmptables but they don't like inserting distinct rows.
SELECT [C].[name_1] AS [APPLICATION]
,COUNT([name_1]) AS [CALL COUNT]
,[l].[location_ref]
,[dbo].[ufn_GetDateTime_CalenderYearMonth]([resolve_time]) AS [StartCalenderYearMonth]
FROM [call_logging] [C] INNER JOIN [problem_type] [t]
ON [t].[ref_composite] = [c].[ref_composite]
AND [c].[resolve_group] IN ('48', '60')
INNER JOIN [location] [l] ON [c].[location_ref] = [l].[location_ref]
WHERE [C].[resolve_time] BETWEEN '2011-01-01' AND GETDATE()
GROUP BY [C].[name_1], [l].[location_ref], [dbo].[ufn_GetDateTime_CalenderYearMonth]([resolve_time])
And the code for ufn_GetDateTime_CalenderYearMonth is:
CREATE FUNCTION [dbo].[ufn_GetDateTime_CalenderYearMonth] (#DateTime datetime)
RETURNS varchar(20)
AS
BEGIN
declare #dateString varchar(20)
declare #yearString varchar(10)
declare #monthString varchar(10)
set #yearString = cast( DATEPART(year, #DateTime) as varchar(10))
if(DATEPART(month, #DateTime) < 10)
set #monthString = '0' + cast( DATEPART(month, #DateTime) as varchar(5) )
else
set #monthString = cast( DATEPART(month, #DateTime) as varchar(5) )
set #dateString = #yearString + '-' + #monthString
RETURN (#dateString)
END
You just slap the resultset in a matrix and group everything by [StartCalenderYearMonth] and it will show numbers for each month from 1st of Jan 2011 till now..

T-SQL duration in hours:minutes:seconds

I have average duration between several dates (DATETIME format) ie. 1900-01-01 01:30.00.00.
How can I convert DATETIME to format hours:minutes:seconds where hours can be more that 24 - output format can be VARCHAR.
IE.
1 days 12 hours 5 minutes convert to 36:05:00
2 days 1 hour 10 minutes 5 seconds convert to 49:10:05
etc...
DECLARE #date1 DATETIME = '2011-08-03 13:30'
DECLARE #date2 DATETIME = '2011-08-03 13:00'
DECLARE #date3 DATETIME = '2011-08-03 14:00'
DECLARE #abc DATETIME = '2011-08-03 12:00'
select CAST(AVG(CAST(data-#abc as float)) as datetime)
from
(
select 'data' as label, #date1 as data
union all
select 'data' as label, #date2 as data
union all
select 'data' as label, #date3 as data
) as a
group by label
I would like to get result as 01:30:00 which means that average time is 1 hours and 30 minutes.
I tried it:
CONVERT(VARCHAR(10), CAST(AVG(CAST(data-#abc as float)) as datetime), 108)
but then I get only time portion in HH:MM:SS. When I set #abc = 2011-08-02 then the results will be the same - this is incorrect.
King regards,
Marcin
In T-SQL a datetime is precisely that, a date and a time where the hours can never exceed 24 because that moves it to the next day. You could use datepart to piece the datetime values out and treat them as integers and then rejoin them into the string you want. Depending on your final goal, you may be better of doing this type of work in your application or presentation layers where more general purpose languages often have more robust datetime libraries to work with.
I think you need to write a scalar-valued function that takes an integer argument (time difference in seconds) and format it as needed. For example,
CREATE FUNCTION intToDateTime ( #time_in_secs BIGINT) RETURNS VARCHAR(30)
AS
BEGIN
DECLARE #retval VARCHAR(30);
SET #retval = cast(#time_in_secs/(60*60) as varchar(10))+':'+
cast( (#time_in_secs-#time_in_secs/(60*60)*3600)/60 as varchar(10))+':'+
cast( (#time_in_secs-#time_in_secs/(60)*60) as varchar(10));
return #retval;
END
This function needs some changes - you may want to display leading zero for 0-9(i.g. '00' instead of '0' as this function currently does); also you need to handle negative values in a better way.
Now you can use it with DATEDIFF(second, #val1,#val2).
Hope I pointed you to the right direction.
select cast(cast(cast(t as float) *24 as int) as varchar) + right(convert(varchar,t, 20), 6)
from(
select cast(AVG(CAST(data-#abc as float)) as datetime) t
from
(
select 'data' as label, #date1 as data
union all
select 'data' as label, #date2 as data
union all
select 'data' as label, #date3 as data
) as a
group by label
) a
Result:
1:30:00
You can't convert datetime to handle non-real dates and times.
However, you can get an output that looks like a datetime, simply by concatenating an hours string with ':' with minutes, etc.
Lookup the DATEADD() and DATEDIFF() functions...