Remove holidays from datediff? - sql

So I've made the following code in order to remove weekends, however now I need to remove the holidays from the datediff part of the code:
Select
ii.initialDateFixed
,hh.holdDateFixed
,(datediff(dd,ii.initialDatefixed,hh.holdDatefixed) + 1)
-(DATEDIFF(wk,ii.initialDatefixed,hh.holdDatefixed)*2)
-(case when DATENAME(dw, ii.initialDatefixed) = 'Saturday' then 1 else 0 end)
-(case when DATENAME(dw, ii.initialDatefixed) = 'Sunday' then 1 else 0 end)
from tempTable
I have the following table on my db called dbo.holidays
holidayDate
description
2022-09-05 00:00:00.000
Labor Day
2022-11-24 00:00:00.000
Thanksgiving
Is there a way that I can use this table to remove the holidays in the datediff?
My main table called tempTable looks something like this:
ID
start date
end date
sadasfdas234134
2022-09-03 14:14:32.0000000
2022-09-16 14:14:32.0000000
dsf3245
2022-07-12 06:32:12.0000000
2022-07-19 12:12:24.0000000

Sure. You can use a correlated subquery for this. I'm not sure how the query you provided is running (i.e. what do the table aliases ii and hh point to?); I'm chalking that up to an incomplete redaction of your actual query. But here's a proof-of-concept that should get you what you need:
use tempdb;
go
drop table if exists #t;
create table #t (
ID varchar(40) not null,
start_date datetime2(0),
end_date datetime2(0)
);
insert into #t values
('sadasfdas234134', '2022-09-03 14:14:32', '2022-09-16 14:14:32'),
('dsf3245', '2022-07-12 06:32:12', '2022-07-19 12:12:24');
drop table if exists #holidays;
create table #holidays (
holidayDate date,
[description] varchar(100)
)
insert into #holidays values
('2022-09-05', 'Labor Day'),
('2022-11-24', 'Thanksgiving');
select ID,
[allDays] = datediff(day, start_date, end_date),
[nonHolidays] = datediff(day, start_date, end_date) -
(
select count(*)
from #holidays
where holidayDate between t.start_date and t.end_date
)
from #t as t;

Related

SQL - Insert all dates within range from table into another

I'm looking to populate a table with dates, based upon values contained within another.
Source : tblA
dtFrom dtTo
2019-01-01 2019-01-03
2019-02-01 2019-02-02
2019-03-01 2019-03-01
Destination : tblB
sDate
2019-01-01
2019-01-02
2019-01-03
2019-02-01
2019-02-02
2019-03-01
SQL Server 2014. As always, thanks in advance :-)
You can use a recursive CTE:
with dates as (
select dtfrom as dt, dtto
from tblA
union all
select dateadd(day, 1, dt), dtto
from dates
where dt < dtto
)
insert tblB (sDate)
select distinct dt
from dates;
The select distinct is only necessary to handle overlapping periods. If you know there are no overlaps, then don't use it.
You can use union to combine values from both columns into one rowset:
insert tblB
(sDate)
select distinct dt
from (
select dtFrom as dt
from tblA
union all
select dtTo
from tblA
) s
Use the always handy Calendar Table, which is a table that holds 1 row for each day, for all days between specific years. You can add additional columns like IsBusinessDay or WorkingStartHour / WorkingEndHour to make your date queries much easier.
-- Create Calendar Table
DECLARE #StartDate DATE = '2000-01-01'
DECLARE #EndDate DATE = '2050-01-01'
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
CREATE TABLE CalendarTable (
Date DATE PRIMARY KEY,
IsWorkingDay BIT
-- Other columns you might need
)
;WITH RecursiveCTE AS
(
SELECT
Date = #StartDate
UNION ALL
SELECT
Date = DATEADD(DAY, 1, R.Date)
FROM
RecursiveCTE AS R
WHERE
DATEADD(DAY, 1, R.Date) <= #EndDate
)
INSERT INTO CalendarTable (
Date,
IsWorkingDay)
SELECT
Date = R.Date,
IsWorkingDay = CASE WHEN DATEPART(WEEKDAY, R.Date) BETWEEN 1 AND 5 THEN 1 ELSE 0 END
FROM
RecursiveCTE AS R
OPTION
(MAXRECURSION 0)
Now with your calendar table, just join with a BETWEEN and INSERT to your destination table. You can use DISTINCT to make sure dates don't repeat:
INSERT INTO tblB (
sDate)
SELECT DISTINCT
sDate = C.Date
FROM
tlbA AS A
INNER JOIN CalendarTable AS C ON C.Date BETWEEN A.dtFrom AND A.dtTo
Let's say for example that you only want to insert records that are working days (monday to friday). You just need to filter the calendar table and done. You can add whichever logic you want on your table and just filter it when using, without repeating complex datetime logics.
INSERT INTO tblB (
sDate)
SELECT DISTINCT
sDate = C.Date
FROM
tlbA AS A
INNER JOIN CalendarTable AS C ON C.Date BETWEEN A.dtFrom AND A.dtTo
WHERE
C.IsWorkingDay = 1
With a Calendar you can inner join on the ranges to produce an Insert statement.
DECLARE #StartDate DATETIME = (SELECT MIN(dtFrom) FROM tblA)
DECLARE #EndDate DATETIME = (SELECT MAX(dtTo) FROM tblB)
;WITH Calendar as
(
SELECT CalendarDate = #StartDate, CalendarYear = DATEPART(YEAR, #StartDate), CalendarMonth = DATEPART(MONTH, #StartDate)
UNION ALL
SELECT CalendarDate = DATEADD(MONTH, 1, CalendarDate), CalendarYear = DATEPART(YEAR, CalendarDate), CalendarMonth = DATEPART(MONTH, CalendarDate)
FROM Calendar WHERE DATEADD (MONTH, 1, CalendarDate) <= #EndDate
)
INSERT INTO tblB
SELECT DISTINCT
C.CalendarDate
FROM
Calendar C
INNER JOIN tblA A ON C.CalendarDate BETWEEN A.dtFrom AND A.dtTo
You can achieve this result by using the below queries.
Steps 1 - Create a Custom Function which will take date range as a parameter and will return date series.
CREATE FUNCTION [dbo].[GenerateDateRange]
(#StartDate AS DATE,
#EndDate AS DATE,
#Interval AS INT
)
RETURNS #Dates TABLE(DateValue DATE)
AS
BEGIN
DECLARE #CUR_DATE DATE
SET #CUR_DATE = #StartDate
WHILE #CUR_DATE <= #EndDate BEGIN
INSERT INTO #Dates VALUES(#CUR_DATE)
SET #CUR_DATE = DATEADD(DAY, #Interval, #CUR_DATE)
END
RETURN;
END;
Step 2 - Join this custom function with your table tblA and insert the record in tblb as needed
insert tblb
select b.* from tblA a cross apply dbo.GenerateDateRange(a.dtFrom, a.dtTo, 1) b

Need to delete three consecutive days excluding weekends and holidays in sql

I have a requirement to delete rows from a table with three consecutive days (exclude the days if weekends come in between)
CREATE TABLE [dbo].[Test]
(
[Scanid] [bigint] NULL,
[Employeeid] [int] NULL,
[Datescanned] [datetime] NULL
)
INSERT INTO [dbo].[Test]([Scanid], [Employeeid], [Datescanned])
VALUES (108639, 3820, '2016-04-28 17:12:33.000'),
(108639, 3820, '2016-04-28 18:05:46.000'),
(108639, 3820, '2016-04-28 20:58:36.000'),
(999999, 3820, '2016-04-29 10:08:00.000'),
(999999, 3820, '2016-04-29 10:12:10.000'),
(777777, 3820, '2016-05-02 10:12:00.000'),
(111111, 3820, '2016-04-04 10:12:00.000'),
(33333, 3820, '2016-04-11 17:23:00.000'),
(987623, 3820, '2016-04-18 11:12:00.000'),
(1234, 3820, '2016-05-26 10:00:00.000'),
(5678, 3820, '2016-05-27 10:00:00.000'),
(8920, 3820, '2016-05-31 10:00:00.000')
Output:
Scanid Employeeid Datescanned
----------------------------------------
108639 3820 2016-04-28 17:12:33.000
108639 3820 2016-04-28 18:05:46.000
108639 3820 2016-04-28 20:58:36.000
999999 3820 2016-04-29 10:08:00.000
999999 3820 2016-04-29 10:12:10.000
777777 3820 2016-05-02 10:12:00.000
111111 3820 2016-04-04 10:12:00.000
33333 3820 2016-04-11 17:23:00.000
987623 3820 2016-04-18 11:12:00.000
1234 3820 2016-05-26 10:00:00.000
5678 3820 2016-05-27 10:00:00.000
8920 3820 2016-05-31 10:00:00.000
We can take date only from datescanned field and then in the above example we should delete rows with 3 consecutive date from '2016-04-28' to '2016-05-02' (2016-04-30 and 31 are weekends so we can ignore) and also delete rows with 3 consecutive date from '2016-05-26' to '2016-05-31' (2016-05-29 and 30th are weekends so we can ignore). so only results should display row with days 2016-04-04,2016-04-11,2016-04-18 which don't have 3 consecutive days before or after them.
Here is the exact output that you want..
I could see one mistake in your question,[ie. delete rows with 3 consecutive date from '2016-05-26' to '2016-05-31' (2016-05-29 and 30th are weekends so we can ignore)'],those weekends days are not correct..and the correct dates are 2016-05-28 and 2016-05-29.
DROP TABLE [TestDates]
GO
CREATE TABLE [dbo].[TestDates](
[Scanid] [bigint] NULL,
[Employeeid] [int] NULL,
[Datescanned] [datetime] NULL
)
INSERT INTO [dbo].[TestDates] ([Scanid] ,[Employeeid],[Datescanned])
VALUES (108639,3820,'2016-04-28 17:12:33.000'),(108639,3820,'2016-04-28 18:05:46.000'),
(108639,3820,'2016-04-28 20:58:36.000'),(999999,3820,'2016-04-29 10:08:00.000'),
(999999,3820,'2016-04-29 10:12:10.000'),(777777,3820,'2016-05-02 10:12:00.000'),
(111111,3820,'2016-04-04 10:12:00.000'),(33333,3820,'2016-04-11 17:23:00.000'),
(987623,3820,'2016-04-18 11:12:00.000'),(1234,3820,'2016-05-26 10:00:00.000'),
(5678,3820,'2016-05-27 10:00:00.00'), (8920, 3820, '2016-05-30 10:00:00.000')
GO
DROP TABLE #t
GO
SELECT DISTINCT Employeeid,CONVERT(date,Datescanned) Datescanned INTO #T
FROM [TestDates]
GO
;WITH cte_cnt
AS
(
SELECT Employeeid, MIN(Datescanned) AS FROM_DATE
,MAX(Datescanned) AS TO_DATE
, COUNT('A') AS DayDiff
FROM (
SELECT Employeeid,Datescanned,
ROW_NUMBER() OVER(ORDER BY Datescanned) AS ROW_NUMBER,
DATEDIFF(D, ROW_NUMBER() OVER(ORDER BY Datescanned)
,CASE WHEN DATENAME(dw, cast (Datescanned as datetime)-1) = 'Sunday' THEN DATEADD(DAY, -2, Datescanned) ELSE Datescanned END) AS Diff
FROM #t) AS dt
GROUP BY Employeeid, Diff )
DELETE t
--SELECT *
FROM cte_cnt c
JOIN [TestDates] t
ON c.Employeeid=t.Employeeid
WHERE CAST(t.Datescanned as DATE) BETWEEN c.FROM_DATE AND c.TO_DATE and c.DayDiff=3
GO
SELECT *
FROM [TestDates]
GO
A solution which doesn't account for holidays would be
SELECT
t.*
FROM (SELECT DISTINCT
CAST(t1.datescanned AS date) first_date,
CAST(t2.datescanned AS date) second_date,
CAST(t3.datescanned AS date) third_date
FROM test t1
JOIN test t2 --add a join condition for employeeid as well
ON DATEDIFF(dd, CAST(t1.datescanned AS date), CAST(t2.datescanned AS date)) = 1
OR (DATEPART(WEEKDAY, CAST(t2.datescanned AS date)) = 2
AND DATEDIFF(dd, CAST(t1.datescanned AS date), CAST(t2.datescanned AS date)) = 3)
JOIN test t3 --add a join condition for employeeid as well
ON DATEDIFF(dd, CAST(t2.datescanned AS date), CAST(t3.datescanned AS date)) = 1
OR (DATEPART(WEEKDAY, CAST(t3.datescanned AS date)) = 2
AND DATEDIFF(dd, CAST(t2.datescanned AS date), CAST(t3.datescanned AS date)) = 3)
) x
JOIN test t
ON CAST(t.datescanned AS date) = x.first_date
OR CAST(t.datescanned AS date) = x.second_date
OR CAST(t.datescanned AS date) = x.third_date
Self join the table twice, each time on
a date difference of 1 or
3 when a weekend occurs and check if the weekday is Monday (weekday=2)
Sample demo
The result gives the rows which need to be deleted. But the caveat here is that, this would give you more than 3 consecutive days if there are no gaps. In that case you need to explain if you want to stop deleting at the 3rd day.
The previous scripts are good. I would make an improvement and for weekend and Holiday check, add a function and call it in the select statement.
Here is a simple function that you can use (I am assuming that you have a table called Holiday, which holds all holiday dates per State )
Create FUNCTION [dbo].[IsHolidayOrWeekend]
(
#date DateTime,
#stateId int
)
RETURNS Bit
AS
BEGIN
declare #dayOfWeek VARCHAR(9);
set #dayOfWeek = DATEName(DW, #date);
IF(#dayOfWeek = 'Saturday' OR #dayOfWeek = 'Sunday')
RETURN 1;
ELSE
begin
set #date = cast(#date as date) -- Remove the time portion
RETURN IsNull((SELECT 1 from Holiday where StateId = #provinceId and HolidayDate = #date ), 0)
end;
END
Perhaps this?:
delete from Test
where not exists (
select 1
from Test t2
where cast(t2.Datescanned as date)
between
dateadd(day,
case datepart(dayofweek, cast(Test.Datescanned as date))
when 1 then -4 when 2 then -4
else -2
end,
cast(Test.Datescanned as date)
)
and
dateadd(day,
case datepart(dayofweek, cast(Test.Datescanned as date))
when 4 then 4 when 5 then 4
else 2
end,
cast(Test.Datescanned as date)
)
)
I have an added requirement to igonre holidays also along with weekend. I cretaed Test_calendar table removing all holidays and weekends and assigned row number to active days. Then this is the code I used. It working but may not be the fast if we have millions of rows. For me data is small so it will get the work done. Please let me know if you can simplify deletion process to make it fast.
SELECT distinct scanid as [badgeid] , Employeeid,CONVERT(date, Datescanned) as Datescanned,RN
into #test1
FROM [dbo].[Test] a
inner join Test_Calendar b
on CONVERT(date, a.Datescanned)=b.Cal_date
order by Datescanned asc
declare #min int
declare #max int
declare #i int
select #min=MIN(rn) from #test1
select #max=Max(rn) from #test1
while(#min<#max)
begin
select #i=COUNT(*) from #test1 where rn in(#min ,#min+1,#min+2)
if(#i=3)
select * from #test1 where rn in(#min ,#min+1,#min+2)
set #min=#min+1
end

How to get the summation of days in specific month of year in range

If i have Vacation table with the following structure :
emp_num start_date end_date
234 8-2-2015 8-5-2015
234 6-28-2015 7-1-2015
234 8-29-2015 9-2-2015
115 6-7-2015 6-7-2015
115 8-7-2015 8-10-2015
considering date format is: m/dd/yyyy
How could i get the summation of vacations for every employee during specific month .
Say i want to get the vacations in 8Aug-2015
I want the result like this
emp_num sum
234 7
115 4
7 = all days between 8-2-2015 and 8-5-2015 plus all days between 8-29-2015 AND 8-31-2015 the end of the month
i hope this will help you
declare #temp table
(emp_num int, startdate date, enddate date)
insert into #temp values (234,'8-2-2015','8-5-2015')
insert into #temp values (234,'6-28-2015','7-1-2015')
insert into #temp values (234,'8-29-2015','9-2-2015')
insert into #temp values (115,'6-7-2015','6-7-2015')
insert into #temp values (115,'8-7-2015','8-10-2015')
-- i am passing 8 as month number in your case is August
select emp_num,
SUM(
DATEDIFF (DAY , startdate,
case when MONTH(enddate) = 8
then enddate
else DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,startdate)+1,0))--end date of month
end
)+1) AS Vacation from #temp
where (month(startdate) = 8 OR month(enddate) = 8) AND (Year(enddate)=2015 AND Year(enddate)=2015)
group by emp_num
UPDATE after valid comment: This will fail with these dates: 2015-07-01, 2015-09-30 –#t-clausen.dk
i was assumed OP wants for month only which he will pass
declare #temp table
(emp_num int, startdate date, enddate date)
insert into #temp values (234,'8-2-2015','8-5-2015')
insert into #temp values (234,'6-28-2015','7-1-2015')
insert into #temp values (234,'8-29-2015','9-2-2015')
insert into #temp values (115,'6-7-2015','6-7-2015')
insert into #temp values (115,'8-7-2015','8-10-2015')
insert into #temp values (116,'07-01-2015','9-30-2015')
select emp_num,
SUM(
DATEDIFF (DAY , startdate,
case when MONTH(enddate) = 8
then enddate
else DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,startdate)+1,0))
end
)+1) AS Vacation from #temp
where (Year(enddate)=2015 AND Year(enddate)=2015)
AND 8 between MONTH(startdate) AND MONTH(enddate)
group by emp_num
This will work for sqlserver 2012+
DECLARE #t table
(emp_num int, start_date date, end_date date)
INSERT #t values
( 234, '8-2-2015' , '8-5-2015'),
( 234, '6-28-2015', '7-1-2015'),
( 234, '8-29-2015', '9-2-2015'),
( 115, '6-7-2015' , '6-7-2015'),
( 115, '8-7-2015' , '8-10-2015')
DECLARE #date date = '2015-08-01'
SELECT
emp_num,
SUM(DATEDIFF(day,
CASE WHEN #date > start_date THEN #date ELSE start_date END,
CASE WHEN EOMONTH(#date) < end_date
THEN EOMONTH(#date)
ELSE end_date END)+1) [sum]
FROM #t
WHERE
start_date <= EOMONTH(#date)
and end_date >= #date
GROUP BY emp_num
Using a Tally Table:
SQL Fiddle
DECLARE #month INT,
#year INT
SELECT #month = 8, #year = 2015
--SELECT
-- DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0)) AS start_day,
-- DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0)) AS end_d
;WITH CteVacation AS(
SELECT
emp_num,
start_date = CONVERT(DATE, start_date, 101),
end_date = CONVERT(DATE, end_date, 101)
FROM vacation
)
,E1(N) AS(
SELECT * FROM(VALUES
(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
)t(N)
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b),
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b),
Tally(N) AS(
SELECT TOP(SELECT MAX(DATEDIFF(DAY, start_date, end_date)) FROM vacation)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM E4
)
SELECT
v.emp_num,
COUNT(*)
FROM CteVacation v
CROSS JOIN Tally t
WHERE
DATEADD(DAY, t.N - 1, v.start_date) <= v.end_date
AND DATEADD(DAY, t.N - 1, v.start_date) >= DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0))
AND DATEADD(DAY, t.N - 1, v.start_date) < DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0))
GROUP BY v.emp_num
First, you want to use the correct data type to ease your calculation. In my solution, I used a CTE to format your data type. Then build a tally table from 1 up to the max duration of the all the vacations. Using that tally table, do a CROSS JOIN on the vacation table to generate all vacation dates from its start_date up to end_date.
After that, add a WHERE clause to filter dates that falls on the passed month-year parameter.
Here, #month and #year is declared as INT. What you want is to get all dates from the first day of the month-year up to its last day. The formula for first day of the month is:
DATEADD(MONTH, #month - 1, DATEADD(YEAR, #year - 1900, 0))
And for the last day of the month, add one month to the above and just use <:
DATEADD(MONTH, #month, DATEADD(YEAR, #year - 1900, 0))
Some common date routines.
More explanation on tally table.
Select(emp_name,start_date,end_date) AS sum_day from table_Name Group by emp_num,start_date,end_date
Try this
with cte(
Select emp_num,DATEDIFF(day,start_date,end_date) AS sum_day from table_Name
Group by emp_num,start_date,end_date
)
Select emp_num,sum(sum_day) as sum_day from cte group by emp_num

SQL populate total working days per month minus bank holidays for current financial year

I am after a view which will look like my first attached picture however with right hand column populated and not blank. The logic is as follows:
The data must be for current financial period. Therfore April will be 2011 and March will be 2012 and so on.
The calculation for Days Available for the single months will be:
Total number of working days (Monday-Friday) minus any bank holidays that fall into that particular month, for that particular financial year (Which we have saved in a table - see second image).
Column names for holiday table left to right: holidaytypeid, name, holstart, holend.
Table name: holidaytable
To work out the cumulative months 'Days Available' it will be a case of summing already populated data for the single months. E.g April-May will be April and May's data SUMMED and so on and so forth.
I need the SQL query in perfect format so that this can be pasted straight in and will work (i.e with the correct column names and table names)
Thanks for looking.
DECLARE #StartDate DATETIME, #EndDate DATETIME
SELECT #StartDate = '01/04/2011',
#EndDate = '31/03/2012'
CREATE TABLE #Data (FirstDay DATETIME NOT NULL PRIMARY KEY, WorkingDays INT NOT NULL)
;WITH DaysCTE ([Date]) AS
( SELECT #StartDate
UNION ALL
SELECT DATEADD(DAY, 1, [Date])
FROM DaysCTE
WHERE [Date] <= #Enddate
)
INSERT INTO #Data
SELECT MIN([Date]),
COUNT(*) [Day]
FROM DaysCTE
LEFT JOIN HolidayTable
ON [Date] BETWEEN HolStart AND HolEnd
WHERE HolidayTypeID IS NULL
AND DATENAME(WEEKDAY, [Date]) NOT IN ('Saturday', 'Sunday')
GROUP BY DATEPART(MONTH, [Date]), DATEPART(YEAR, [Date])
OPTION (MAXRECURSION 366)
DECLARE #Date DATETIME
SET #Date = (SELECT MIN(FirstDay) FROM #Data)
SELECT Period,
WorkingDays [Days Available (Minus the Holidays)]
FROM ( SELECT DATENAME(MONTH, Firstday) [Period],
WorkingDays,
0 [SortField],
FirstDay
FROM #Data
UNION
SELECT DATENAME(MONTH, #Date) + ' - ' + DATENAME(MONTH, Firstday),
( SELECT SUM(WorkingDays)
FROM #Data b
WHERE b.FirstDay <= a.FirstDay
) [WorkingDays],
1 [SortField],
FirstDay
FROM #Data a
WHERE FirstDay > #Date
) data
ORDER BY SortField, FirstDay
DROP TABLE #Data
If you do this for more than 1 year you will need to change the line:
OPTION (MAXRECURSION 366)
Otherwise you'll get an error - The number needs to be higher than the number of days you are querying.
EDIT
I have just come accross this old answer of mine and really don't like it, there are so many things that I now consider bad practise, so am going to correct all the issues:
I did not terminate statements with a semi colon properly
Used a recursive CTE to generate a list of dates
Generate a set or sequence without loops – part 1
Generate a set or sequence without loops – part 2
Generate a set or sequence without loops – part 3
Did not include the column list for an insert
Used DATENAME to elimiate weekends, which is language specific, much better to explicitly set DATEFIRST and use DATEPART
Used LEFT JOIN/IS NULL instead of NOT EXISTS to elimiate records from the holiday table. In SQL Server LEFT JOIN/IS NULL is less efficient than NOT EXISTS
These are all minor things, but they are things I would critique (at least in my head if not outloud) when reviewing someone else's query, so can't really not correct my own work! Rewriting the query would give.
SET DATEFIRST 1;
DECLARE #StartDate DATETIME = '20110401',
#EndDate DATETIME = '20120331';
CREATE TABLE #Data (FirstDay DATETIME NOT NULL PRIMARY KEY, WorkingDays INT NOT NULL);
WITH DaysCTE ([Date]) AS
( SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate) + 1)
DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #StartDate)
FROM sys.all_objects a
)
INSERT INTO #Data (FirstDay, WorkingDays)
SELECT FirstDay = MIN([Date]),
WorkingDays = COUNT(*)
FROM DaysCTE d
WHERE DATEPART(WEEKDAY, [Date]) NOT IN (6, 7)
AND NOT EXISTS
( SELECT 1
FROM dbo.HolidayTable ht
WHERE d.[Date] BETWEEN ht.HolStart AND ht.HolEnd
)
GROUP BY DATEPART(MONTH, [Date]), DATEPART(YEAR, [Date]);
DECLARE #Date DATETIME = (SELECT MIN(FirstDay) FROM #Data);
SELECT Period,
[Days Available (Minus the Holidays)] = WorkingDays
FROM ( SELECT DATENAME(MONTH, Firstday) [Period],
WorkingDays,
0 [SortField],
FirstDay
FROM #Data
UNION
SELECT DATENAME(MONTH, #Date) + ' - ' + DATENAME(MONTH, Firstday),
( SELECT SUM(WorkingDays)
FROM #Data b
WHERE b.FirstDay <= a.FirstDay
) [WorkingDays],
1 [SortField],
FirstDay
FROM #Data a
WHERE FirstDay > #Date
) data
ORDER BY SortField, FirstDay;
DROP TABLE #Data;
As a final point, this query becomes much simpler with a calendar table that stores all dates, and has flags for working days, holidays etc, rather than using a holiday table that just stores holidays.
Let me add few cents to this post. Just got assignment to calculate difference between planned hours and actual hour. The code below was converted to a function. So far no issue with the logic:
declare #date datetime = '11/07/2012'
declare #t table (HolidayID int IDENTITY(1,1) primary key,
HolidayYear int,
HolidayName varchar(50),
HolidayDate datetime)
INSERT #t
VALUES(2012, 'New Years Day', '01/02/2012'),
(2012,'Martin Luther King Day', '01/16/2012'),
(2012,'Presidents Day', '02/20/2012'),
(2012,'Memorial Day', '05/28/2012'),
(2012,'Independence Day', '07/04/2012'),
(2012,'Labor Day', '09/03/2012'),
(2012,'Thanksgiving Day', '11/22/2012'),
(2012,'Day After Thanksgiving', '11/23/2012'),
(2012,'Christmas Eve', '12/24/2012'),
(2012,'Christmas Day', '12/25/2012'),
(2013, 'New Years Day', '01/01/2013'),
(2013,'Martin Luther King Day', '01/21/2013'),
(2013,'Presidents Day', '02/18/2013'),
(2013,'Good Friday', '03/29/2013'),
(2013,'Memorial Day', '05/27/2013'),
(2013,'Independence Day', '07/04/2013'),
(2013,'Day After Independence Day', '07/05/2013'),
(2013,'Labor Day', '09/02/2013'),
(2013,'Thanksgiving Day', '11/28/2013'),
(2013,'Day After Thanksgiving', '11/29/2013'),
(2013,'Christmas Eve', NULL),
(2013,'Christmas Day', '12/25/2013')
DECLARE #START_DATE DATETIME,
#END_DATE DATETIME,
#Days int
SELECT #START_DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, #date), 0)
SELECT #END_DATE = DATEADD(month, 1,#START_DATE)
;WITH CTE AS
(
SELECT DATEADD(DAY, number, (DATEADD(MONTH, DATEDIFF(MONTH, 0, #date), 0) )) CDate
FROM master.dbo.spt_values where type = 'p' and number between 0 and 365
EXCEPT
SELECT HolidayDate FROM #t WHERE HolidayYear = YEAR(#START_DATE)
)
SELECT #Days = COUNT(CDate) --, datepart(dw, CDate) WDay
FROM CTE
WHERE (CDate >=#START_DATE and CDate < #END_DATE) AND DATEPART(dw, CDate) NOT IN(1,7)
SELECT #Days

How can I generate a temporary table filled with dates in SQL Server 2000?

I need to make a temporary table that holds of range of dates, as well as a couple of columns that hold placeholder values (0) for future use. The dates I need are the first day of each month between $startDate and $endDate where these variables can be several years apart.
My original sql statement looked like this:
select dbo.FirstOfMonth(InsertDate) as Month, 0 as Trials, 0 as Sales
into #dates
from customer
group by dbo.FirstOfMonth(InsertDate)
"FirstOfMonth" is a user-defined function I made that pretty much does what it says, returning the first day of the month for the provided date with the time at exactly midnight.
This produced almost exactly what I needed until I discovered there were occasionally gaps in my dates where I had a few months were there were no records insert dates. Since my result must still have the missing months I need a different approach.
I have added the following declarations to the stored procedure anticipating their need for the range of the dates I need ...
declare $startDate set $startDate = select min(InsertDate) from customer
declare $endDate set $endDate = select max(InsertDate) from customer
... but I have no idea what to do from here.
I know this question is similar to this question but, quite frankly, that answer is over my head (I don't often work with SQL and when I do it tends to be on older versions of SQL Server) and there are a few minor differences that are throwing me off.
I needed something similar, but all DAYS instead of all MONTHS.
Using the code from MatBailie as a starting point, here's the SQL for creating a permanent table with all dates from 2000-01-01 to 2099-12-31:
CREATE TABLE _Dates (
d DATE,
PRIMARY KEY (d)
)
DECLARE #dIncr DATE = '2000-01-01'
DECLARE #dEnd DATE = '2100-01-01'
WHILE ( #dIncr < #dEnd )
BEGIN
INSERT INTO _Dates (d) VALUES( #dIncr )
SELECT #dIncr = DATEADD(DAY, 1, #dIncr )
END
This will quickly populate a table with 170 years worth of dates.
CREATE TABLE CalendarMonths (
date DATETIME,
PRIMARY KEY (date)
)
DECLARE
#basedate DATETIME,
#offset INT
SELECT
#basedate = '01 Jan 2000',
#offset = 1
WHILE (#offset < 2048)
BEGIN
INSERT INTO CalendarMonths SELECT DATEADD(MONTH, #offset, date) FROM CalendarMonths
SELECT #offset = #offset + #offset
END
You can then use it by LEFT joining on to that table, for the range of dates you require.
I would probably use a Calendar table. Create a permanent table in your database and fill it with all of the dates. Even if you covered a 100 year range, the table would still only have ~36,525 rows in it.
CREATE TABLE dbo.Calendar (
calendar_date DATETIME NOT NULL,
is_weekend BIT NOT NULL,
is_holiday BIT NOT NULL,
CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED (calendar_date)
)
Once the table is created, just populate it once in a loop, so that it's always out there and available to you.
Your query then could be something like this:
SELECT
C.calendar_date,
0 AS trials,
0 AS sales
FROM
dbo.Calendar C
WHERE
C.calendar_date BETWEEN #start_date AND #end_date AND
DAY(C.calendar_date) = 1
You can join in the Customers table however you need to, outer joining on FirstOfMonth(InsertDate) = C.calendar_date if that's what you want.
You can also include a column for day_of_month if you want which would avoid the overhead of calling the DAY() function, but that's fairly trivial, so it probably doesn't matter one way or another.
This of course will not work in SQL-Server 2000 but in a modern database where you don't want to create a permanent table. You can use a table variable instead creating a table so you can left join the data try this. Change the DAY to HOUR etc to change the increment type.
declare #CalendarMonths table (date DATETIME, PRIMARY KEY (date)
)
DECLARE
#basedate DATETIME,
#offset INT
SELECT
#basedate = '01 Jan 2014',
#offset = 1
INSERT INTO #CalendarMonths SELECT #basedate
WHILE ( DATEADD(DAY, #offset, #basedate) < CURRENT_TIMESTAMP)
BEGIN
INSERT INTO #CalendarMonths SELECT DATEADD(HOUR, #offset, date) FROM #CalendarMonths where DATEADD(DAY, #offset, date) < CURRENT_TIMESTAMP
SELECT #offset = #offset + #offset
END
A starting point of a useful kludge to specify a range or specific list of dates:
SELECT *
FROM
(SELECT CONVERT(DateTime,'2017-1-1')+number AS [Date]
FROM master..spt_values WHERE type='P' AND number<370) AS DatesList
WHERE DatesList.Date IN ('2017-1-1','2017-4-14','2017-4-17','2017-12-25','2017-12-26')
You can get 0 to 2047 out of master..spt_values WHERE type='P', so that's five and a half year's worth of dates if you need it!
Tested below and it works, though it's a bit convoluted.
I assigned arbitrary values to the dates for the test.
DECLARE #SD smalldatetime,
#ED smalldatetime,
#FD smalldatetime,
#LD smalldatetime,
#Mct int,
#currct int = 0
SET #SD = '1/15/2011'
SET #ED = '2/02/2012'
SET #FD = (DATEADD(dd, -1*(Datepart(dd, #SD)-1), #sd))
SET #LD = (DATEADD(dd, -1*(Datepart(dd, #ED)-1), #ED))
SET #Mct = DATEDIFF(mm, #FD, #LD)
CREATE TABLE #MyTempTable (FoM smalldatetime, Trials int, Sales money)
WHILE #currct <= #Mct
BEGIN
INSERT INTO #MyTempTable (FoM, Trials, Sales)
VALUES
(DATEADD(MM, #currct, #FD), 0, 0)
SET #currct = #currct + 1
END
SELECT * FROM #MyTempTable
DROP TABLE #MyTempTable
For SQL Server 2000, this stackoverflow post looks promising for a way to temporarily generate dates calculated off of a start and end date. It's not exactly the same but quite similar. This post has a very in-depth answer on truncating dates, if needed.
In case anyone stumbles on this question and is working in PostgreSQL instead of SQL Server 2000, here is how you might do it there...
PostgreSQL has a nifty series generating function. For your example, you could use this series of all days instead of generating an entire calendar table, and then do groupings and matchups from there.
SELECT current_date + s.a AS dates FROM generate_series(0,14,7) AS s(a);
dates
------------
2004-02-05
2004-02-12
2004-02-19
(3 rows)
SELECT * FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00', '10 hours');
generate_series
---------------------
2008-03-01 00:00:00
2008-03-01 10:00:00
2008-03-01 20:00:00
2008-03-02 06:00:00
2008-03-02 16:00:00
2008-03-03 02:00:00
2008-03-03 12:00:00
2008-03-03 22:00:00
2008-03-04 08:00:00
(9 rows)
I would also look into date_trunc from PostgreSQL using 'month' for the truncator field to maybe refactor your original query to easily match with a date_trunc version of the calendar series.
select top (datediff(D,#start,#end)) dateadd(D,id-1,#start)
from BIG_TABLE_WITH_NO_JUMPS_IN_ID
declare #start datetime
set #start = '2016-09-01'
declare #end datetime
set #end = '2016-09-30'
create table #Date
(
table_id int identity(1,1) NOT NULL,
counterDate datetime NULL
);
insert into #Date select top (datediff(D,#start,#end)) NULL from SOME_TABLE
update #Date set counterDate = dateadd(D,table_id - 1, #start)
The code above should populate the table with all the dates between the start and end. You would then just join on this table to get all of the dates needed. If you only needed a certain day of each month, you could dateadd a month instead.
SELECT P.Id
, DATEADD ( DD, -P.Id, P.Date ) AS Date
FROM (SELECT TOP 1000 ROW_NUMBER () OVER (ORDER BY (SELECT NULL)) AS Id, CAST(GETDATE () AS DATE) AS Date FROM master.dbo.spt_values) AS P
This query returns a table calendar for the last 1000 days or so. It can be put in a temporary or other table.
Create a table variable containing a date for each month in a year:
declare #months table (reportMonth date, PRIMARY KEY (reportMonth));
declare #start date = '2018', #month int = 0; -- base 0 month
while (#month < 12)
begin
insert into #months select dateAdd(month, #month, #start);
select #month = #month + 1;
end
--verify
select * from #months;
This is by far the quickest method I have found (much quicker than inserting rows 1 by 1 in a WHILE loop):
DECLARE #startDate DATE = '1900-01-01'
DECLARE #endDate DATE = '2050-01-01'
SELECT DATEADD(DAY, sequenceNumber, #startDate) AS TheDate
INTO #TheDates
FROM (
SELECT ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n + 10000*tenthousands.n AS sequenceNumber
FROM
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) ones(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tens(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) hundreds(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) thousands(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tenthousands(n)
WHERE ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n + 10000*tenthousands.n <= DATEDIFF(day, #startDate, #endDate)
) theNumbers
SELECT *
FROM #TheDates
ORDER BY TheDate
The recursive answer:
DECLARE #startDate AS date = '20220315';
DECLARE #endDate AS date = '20230316'; -- inclusive
WITH cte_minutes(dt)
AS (
SELECT
DATEFROMPARTS(YEAR(#startDate), MONTH(#startDate), 1)
UNION ALL
SELECT
DATEADD(month, 1, dt)
FROM
cte_minutes
WHERE DATEADD(month, 1, dt) < #endDate
)
SELECT
dt
into #dates
FROM
cte_minutes
WHERE
dt >= #startDate
AND
dt <= #endDate
OPTION (MAXRECURSION 2000);
DROP TABLE dbo.#dates