Performance function SQL Server - sql

I'm creating function to return me a number of worked minutes between two dates.
This returns me the exact number of minuts but when I use it on many records, the treatment is very long.
I have 3 functions:
CREATE FUNCTION FN_FERIES_SELON_ANNEE (#YEAR INT)
RETURNS #FERIES TABLE (JourId INT NOT NULL,
JourDate DATETIME NOT NULL,
JoURLabel VARCHAR(50) NULL)
AS
BEGIN
DECLARE #JoursFeries TABLE (
[JourId] [INT] IDENTITY(1,1) NOT NULL,
[JourDate] [DATETIME] NOT NULL,
[JoURLabel] [VARCHAR](50) NULL
)
DECLARE #an INT
DECLARE #G INT
DECLARE #I INT
DECLARE #J INT
DECLARE #C INT
DECLARE #H INT
DECLARE #L INT
DECLARE #JourPaque INT
DECLARE #MoisPaque INT
DECLARE #DimPaque DATETIME
DECLARE #LunPaque DATETIME
DECLARE #JeuAscension DATETIME
DECLARE #LunPentecote DATETIME
DECLARE #NouvelAn DATETIME
DECLARE #FeteTravail DATETIME
DECLARE #Armistice3945 DATETIME
DECLARE #Assomption DATETIME
DECLARE #Armistice1418 DATETIME
DECLARE #FeteNationale DATETIME
DECLARE #ToussaINT DATETIME
DECLARE #Noel DATETIME
SET #an = #YEAR
SET #G = #an % 19
SET #C = #an / 100
SET #H = (#C - #C / 4 - (8 * #C + 13) / 25 + 19 * #G + 15) % 30
SET #I = #H - (#H / 28) * (1 - (#H / 28) * (29 / (#H + 1)) * ((21 - #G) / 11))
SET #J = (#an + #an / 4 + #I + 2 - #C + #C / 4) % 7
SET #L = #I - #J
SET #MoisPaque = 3 + (#L + 40) / 44
SET #JourPaque = #L + 28 - 31 * (#MoisPaque / 4)
-- Jours fériés mobiles
SET #DimPaque = cast(cast(#an AS VARCHAR(4)) + '-'
+ cast(#MoisPaque AS VARCHAR(2)) + '-'
+ cast(#JourPaque AS VARCHAR(2)) AS DATETIME)
SET #LunPaque = DATEADD(DAY, 1, #DimPaque)
SET #JeuAscension = DATEADD(DAY, 39, #DimPaque)
SET #LunPentecote = DATEADD(DAY, 50, #DimPaque)
-- Jours fériés fixes
SET #NouvelAn = cast(cast(#an AS VARCHAR(4))+'-01-01 00:00:00' AS DATETIME)
SET #FeteTravail = cast(cast(#an AS VARCHAR(4))+'-05-01 00:00:00' AS DATETIME)
SET #Armistice3945 = cast(cast(#an AS VARCHAR(4))+'-05-08 00:00:00' AS DATETIME)
SET #Assomption = cast(cast(#an AS VARCHAR(4))+'-08-15 00:00:00' AS DATETIME)
SET #Armistice1418 = cast(cast(#an AS VARCHAR(4))+'-11-11 00:00:00' AS DATETIME)
SET #FeteNationale = cast(cast(#an AS VARCHAR(4))+'-07-14 00:00:00' AS DATETIME)
SET #ToussaINT = cast(cast(#an AS VARCHAR(4))+'-11-01 00:00:00' AS DATETIME)
SET #Noel = cast(cast(#an AS VARCHAR(4))+'-12-25 00:00:00' AS DATETIME)
INSERT INTO #JoursFeries (JourDate, JoURLabel)
SELECT #LunPaque, 'Lundi de Pâques'
UNION
SELECT #JeuAscension, 'Jeudi de l''Ascension'
UNION
SELECT #LunPentecote, 'Lundi de Pentecôte'
UNION
SELECT #NouvelAn, 'Nouvel an'
UNION
SELECT #FeteTravail, 'Fête du travail'
UNION
SELECT #Armistice3945, 'Armistice 39-45'
UNION
SELECT #Assomption, 'Assomption'
UNION
SELECT #FeteNationale, 'Fête Nationale'
UNION
SELECT #ToussaINT, 'Toussaint'
UNION
SELECT #Armistice1418, 'Armistice 14-18'
UNION
SELECT #Noel, 'Noël'
INSERT INTO #FERIES
SELECT * FROM #JoursFeries
RETURN
END
GO
CREATE FUNCTION FN_JOUR_TRAVAILLE (#Date1 DATETIME)
RETURNS INT AS
BEGIN
DECLARE #FLAG INT
SET #FLAG = 1
DECLARE #YEAR INT
SET #YEAR = DATEPART(YEAR, #DATE1)
IF EXISTS(SELECT * FROM FN_FERIES_SELON_ANNEE(#YEAR) WHERE JourDate = #Date1) BEGIN
SET #FLAG = 0
END
ELSE
IF DatePart(weekday, #DATE1) = 7 OR DatePart(weekday, #DATE1) = 1 BEGIN
SET #FLAG = 0
END
RETURN #FLAG
END
GO
CREATE FUNCTION FN_DATEDIFF_SELON_HORAIRES_ENTREPRISE2 (#Date1 DATETIME, #Date2 DATETIME)
RETURNS INT AS
BEGIN
DECLARE #NB_Jours INT
DECLARE #Cpt INT
DECLARE #Jours_Travailles INT
DECLARE #Date1_at_8_am DATETIME
DECLARE #Date2_at_6_pm DATETIME
DECLARE #Excedent INT
SET #NB_Jours = DATEDIFF(day, #Date1, #Date2) + 1
SET #Cpt = 0
SET #Jours_Travailles = 0
SET #Excedent = 0
SET #Date1_at_8_am = #Date1
SET #Date1_at_8_am = DATEADD(hour, - (DATEPART(hour, #Date1) - 8), #Date1_at_8_am)
SET #Date1_at_8_am = DATEADD(minute, - DATEPART(minute, #Date1_at_8_am), #Date1_at_8_am)
SET #Date1_at_8_am = DATEADD(second, - DATEPART(second, #Date1_at_8_am), #Date1_at_8_am)
SET #Date2_at_6_pm = #Date2
SET #Date2_at_6_pm = DATEADD(hour, 18 - DATEPART(hour, #Date2), #Date2_at_6_pm)
SET #Date2_at_6_pm = DATEADD(minute, - DATEPART(minute, #Date2_at_6_pm), #Date2_at_6_pm)
SET #Date2_at_6_pm = DATEADD(second, - DATEPART(second, #Date2_at_6_pm), #Date2_at_6_pm)
IF dbo.FN_JOUR_TRAVAILLE(#Date1) = 1 AND #Date1 > #Date1_at_8_am BEGIN
SET #Excedent = #Excedent + DATEDIFF(minute, #Date1_at_8_am, #Date1)
END
IF dbo.FN_JOUR_TRAVAILLE(#Date2) = 1 AND #Date2 < #Date2_at_6_pm BEGIN
SET #Excedent = #Excedent + DATEDIFF(minute, #Date2, #Date2_at_6_pm)
END
WHILE #Cpt < #NB_Jours
BEGIN
IF dbo.FN_JOUR_TRAVAILLE(DATEADD(day, #Cpt, #Date1)) = 1
BEGIN
SET #Jours_Travailles = #Jours_Travailles + 1
END
SET #Cpt = #Cpt + 1
END
RETURN #Jours_Travailles*600 - #Excedent
END
GO
-----------------------------------------------------------------------
-----------------------------------------------------------------------
-----------------------------------------------------------------------
The problem is in the function FN_JOUR_TRAVAILLE at this line:
IF EXISTS(SELECT * FROM FN_FERIES_SELON_ANNEE(#YEAR) WHERE JourDate = #Date1) BEGIN
SET #FLAG = 0
END
For each day of each period I calculate I generate the table of public holidays for the year.
The best solution would be to check if I did not already create this table, whether I search in it, if not I create it and I store it.
But I dont know how to do this.
I need to create a table in a superior scope and pass it as parameter in my functions I think.

How about a table type parameter:
create type FERIES as table (
JourId INT NOT NULL,
JourDate DATETIME NOT NULL,
JoURLabel VARCHAR(50) NULL
)
GO
In your outer scope use the FN_FERIES_SELON_ANNEE function to populate a table value:
declare #var FERIES;
insert into #var
select * from FN_FERIES_SELON_ANNEE(#YEAR)
and then pass it to your other function to use.
Your other function would become:
CREATE FUNCTION FN_JOUR_TRAVAILLE (#Date1 DATETIME, #table FERIES readonly)
RETURNS INT AS
BEGIN
DECLARE #FLAG INT
SET #FLAG = 1
DECLARE #YEAR INT
SET #YEAR = DATEPART(YEAR, #DATE1)
IF EXISTS(SELECT * FROM #table WHERE JourDate = #Date1) BEGIN
SET #FLAG = 0
END
ELSE
IF DatePart(weekday, #DATE1) = 7 OR DatePart(weekday, #DATE1) = 1 BEGIN
SET #FLAG = 0
END
RETURN #FLAG
END
GO

Try use XML -
DECLARE #XML XML
SELECT #XML = (
SELECT JourId, JourDate, JoURLabel
FROM #JoursFeries t
FOR XML AUTO
)
RETURN #XML
CREATE FUNCTION FN_JOUR_TRAVAILLE (#Date1 DATETIME, #XML XML)
RETURNS INT
AS BEGIN
DECLARE #YEAR INT
SELECT #YEAR = DATEPART(YEAR, #DATE1)
IF EXISTS(
SELECT 1
FROM #XML.nodes('/t') t(p)
WHERE t.p.value('#JourDate', 'DATETIME') = #Date1
) OR DATEPART(weekday, #DATE1) IN (1, 7)
BEGIN
RETURN 0
END
RETURN 1
END
UPDATE (2005 or higher):
Or try this solution -
ALTER FUNCTION FN_FERIES_SELON_ANNEE (#YEAR INT)
RETURNS XML
AS BEGIN
DECLARE
#an VARCHAR(4)
, #G INT, #I INT
, #J INT, #C INT
, #H INT, #L INT
, #JourPaque INT
, #MoisPaque INT
, #DimPaque DATETIME
SELECT #an = CAST(#YEAR AS VARCHAR(4))
SELECT
#G = #YEAR % 19
, #C = #YEAR / 100
, #H = (#C - #C / 4 - (8 * #C + 13) / 25 + 19 * #G + 15) % 30
, #I = #H - (#H / 28) * (1 - (#H / 28) * (29 / (#H + 1)) * ((21 - #G) / 11))
, #J = (#YEAR + #YEAR / 4 + #I + 2 - #C + #C / 4) % 7
, #L = #I - #J
, #MoisPaque = 3 + (#L + 40) / 44
, #JourPaque = #L + 28 - 31 * (#MoisPaque / 4)
, #DimPaque = CAST(#an + '-' + CAST(#MoisPaque AS VARCHAR(2)) + '-' + CAST(#JourPaque AS VARCHAR(2)) AS DATETIME)
DECLARE #XML XML
SELECT #XML = (
SELECT JourId, JourDate, JoURLabel
FROM (
SELECT JourId = 1, JourDate = DATEADD(DAY, 1, #DimPaque), JoURLabel = 'Lundi de Pâques'
UNION ALL
SELECT 2, DATEADD(DAY, 39, #DimPaque), 'Jeudi de l''Ascension'
UNION ALL
SELECT 3, DATEADD(DAY, 50, #DimPaque), 'Lundi de Pentecôte'
UNION ALL
SELECT 4, CAST(#an + '0101' AS DATETIME), 'Nouvel an'
UNION ALL
SELECT 5, CAST(#an + '0501' AS DATETIME), 'Fête du travail'
UNION ALL
SELECT 6, CAST(#an + '0508' AS DATETIME), 'Armistice 39-45'
UNION ALL
SELECT 7, CAST(#an + '0815' AS DATETIME), 'Assomption'
UNION ALL
SELECT 8, CAST(#an + '0714' AS DATETIME), 'Fête Nationale'
UNION ALL
SELECT 9, CAST(#an + '1101' AS DATETIME), 'Toussaint'
UNION ALL
SELECT 10, CAST(#an + '1111' AS DATETIME), 'Armistice 14-18'
UNION ALL
SELECT 11, CAST(#an + '1101' AS DATETIME), 'Noël'
) t
FOR XML AUTO
)
RETURN #XML
END
GO
ALTER FUNCTION FN_JOUR_TRAVAILLE (#Date1 DATETIME, #XML XML)
RETURNS INT
AS BEGIN
DECLARE #YEAR INT
SELECT #YEAR = DATEPART(YEAR, #DATE1)
IF EXISTS(
SELECT 1
FROM #XML.nodes('/t') t(p)
WHERE t.p.value('#JourDate', 'DATETIME') = #Date1
) OR DATEPART(weekday, #DATE1) IN (1, 7)
BEGIN
RETURN 0
END
RETURN 1
END
GO
ALTER FUNCTION FN_DATEDIFF_SELON_HORAIRES_ENTREPRISE2
(
#Date1 DATETIME
, #Date2 DATETIME
)
RETURNS INT AS
BEGIN
DECLARE
#NB_Jours INT
, #Cpt INT
, #Jours_Travailles INT
, #Date1_at_8_am DATETIME
, #Date2_at_6_pm DATETIME
, #Excedent INT
SELECT
#NB_Jours = DATEDIFF(DAY, #Date1, #Date2) + 1
, #Cpt = 0
, #Jours_Travailles = 0
, #Excedent = 0
, #Date1_at_8_am = DATEADD(HOUR, 8, CAST(FLOOR(CAST(#Date1 AS FLOAT)) AS DATETIME))
, #Date2_at_6_pm = DATEADD(HOUR, 18, CAST(FLOOR(CAST(#Date1 AS FLOAT)) AS DATETIME))
SELECT #Excedent = #Excedent +
CASE WHEN #Date1 > #Date1_at_8_am AND dbo.FN_JOUR_TRAVAILLE(#Date1, dbo.FN_FERIES_SELON_ANNEE(YEAR(#Date1))) = 1
THEN DATEDIFF(MINUTE, #Date1_at_8_am, #Date1)
ELSE 0
END +
CASE WHEN #Date2 < #Date2_at_6_pm AND dbo.FN_JOUR_TRAVAILLE(#Date2, dbo.FN_FERIES_SELON_ANNEE(YEAR(#Date2))) = 1
THEN DATEDIFF(MINUTE, #Date2, #Date2_at_6_pm)
ELSE 0
END
;WITH years AS
(
SELECT cont = 1, dt = DATEADD(DAY, #Cpt, #Date1), years = YEAR(DATEADD(DAY, #Cpt, #Date1))
UNION ALL
SELECT cont + 1, DATEADD(DAY, 1, dt), YEAR(DATEADD(DAY, 1, dt))
FROM years
WHERE cont < #NB_Jours
)
SELECT #Jours_Travailles = SUM(dbo.FN_JOUR_TRAVAILLE(dt, tt.xmls))
FROM years y
JOIN (
SELECT y3.years, xmls = dbo.FN_FERIES_SELON_ANNEE(y3.years)
FROM (
SELECT DISTINCT y2.years
FROM years y2
) y3
) tt ON tt.years = y.years
OPTION (MAXRECURSION 0)
RETURN #Jours_Travailles * 600 - #Excedent
END
UPDATE2 (2000):
There appear to be quite a big number of restrictions if using version 2000. Please try this example. I do not count it as a perfect decision to solve the problem without changing the current logic, but I think it should be helpful for you.
ALTER FUNCTION FN_FERIES_SELON_ANNEE (#YEAR INT)
RETURNS #FERIES TABLE
(
JourId INT NOT NULL
, JourDate DATETIME NOT NULL
, JoURLabel VARCHAR(50) NULL
)
AS BEGIN
DECLARE
#an VARCHAR(4)
, #G INT, #I INT
, #J INT, #C INT
, #H INT, #L INT
, #JourPaque INT
, #MoisPaque INT
, #DimPaque DATETIME
SELECT #an = CAST(#YEAR AS VARCHAR(4))
SELECT
#G = #YEAR % 19
, #C = #YEAR / 100
, #H = (#C - #C / 4 - (8 * #C + 13) / 25 + 19 * #G + 15) % 30
, #I = #H - (#H / 28) * (1 - (#H / 28) * (29 / (#H + 1)) * ((21 - #G) / 11))
, #J = (#YEAR + #YEAR / 4 + #I + 2 - #C + #C / 4) % 7
, #L = #I - #J
, #MoisPaque = 3 + (#L + 40) / 44
, #JourPaque = #L + 28 - 31 * (#MoisPaque / 4)
, #DimPaque = CAST(#an + '-' + CAST(#MoisPaque AS VARCHAR(2)) + '-' + CAST(#JourPaque AS VARCHAR(2)) AS DATETIME)
INSERT INTO #FERIES (JourId, JourDate, JoURLabel )
SELECT JourId, JourDate, JoURLabel
FROM (
SELECT JourId = 1, JourDate = DATEADD(DAY, 1, #DimPaque), JoURLabel = 'Lundi de Pâques'
UNION ALL
SELECT 2, DATEADD(DAY, 39, #DimPaque), 'Jeudi de l''Ascension'
UNION ALL
SELECT 3, DATEADD(DAY, 50, #DimPaque), 'Lundi de Pentecôte'
UNION ALL
SELECT 4, CAST(#an + '0101' AS DATETIME), 'Nouvel an'
UNION ALL
SELECT 5, CAST(#an + '0501' AS DATETIME), 'Fête du travail'
UNION ALL
SELECT 6, CAST(#an + '0508' AS DATETIME), 'Armistice 39-45'
UNION ALL
SELECT 7, CAST(#an + '0815' AS DATETIME), 'Assomption'
UNION ALL
SELECT 8, CAST(#an + '0714' AS DATETIME), 'Fête Nationale'
UNION ALL
SELECT 9, CAST(#an + '1101' AS DATETIME), 'Toussaint'
UNION ALL
SELECT 10, CAST(#an + '1111' AS DATETIME), 'Armistice 14-18'
UNION ALL
SELECT 11, CAST(#an + '1101' AS DATETIME), 'Noël'
) t
RETURN
END
GO
ALTER FUNCTION FN_DATEDIFF_SELON_HORAIRES_ENTREPRISE2
(
#Date1 DATETIME
, #Date2 DATETIME
)
RETURNS INT AS
BEGIN
DECLARE
#NB_Jours INT
, #Year INT
, #Jours_Travailles INT
, #Date1_at_8_am DATETIME
, #Date2_at_6_pm DATETIME
, #Excedent INT
SELECT
#NB_Jours = DATEDIFF(DAY, #Date1, #Date2) + 1
, #Jours_Travailles = 0
, #Excedent = 0
, #Date1_at_8_am = DATEADD(HOUR, 8, CAST(FLOOR(CAST(#Date1 AS FLOAT)) AS DATETIME))
, #Date2_at_6_pm = DATEADD(HOUR, 18, CAST(FLOOR(CAST(#Date1 AS FLOAT)) AS DATETIME))
DECLARE #emun TABLE (i BIGINT IDENTITY, blank BIT )
INSERT INTO #emun (blank)
SELECT NULL
FROM [master].dbo.spt_values n
CROSS JOIN (
SELECT i = 1
UNION ALL
SELECT 2
UNION ALL
SELECT 3
) b
DECLARE #temp TABLE (dt DATETIME)
INSERT INTO #temp (dt)
SELECT dt
FROM (
SELECT dt = #Date1
UNION ALL
SELECT DATEADD(DAY, i, #Date1)
FROM #emun
WHERE i < #NB_Jours
) d
DECLARE #temp2 TABLE
(
JourId INT NOT NULL
, JourDate DATETIME NOT NULL
, JoURLabel VARCHAR(50) NULL
)
DECLARE cur CURSOR FAST_FORWARD READ_ONLY LOCAL FOR
SELECT DISTINCT YEAR(y.dt)
FROM #temp y
OPEN cur
FETCH NEXT FROM cur INTO #Year
WHILE ##FETCH_STATUS = 0 BEGIN
INSERT INTO #temp2 (JourId, JourDate, JoURLabel)
SELECT JourId, JourDate, JoURLabel
FROM FN_FERIES_SELON_ANNEE(#Year)
FETCH NEXT FROM cur INTO #Year
END
CLOSE cur
DEALLOCATE cur
SELECT #Excedent = #Excedent +
CASE WHEN #Date1 > #Date1_at_8_am AND NOT EXISTS(SELECT 1 FROM #temp2 WHERE JourDate = #Date1 OR DATEPART(weekday, #Date1) IN (1,7))
THEN DATEDIFF(MINUTE, #Date1_at_8_am, #Date1)
ELSE 0
END +
CASE WHEN #Date2 < #Date2_at_6_pm AND NOT EXISTS(SELECT 1 FROM #temp2 WHERE JourDate = #Date2 OR DATEPART(weekday, #Date2) IN (1,7))
THEN DATEDIFF(MINUTE, #Date2, #Date2_at_6_pm)
ELSE 0
END
SELECT #Jours_Travailles = COUNT(1)
FROM #temp t
LEFT JOIN #temp2 t2 ON t.dt = t2.JourDate
WHERE NOT(t2.JourId IS NOT NULL OR DATEPART(weekday, t.dt) IN (1,7))
RETURN #Jours_Travailles * 600 - #Excedent
END

Related

Convert a string to date in SQL Server

I am facing a problem where I need to convert a string to date like
11 years 10 months 12 days to a date in SQL Server.
Please help any help would be appreciated.
I guess you want something like this
DECLARE #str VARCHAR(50)= '1 year 12 months 2 days'
DECLARE #days INT= LEFT(#str, Charindex(' ', #str)),
#months INT = Substring(#str, Charindex('months', #str) - 3, 2),
#years INT = Substring(#str, Charindex('days', #str) - 3, 2);
WITH days_back
AS (SELECT Dateadd(day, -#days, Cast(Getdate() AS DATE)) AS day_date),
month_back
AS (SELECT Dateadd(month, -#months, day_date) AS month_date
FROM days_back)
SELECT Result = Dateadd(year, -#years, month_date)
FROM month_back
declare #fuzzy_date varchar(255) = '11 years 10 months 12 days'
declare #date date
declare #startdate date = '1999-12-31'
declare #today_to_before date = getdate()
declare #today_to_after date = getdate()
declare #years int, #months int, #days int, #foo varchar(255)
Set #years = left(#fuzzy_date, charindex('years', #fuzzy_date) - 1)
Select #fuzzy_date = right(#fuzzy_date, len(#fuzzy_date) - charindex('years', #fuzzy_date) - 5)
select #months = left(#fuzzy_date, charindex('months', #fuzzy_date) - 1)
Select #days = replace(right(#fuzzy_date, len(#fuzzy_date) - charindex('months', #fuzzy_date) - 6), 'days', '')
Select #years, #months, #days
Set #date = dateadd(yy, #years, #startdate)
Set #date = dateadd(mm, #months, #date)
Set #date = dateadd(dd, #days, #date)
Set #today_to_after = dateadd(yy, #years, #today_to_after)
Set #today_to_after = dateadd(mm, #months, #today_to_after)
Set #today_to_after = dateadd(dd, #days, #today_to_after)
Set #today_to_before = dateadd(yy, -#years, #today_to_before)
Set #today_to_before = dateadd(mm, -#months, #today_to_before)
Set #today_to_before = dateadd(dd, -#days, #today_to_before)
Select #date,#today_to_after,#today_to_before

SQL Query - Select last date before timespan where no dates exists in timespan

In my table, I have a datetime column and a tag column. I would like to select the latest value before a timespan, if there are no values in the timespan. If there are values in the timespan, I would not like to return any values.
My query has the following input parameters:
#StartDate datetime
#EndDate datetime
The most important query results:
The query should not return any results if there is data between #StartDate and #EndDate
The query should return the latest value for each Tag before the #StartDate IF, and only if, there are no results between #StartDate and #EndDate
My issue, I have created two queries:
Returns the latest result before the timespan.
Returns all results IN the timespan.
The idea is to SELECT [Values before the timespan] WHERE NOT EXISTS IN [Values in the timespan].
I have tried to join these queries to get the end result, but this is where I struggle.
STEPS TO REPRODUCE (SETUP):
CREATE TABLE dbo.MyTable(id int IDENTITY(1,1) NOT NULL, Tag nvarchar(200) NOT NULL, StartTime datetime NOT NULL)
DECLARE #day int, #month int, #year int
SELECT #day = 15, #month = 1, #year = 2015
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MyTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MySuperTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
SELECT #day = 16, #month = 1, #year = 2015
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MyTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MySuperTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
SELECT #day = 18, #month = 1, #year = 2015
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MyTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MySuperTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
SELECT #day = 19, #month = 1, #year = 2015
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MyTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MySuperTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
SELECT #day = 26, #month = 1, #year = 2015
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MyTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
INSERT INTO dbo.MyTable(Tag, StartTime) VALUES('MySuperTag',dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1))
STEPS TO REPRODUCE (QUERY):
This should not return any values, since there are values in the timespan.
DECLARE #day int, #month int, #year int
DECLARE #StartTime datetime
DECLARE #EndTime datetime
SELECT #day = 17, #month = 1, #year = 2015
SET #StartTime = dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1)
SET #EndTime = dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1 + 3)
SELECT * FROM (SELECT id, Tag, StartTime FROM dbo.MyTable WHERE StartTime < #StartTime AND Tag NOT IN ( SELECT Tag FROM dbo.MyTable WHERE (StartTime > #StartTime AND StartTime < #EndTime))) as d WHERE EXISTS ( SELECT Tag, StartTime, ROW_NUMBER FROM ( SELECT Tag, StartTime, ROW_NUMBER() OVER(PARTITION BY Tag ORDER BY StartTime DESC) AS ROW_NUMBER FROM dbo.MyTable WHERE StartTime < #StartTime) AS b WHERE ROW_NUMBER = '1')
STEPS TO REPRODUCE (QUERY2):
This should produce the latest values before the timespan, since there are no values in the timespan.
SELECT #day = 21, #month = 1, #year = 2015
SET #StartTime = dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1)
SET #EndTime = dateadd(mm, (#year - 1900) * 12 + #month - 1 , #day - 1 + 3)
SELECT * FROM (SELECT id, Tag, StartTime FROM dbo.MyTable WHERE StartTime < #StartTime AND Tag NOT IN ( SELECT Tag FROM dbo.MyTable WHERE (StartTime > #StartTime AND StartTime < #EndTime))) as d WHERE EXISTS ( SELECT Tag, StartTime, ROW_NUMBER FROM ( SELECT Tag, StartTime, ROW_NUMBER() OVER(PARTITION BY Tag ORDER BY StartTime DESC) AS ROW_NUMBER FROM dbo.MyTable WHERE StartTime < #StartTime) AS b WHERE ROW_NUMBER = '1')
EDIT: Added "latest value for each Tag" in section about expected results.
Here is my revised answer for the modified question:
SELECT [A].* FROM [dbo].[MyTable] AS [A]
INNER JOIN (
SELECT [Tag], MAX([StartTime]) AS [StartTime]
FROM [dbo].[MyTable]
WHERE [StartTime] < #StartTime
GROUP BY [Tag]
) AS B ON ([A].[Tag] = [B].[Tag] AND [A].[StartTime] = [B].[StartTime])
WHERE
[A].[StartTime] < #StartTime AND
0 = (
SELECT COUNT(*)
FROM [dbo].[MyTable]
WHERE [StartTime] BETWEEN #StartTime AND #EndTime
)
;
The joined subquery works out the latest date for each tag before the #StartTime and joins back to it's self so that the full row can be returned (with the id).
The following should be roughly what you are looking for:
SELECT TOP 1 *
FROM [dbo].[MyTable]
WHERE
[StartTime] < #StartTime AND
0 = (
SELECT COUNT(*)
FROM [dbo].[MyTable]
WHERE [StartTime] BETWEEN #StartTime AND #EndTime
)
ORDER BY [StartTime] DESC;
The 0 = (SELECT COUNT(*) ...) bit causes the query to return no data when there is data between the #StartTime and #EndTime. The rest of the query is then just selecting the first row before the #StartTime.
SQL Fiddle
Comparison of query plans for 0 = (SELECT COUNT(*) ...) vs EXISTS(SELECT 1 ...)
SELECT *
FROM MyTable t
WHERE StartTime < #start
AND id =
(SELECT TOP 1 mt.id FROM MyTable mt WHERE mt.Tag = t.Tag ORDER BY StartTime DESC)
AND NOT EXISTS
(SELECT 1 FROM MyTable WHERE StartTime >= #start AND StartTime <= #end)
ORDER BY StartTime DESC;

Finding Closest future date

Is it possible to find the closest future date (datetime) by a date varchar value?
Given,
DECLARE #DayValue VARCHAR(3)
, #DateValue DATETIME
SET #DayValue = 'Tue' -- Values could be 'Mon', 'Tue', 'Wed' and etc.
SET #DateValue = '10/15/2014' -- Format is MM/dd/yyyy
I want to get:
Oct 21 2014 12:00AM
Using Loop,
DECLARE #DayValue VARCHAR(3)
,#DateValue DATETIME
SET #DayValue = 'tue'
SET #DateValue = '10/15/2014'
declare #i int= 1 ,#day varchar(3) = null
while (#i<=7 )
begin
Select #day = left(datename (dw,#DateValue),3)
if #day = #DayValue
begin
Select #DateValue
break
end
Select #DateValue = #DateValue+ 1
Select #i = #i+1
end
You could use this function if you had a date-table:
CREATE FUNCTION [dbo].[GetNextDayOfWeek]
( #DayOfWeek VARCHAR(3),
#DateValue datetime
)
RETURNS SmallDateTime
AS
BEGIN
DECLARE #NextDayOfWeek smalldatetime
SET #NextDayOfWeek = (
SELECT
MIN(d.Date)
FROM
tDefDate d
WHERE
d.Date > #DateValue
AND LEFT(DATENAME(Weekday, d.Date), 3) = #DayOfWeek);
RETURN #NextDayOfWeek
END
Then it's simple as:
select [dbo].[GetNextDayOfWeek]('Tue', Getdate()) -- next tuesday=> 2014-10-21
Note that it takes the language of the database into account. So if it's in german:
select [dbo].[GetNextDayOfWeek]('Die', Getdate()) -- next tuesday(Dienstag)
Here's a version that works also without a date-table (but is less efficient).
CREATE FUNCTION [dbo].[GetNextDayOfWeek]
( #DayOfWeek VARCHAR(3),
#DateValue datetime
)
RETURNS SmallDateTime
AS
BEGIN
DECLARE #NextDayOfWeek smalldatetime
;WITH CTE as
(
SELECT GetDate() DateValue, DayNum=0
UNION ALL
SELECT DateValue + 1, DayNum=DayNum+1
FROM CTE
WHERE DayNum <=7
)
SELECT #NextDayOfWeek = (
SELECT
MIN(d.DateValue)
FROM
CTE d
WHERE d.DateValue > #DateValue
AND LEFT(DATENAME(Weekday, d.DateValue), 3) = #DayOfWeek
)OPTION (MAXRECURSION 8);
RETURN #NextDayOfWeek
END
If you could define DayValue as an integer, you solve this problem with more elegant way:
DECLARE #DayValue int, #DateValue DATETIME 
SET #DayValue = 3 -- Values could be 1-Sun, 2-Mon, 3-Tue, 4-Wed and etc. 
SET #DateValue = '10/15/2014' -- Format is MM/dd/yyyy
select dateadd(day,(7 + #DayValue - datepart(w,#DateValue)), #DateValue)
TRY SQL FIDDLE DEMO
No loops and will work in selects with multiple rows. :)
DECLARE #DayValue CHAR(3)
DECLARE #DateValue DATETIME
DECLARE #FutureDate DATE
SET #DayValue='MON'
SET #DateValue='10/12/2014'
DECLARE #Days TABLE
(
[DayOfWeek] TINYINT,
[DayValue] CHAR(3)
)
INSERT INTO #Days([DayOfWeek],[DayValue])
SELECT 0,'SUN' UNION
SELECT 1,'MON' UNION
SELECT 2,'TUE' UNION
SELECT 3,'WED' UNION
SELECT 4,'THU' UNION
SELECT 5,'FRI' UNION
SELECT 6,'SAT'
SET #FutureDate=
DATEADD(DAY,
--Skip to next week if we are already on the desired day or past it
+ CASE WHEN ((SELECT [DayOfWeek] FROM #Days WHERE [DayValue]=#DayValue)<DATEPART(WEEKDAY,#DateValue)) THEN 7 ELSE 0 END
--reset to start of week (add one as DATEPART is base 1, not base 0)
- DATEPART(WEEKDAY,#DateValue) + 1
--Add the desired day of the week
+ (SELECT [DayOfWeek] FROM #Days WHERE [DayValue]=#DayValue)
,#DateValue)
SELECT #FutureDate
This is a bit chunky solution, but it works. :)
SET DATEFIRST 1
DECLARE #DateValue DateTime
, #DayValue VARCHAR(3)
, #tmp INT
SET #DateValue = '09/30/2014'
SET #DayValue = 'wed'
SET #tmp = CASE #DayValue
WHEN 'Mon' THEN (1 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Tue' THEN (2 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Wed' THEN (3 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Thu' THEN (4 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Fri' THEN (5 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Sat' THEN (6 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Sun' THEN (7 - DATEPART(dw, #DateValue) + 7) % 7
END
SELECT
CASE
WHEN #tmp = 0 THEN DATEADD (DAY, 7, #DateValue)
ELSE DATEADD (DAY, #tmp, #DateValue)
END

T-SQL get number of working days between 2 dates

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.
SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15'))
- (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)
How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.
Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.
Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2029-12-31';
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 12
AND DAY(dt) = 25
AND IsWorkDay = 1;
-- continue with other holidays, known company events, etc.
Now the query you're after is quite simple to write:
SELECT COUNT(*) FROM dbo.Calendar
WHERE dt >= '20130110'
AND dt < '20130115'
AND IsWorkDay = 1;
More info on calendar tables:
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
More info on generating sets without loops:
http://www.sqlperformance.com/tag/date-ranges
Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAY be sure you set your DATEFIRST setting appropriately...
For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.
The script i use for this is as follows (Note that i didnt write it # i forget where i found it)
SET DATEFIRST 1
SET NOCOUNT ON
GO
--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( #DATE DATETIME )
RETURNS INT
AS
BEGIN
DECLARE #ISOweek INT
SET #ISOweek = DATEPART(wk, #DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, #DATE) AS CHAR(4)) + '0104')
--Special cases: Jan 1-3 may belong to the previous year
IF ( #ISOweek = 0 )
SET #ISOweek = dbo.ISOweek(CAST(DATEPART(yy, #DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, #DATE) AS CHAR(2))) + 1
--Special case: Dec 29-31 may belong to the next year
IF ( ( DATEPART(mm, #DATE) = 12 )
AND ( ( DATEPART(dd, #DATE) - DATEPART(dw, #DATE) ) >= 28 )
)
SET #ISOweek = 1
RETURN(#ISOweek)
END
GO
--END ISOweek
--CREATE Easter algorithm function
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( #year INT )
RETURNS CHAR(8)
AS
BEGIN
-- Easter date algorithm of Delambre
DECLARE #A INT ,
#B INT ,
#C INT ,
#D INT ,
#E INT ,
#F INT ,
#G INT ,
#H INT ,
#I INT ,
#K INT ,
#L INT ,
#M INT ,
#O INT ,
#R INT
SET #A = #YEAR % 19
SET #B = #YEAR / 100
SET #C = #YEAR % 100
SET #D = #B / 4
SET #E = #B % 4
SET #F = ( #B + 8 ) / 25
SET #G = ( #B - #F + 1 ) / 3
SET #H = ( 19 * #A + #B - #D - #G + 15 ) % 30
SET #I = #C / 4
SET #K = #C % 4
SET #L = ( 32 + 2 * #E + 2 * #I - #H - #K ) % 7
SET #M = ( #A + 11 * #H + 22 * #L ) / 451
SET #O = 22 + #H + #L - 7 * #M
IF #O > 31
BEGIN
SET #R = #O - 31 + 400 + #YEAR * 10000
END
ELSE
BEGIN
SET #R = #O + 300 + #YEAR * 10000
END
RETURN #R
END
GO
--END fnDLA_GetEasterdate
--Create the table
CREATE TABLE MyDateTable
(
FullDate DATETIME NOT NULL
CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
Period INT ,
ISOWeek INT ,
WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
)
GO
--End table create
--Populate table with required dates
DECLARE #DateFrom DATETIME ,
#DateTo DATETIME ,
#Period INT
SET #DateFrom = CONVERT(DATETIME, '20000101')
--yyyymmdd (1st Jan 2000) amend as required
SET #DateTo = CONVERT(DATETIME, '20991231')
--yyyymmdd (31st Dec 2099) amend as required
WHILE #DateFrom <= #DateTo
BEGIN
SET #Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), #DateFrom, 112), 6))
INSERT MyDateTable
( FullDate ,
Period ,
ISOWeek
)
SELECT #DateFrom ,
#Period ,
dbo.ISOweek(#DateFrom)
SET #DateFrom = DATEADD(dd, +1, #DateFrom)
END
GO
--End population
/* Start of WorkingDays UPDATE */
UPDATE MyDateTable
SET WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
--Easter Monday
GO
UPDATE MyDateTable
SET WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 1
AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
GROUP BY DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
OR FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 8
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
ELSE FullDate
END
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 12
AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO
---------------------------------------WEEKENDS--------------------------------------
UPDATE MyDateTable
SET WorkingDay = 'N'
WHERE DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */
--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable
SET NOCOUNT OFF
Once you have created the table, finding the number of working days is easy peasy:
SELECT COUNT(FullDate) AS WorkingDays
FROM dbo.tbl_WorkingDays
WHERE WorkingDay = 'Y'
AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
AND FullDate < CONVERT(DATETIME, '15/01/2013', 103)
Note that this script includes UK bank holidays, i'm not sure what region you're in.
Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):
CREATE FUNCTION dbo.udf_GetBusinessDays (
#START_DATE DATE,
#END_DATE DATE
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #NUMBER_OF_DAYS INT = 0;
DECLARE #DAY_COUNTER INT = 0;
DECLARE #BUSINESS_DAYS INT = 0;
DECLARE #CURRENT_DATE DATE;
DECLARE #DAYNAME NVARCHAR(9)
SET #NUMBER_OF_DAYS = DATEDIFF(DAY, #START_DATE, #END_DATE);
WHILE #DAY_COUNTER <= #NUMBER_OF_DAYS
BEGIN
SET #CURRENT_DATE = DATEADD(DAY, #DAY_COUNTER, #START_DATE)
SET #DAYNAME = DATENAME(WEEKDAY, #CURRENT_DATE)
SET #DAY_COUNTER += 1
IF #DAYNAME = N'Saturday' OR #DAYNAME = N'Sunday'
BEGIN
CONTINUE
END
ELSE
BEGIN
SET #BUSINESS_DAYS += 1
END
END
RETURN #BUSINESS_DAYS
END
GO
This is the method I normally use (When not using a calendar table):
DECLARE #T TABLE (Date1 DATE, Date2 DATE);
INSERT #T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
If like I do you have a table with holidays in you can add this in too:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
AND NOT EXISTS
( SELECT 1
FROM HolidayTable ht
WHERE ht.Date = DATEADD(DAY, s.number, t.Date1)
)
) wd
The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM ( SELECT [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
FROM Master..spt_values s
CROSS JOIN Master..spt_values s2
) s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.
DECLARE #COUNTS int,
#STARTDATE date,
#ENDDATE date
SET #STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
SET #ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
SET #COUNTS=0
WHILE (#STARTDATE<=#ENDDATE)
BEGIN
/*Check for holidays*/
IF ( DATENAME(weekday,#STARTDATE)<>'Saturday' and DATENAME(weekday,#STARTDATE)<>'Sunday')
BEGIN
SET #COUNTS=#COUNTS+1
END
SET #STARTDATE=DATEADD(day,1,#STARTDATE)
END
/* Display the no of working days */
SELECT #COUNTS
By Combining #Aaron Bertrand's answer and the Easter Calculation from #HeavenCore's and adding some code of my own, this code creates a calendar from 2000 to 2049 that includes UK (England) Bank Holidays. Usage and notes as per Aaron's answer:
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2049-12-31';
-- Insert statements for procedure here
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 12 and
(DAY(dt) in (25,26) or
(DAY(dt) in (27, 28) and DATEPART(WEEKDAY, dt) IN (1,2)) );
-- New Year
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 1 AND
( DAY(dt) = 1 or (DAY(dt) IN (2,3) AND DATEPART(WEEKDAY, dt)=1 ));
-- Easter
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE dt = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Good Friday
OR dt = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Easter Monday
-- May Day (first Monday in May)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)<8;
-- Spring Bank Holiday (last Monday in May apart from 2022 when moved to include Platinum Jubilee bank holiday)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE
(YEAR(dt)=2022 and MONTH(dt) = 6 AND DAY(dt) IN (2,3)) OR
(YEAR(dt)<>2022 and MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24);
-- Summer Bank Holiday (last Monday in August)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 8 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24;

SQL query to find out experience of employee in the format 'yy years mm months dd days'

I want a query to find out experience of employee in the format 'yy years mm months dd days'.
SELECT EMPID, EMPNAME, DEPARTMENT, DESIGNATION, DATEDIFF(YEAR, DOJ, GETDATE()) AS EXPERIENCE,
EMPSTATUS AS JOB_STATUS
FROM EMPLOYEE
DOJ - field in db for saving 'date of joining' of employee.
This is the query which returns experience in years only. How to modify it?
SELECT
EMPID, EMPNAME, DEPARTMENT, DESIGNATION,
convert(varchar(3),DATEDIFF(MONTH, DOJ, GETDATE())/12) +' years '+
convert(varchar(2),DATEDIFF(MONTH, DOJ, GETDATE()) % 12)+ ' months'
AS EXPERIENCE,
EMPSTATUS AS JOB_STATUS
FROM EMPLOYEE
Consider Emp_joiningDate as column
SELECT DATEDIFF(year, Emp_joiningDate, GETDATE()) AS Years,
DATEDIFF(month, Emp_joiningDate, GETDATE()) - DATEDIFF(year, '1/2/1999', GETDATE()) * 12 AS Months,
DATEDIFF(day, Emp_joiningDate, getdate())- DATEDIFF(month, '1/2/1999',
GETDATE()) - DATEDIFF(year, '1/2/1999', GETDATE()) * 365 as Days
SQL Fiddle for testing
SELECT EMPID, EMPNAME, DEPARTMENT, DESIGNATION,
cast(floor(experience / 365) as varchar) + ' years ' +
cast(floor(experience % 365 / 30) as varchar) + ' months ' +
cast(experience % 30 as varchar) + ' days' as experience,
EMPSTATUS AS JOB_STATUS
FROM (select *, datediff(DAY, doj, getdate()) as experience
from employee) t
DECLARE #FromDate DATETIME = '2013-12-01 23:59:59.000',
#ToDate DATETIME = '2016-08-30 00:00:00.000',
DECLARE #MONTHS INT
SET #Months = DATEDIFF(MM,#FROMDATE,#TODATE)
IF (DATEPART(MM,#FromDate) <= (DATEPART(MM,#TODATE))+1)
BEGIN
SELECT CAST(DATEDIFF(YY,#FROMDATE,#TODATE) AS VARCHAR(5)) + ' Years '+ CAST( (DATEPART(MM,#TODATE)-DATEPART(MM,#FROMDATE)) AS VARCHAR(5))+1 +' Month'
END
ELSE
BEGIN
SELECT CAST(DATEDIFF(YY,#FROMDATE,#TODATE)-1 AS VARCHAR(5)) +' Years '+ CAST(12-(DATEPART(MM,#FROMDATE)) + DATEPART(MM,#TODATE)+1 AS VARCHAR(5) ) +' Month'
END
create FUNCTION [dbo].[fn_getEmployePeriod]
(
#dateofbirth DATETIME
)
RETURNS VARCHAR(100)
AS
BEGIN
DECLARE #currentdatetime DATETIME;
DECLARE #years INT;
DECLARE #months INT;
DECLARE #days INT;
DECLARE #currentMonthdays INT;
DECLARE #result VARCHAR(100);
SET #currentdatetime = GETDATE();--current datetime
IF ( #dateofbirth <= GETDATE() )
BEGIN
SELECT #years = DATEDIFF(YEAR, #dateofbirth, #currentdatetime); -- To find Years
SELECT #months = DATEDIFF(MONTH, #dateofbirth,
#currentdatetime) - ( DATEDIFF(YEAR,
#dateofbirth,
#currentdatetime)
* 12 );
SELECT #days = DATEPART(d, #currentdatetime) - DATEPART(d,
#dateofbirth);-- To Find Days
SELECT #currentMonthdays = ( SELECT DAY(DATEADD(DD, -1,
DATEADD(mm,
DATEDIFF(mm, 0,
GETDATE()), 0)))
);
IF ( #months < 0 )
BEGIN
SET #months = 12 + #months;
SET #years = #years - 1;
END;
IF ( #days < 0 )
BEGIN
SET #days = #currentMonthdays + #days;
IF(#months<>0)
begin
SET #months = #months - 1;
END
ELSE
BEGIN
SET #years = #years - 1;
SET #months = 11;
end
END;
-- To Find Months
SET #result = CAST(#years AS VARCHAR(3)) + ' years, '
+ CAST(#months AS VARCHAR(3)) + ' months, '
+ CAST(#days AS VARCHAR(3)) + ' days';
END;
ELSE
BEGIN
SET #result = 'Invaild date of birth';
END;
RETURN #result;
END;
SELECT convert(varchar(3),DATEDIFF(MONTH, '2015-01-01', GETDATE())/12) +' years '+
convert(varchar(2),DATEDIFF(DD, '2016-09-21', GETDATE()) % 12)+ ' months'
AS EXPERIENCE