SQL Server: calculate working time - sql-server

How I can calculate the working time in SQL Server between two datetime variables, excluding the holidays?
Any ideas?

Holidays aren't universal - they depends very much on your location. Not even the fact which days of the week are "working" days is the same - it depends on your location.
Because of that, a general, universal answer will not be possible, and for that reason, there's also no system-provided function in T-SQL for doing this. How would SQL Server know what holidays you have in your corner of the world??.
You need to have a table of your holidays somewhere in your system and handle it yourself.
Some posts that might be of some help to you:
Calculate Number of Working Days in SQL Server: this just basically removes any Saturdays and Sundays - but doesn't include other holidays
How do I count the number of business days between two dates? : shows the same main approach, with the addition of a table that contains other holidays like Easter, 4th of July (US National Holiday) and so on

Like marc_s says, you currently need a custom solution. I really hope Microsoft adds some standard functionality: it's tough to get right, and holidays are pretty much standardized by location.
Here's an example:
declare #start_date datetime
declare #end_date datetime
set #start_date = '2010-12-20'
set #end_date = '2010-12-26'
-- A table with all non-working days. This just adds Christmass, but you
-- probably should add weekends as well.
declare #non_working_days table (dt datetime)
insert #non_working_days values ('2010-12-25'), ('2010-12-26')
-- Remove the time part
set #start_date = DATEADD(D, 0, DATEDIFF(D, 0, #start_date))
set #end_date = DATEADD(D, 0, DATEDIFF(D, 0, #end_date))
-- Find the number of non-working-days
declare #nwd_count int
select #nwd_count = count(*)
from #non_working_days
where dt >= #start_date and dt < #end_date
-- Print result
select datediff(DAY, #start_date, #end_date) - #nwd_count
This prints 5, because the 25th is not a working day.

Have a table which has a row for every date you're interested in, and, say, a "working hours" column, or just a "working day" indicator if you want to do it at day granularity. (I find this approach makes the final SQL simpler, plus enables all sorts of other useful queries, but then I'm into data warehousing, rather than operational databases, so you may find the "just list the holidays" approach better, depending...)
You will, of course, have to create that table yourself, working from some feed of holiday dates for the region you're interested in.
Typically you can project these forward at least a year, as most public holidays are agreed a long way in advance (though there are some that pop up at the "last minute" -- in the UK, for example, 29 April will be an extra public holiday in 2010, as there's a royal wedding taking place, and we got less than a year's notice of that.
Then you just
SELECT
SUM(working_hours)
FROM
all_dates
WHERE
the_date BETWEEN #start_date AND #end_date
If you want to do this internationally, it gets incredibly difficult to get your data; there's no sensible source that I know of for international holiday dates, and different regions in a "country" might have different dates -- e.g. you may know that someone's in the United Kingdom, but unless you know if they're in Scotland or not, you won't know if the first two days of the year are a public holiday, or just the first...

Related

How to compare column values to declared variable

I was asked this interview question.
--Without modifying the following code:
DECLARE #StartDateInput SMALLDATETIME = '1/1/2018',
#EndDateInput SMALLDATETIME = '1/1/2018'
--Modify the following query so that it will return contacts modified at any time on January 1st, 2018
SELECT *
FROM dbo.Contacts
I tried the following query but this was not correct. I'm sure that I'm supposed to use the #EndDateInput variable as well but I wasn't sure how to use it. I don't think that this is the right way to approach this in general either.
SELECT *
FROM dbo.Contacts
WHERE ModifiedDate = SMALLDATETIME
It looks like the question is probing your understanding of date and datetime types, namely that a date with a time is after a date without a time (if there is even such a thing; most timeless dates are considered to be midnight on the relevant date, which is a time too.. in the same way that 1.0 is the same thing as 1, and 1.1 is after 1.0)
I'd use a range:
SELECT *
FROM dbo.Contacts
WHERE ModifiedDate >= #StartDateInput AND ModifiedDate < DATEADD(DAY, 1, #EndDateInput)
Why?
This caters for datetimes that have a time component.
It doesn't modify the row data (always a bad idea, e.g. to cast a million datetimes to a date just to strip the time off, every time you query - precludes using an index on the column and is a massive waste of resources) just to perform the query.
It converts the apparent "end date is inclusive" implied by both #variables being the same, to a form that allows the exclusive behavior of < to work inclusively (adds a day and then gets rows less than the following day, thereby including 23:59:59.999999 ...)
The only thing I would say is that strictly, the spec only calls for one day's records, which means it's not mandatory to use the #EndDateInput at all. It seems logical to use it, but it could be argued that if the spec is that this query will only ever return one day, the #End variable could be discarded and a DATEADD performed on the #Start instead
It is saying "any time" meaning consider the time component. With T-SQL the only reliable way is to use >= and < range query (exclusive upper range):
SELECT *
FROM dbo.Contacts
WHERE ModifiedDate >= #StartDateInput and
ModifiedDate < dateadd(d, 1, #EndDateInput);
PS: Initial declaration of #StartDateInput and #ENdDateInput is not robust and probably by chance pointing to Jan 1st, 2018. If it were '1/2/2018' then it would be ambiguous between Jan 2nd and Feb 1st. Better use ODBC canonical and\or ISO 8601 strings like '20180101'.

T-SQL create label “week #1: 1/1/18 - 1/7/18”

Is there a way to create a table with the following?
Label
“week #1: 1/1/18 - 1/7/18”
“week #2: 1/8/18 - 1/15/18”
And so forth?
Basically, I’m looking for the week number and the date range that week includes.
I think what you want as a starting point, is a "date dimension" or "calendar table". Here's one of many examples for creating them (creating them is not really the issue though, it's how you use them that's more important).
In your example, it looks like you want to pivot the data (create a crosstab). As a rule of thumb, you're generally better off pivoting on the client application, than you are persisting that denormalised anti-pattern in a relational database.
Here's a fictitious example:
DECLARE #start_date as datetime = '20180301';
DECLARE #end_date as datetime = dateadd(dd,datediff(dd,0,GETDATE()),0);--midnight last night
SELECT cal.week_starting --The date of the start of the week eg 15 April 2018.
,dateadd(d,6,cal.week_starting) as week_ending -- The date of the last day of the week eg 21 April 2018. You can cast as varchar, format and concatenate to the previous field to suit yourself.
,my_events.my_category
,count(*) as recs
FROM my.CALENDAR cal
JOIN dbo.big_list_of_events my_events ON cal.census_dttm = my_events.event_date
WHERE my_events.event_date >= #start_date
and my_events.event_date < #end_date
GROUP BY cal.week_starting
,my_events.my_category
ORDER BY cal.week_starting
,my_events.my_category
;
Once you get to this point you're ready to query it with your client application (eg Pivot Tables in Excel) and slice and dice to your heart's content. Again, you probably don't want data stored in your db as a crosstab.

TSQL Determine every other Friday from a "seed" date

Greetings StackOverflow Wizards.
SQL datetime calculations always give me trouble. I am trying to determine if an employee's hiredate fell between the last payday of that month and the first of the next month. (I.e. did they get a paycheck in their hire month.
Knowns:
I know our paydays are every other Friday.
I know 01/02/1970 was a Payday, and that date precedes the longest
active employee we have.
I know the hire date of each active employee (pulled from table).
I know (can calculate) the first of the month following the hire
date.
What I cannot seem to wrap my head around is how to use that seed date (01/02/1970) with datediff, dateadd, datepart, etc. to determine if there is a pay date between the hire date in question and the first of the following month.
In pseudo-code, here is what I'm trying to do:
declare #seedDate datetime = '01/02/1970' -- Friday - Payday seed date from which to calculate
declare #hireDate datetime = '09/26/2008' -- this date will actually be pulled from ServiceTotal table
declare #firstOfMonth datetime = DATEADD(MONTH, DATEDIFF(MONTH, 0, #hireDate) + 1, 0) -- the first of the month following #hireDate
declare #priorPayDate datetime -- calculate the friday payday immediately prior to #firstOfMonth
if #priorPayDate BETWEEN #hireDate AND #firstOfMonth
begin
-- do this
end
else
begin
-- do that
end
Using the hard-coded #hireDate above, and the #seedDate to determine every-other-Friday paydays, I know that there was a payday on 9/19/2008 and not another one until 10/03/2008, so the boolean above would be FALSE, and I will "do that" rather than "do this." How do I determine the value of #priorPayDate?
In all my databases where there is a lot going on with dates I create a table with colums for date,day, weekday,month,weeknr,dayof month, etc etc. I then use a procedural programming language or a bunch of handwritten sql to populate this table with every day for a large range of years say 1970 to 2200.
I pack this table 100% and index it heavily. You can then simply join any date to this table and do complex date stuff with simple where clause. So basically you pre calculate a helper table. maybe in you case you add a column to the date helper table with friday since seed column.
hope that makes sense.
Taking a DATEDIFF for days between your #seedDate and #firstOfMonth will give you a total number of days, which you can modulus by number of days between pay periods (14) to get number of days from the last pay period to the #firstOfMonth. You'll run into problems when the 1st is a payday (e.g. next month), which makes a CASE statement necessary:
DECLARE #priorPayDate DATETIME
SET #priorPayDate = CASE
WHEN DATEDIFF(dd, #seedDate, #firstOfMonth) % 14 = 0
THEN DATEADD(dd, -14, #firstOfMonth)
ELSE DATEADD(dd, -(DATEDIFF(dd, #seedDate, #firstOfMonth) % 14), #firstOfMonth)
END

tsql intesect between 2 datetime recordsets

I have an appointments table with appointments for a number of 'resources'
what i need to do is query that and return (for a particular resource) all free appointment slots across a date range.
i had thought the best way to approach this would be to generate a temp table of possible appointment times (as the length of appointment may be 30/60/90 minutes - the appointment length would be specified for the query.) and then select the intersect of those two recordsets. i.e. all of those - across the date range - where there are NOT appointments in the appointments table. thus returning all possible appointments for that resource.
or maybe just - again - generate the records of possible appointment datetimes, and then except the actual appointments already booked..?
unless of course someone can suggest an easier option.?
also not entirely sure how to generate the table of possibles ie a table with records for 2010-12-08 09:00, 2010-12-08 10:00, and so on (for 1 hr appointments)...
any ideas?
edit: have a vague idea on the possibles...
DECLARE #startDate DateTime
DECLARE #EndDate DateTime
set #startDate = '2010-12-08 09:00'
set #endDate = '2010-12-11 09:00';
with mycte as
(
select cast(#startDate as datetime) DateValue
union all
select dateadd(mi,30,DateValue)
from mycte
where DateValue <= #endDate
and datepart(hh, dateadd(mi,30,DateValue)) Between 9 AND 16
)
select DateValue
from mycte
This is a classic gaps and islands problem. It essentially a common problem where you need to identify missing values (gaps) in a sequence. Fortunately there is a free sample chapter on this very topic from the Manning book, SQL Server MVP Deep Dives. Hopefully it will provide inspiration as it gives guidance on a number of possible approaches.
http://www.manning.com/nielsen/SampleChapter5.pdf
Here is Itzik Ben-Gan's description of the problem, quoted from the above chapter.
Gaps and islands problems involve
missing values in a sequence
... The sequences involved can also
be temporal, such as order dates, some
of which are missing due to inactive
periods (weekends, holidays). Finding
periods of inactivity is an example of
the gaps problem, and finding periods
of activity is an example of the
islands problem.

TSQL Function to calculate 30 WORKING days Date from a Specified Date (SQL Server 2005)

TSQL Function to calculate 30 WORKING days Date from a Specified Date (SQL Server 2005)?
Input parameters would be Date and Number of Working Days.
Output would be the Calculated Date.
This would exclude Saturday, Sunday, Holidays and Day Holiday was observered.
i.e. If the Holiday falls on a weekend but it is observed on the Friday or Monday after the holiday.
For the Holidays we have a table with holiday and day it is being observed date.
Have a look at this article by Celko - in general you need to "pre-calculate" a calendar table to take in account all possible vagaries like Easter, bank holidays etc.
There's one right in the SQL online help if you scroll down to UDF to return the number of business days, including a check to a bank holidays table
you can tweak this.
Instead of writing a tsql function, it might easier if you build a table that's similar to the Date Dimension (DimDate) table in data warehouse. DimDate would contain a column named isHoliday. You can also add other columns that might be useful. Then you write a script to populate DimDate
Then you can run a query off it.
I don't have a table of holidays handy, so I haven't tested this very much - but as nobody else has attempted an answer, here's how I'd start:
declare #tempDate datetime,
#StartDate datetime,
#WorkingDays int,
#NonWorkingDays int,
#TargetDate datetime
set #StartDate = '2010-10-26' --Change this to a paramter
set #WorkingDays = 9 --Change this to a parameter
set #NonWorkingDays = -1
/*Work out the answer ignoring holidays */
set #tempDate = dateadd(d,#WorkingDays,#StartDate)
while (dateadd(d,#WorkingDays + #NonWorkingDays, #StartDate) < #tempDate)
begin
/*Work out how many holidays are in the interval we've worked out*/
select #NonWorkingDays = count(HolidayDate)
from Holidays
where HolidayDate between #StartDate and #tempDate;
/*Extend the interval to include the holidays we've just found*/
set #tempDate = dateadd(d,#NonWorkingDays,#tempDate)
/*See if #NonWorkingDays has changed with the new #tempDate*/
select #NonWorkingDays = count(HolidayDate)
from Holidays
where HolidayDate between #StartDate and #tempDate;
end
set #TargetDate = dateadd(d,#WorkingDays + #NonWorkingDays, #StartDate)
print 'Target Date: ' + cast(#TargetDate as varchar(50))
Note this only works for Holidays at the moment - not weekends. You'd have to load all weekends into the holiday table (or join to a weekends table or use the DATENAME function) but the calculation should be the same.
Not sure how your Holiday table handles duplicate dates (eg. Boxing Day and St Stephen's Day both fall on 26th Dec) so you might need to take account of that.

Resources