How to count number of work days and hours extracting public holidays between two dates in SQL - sql

I am new to SQL and stuck in some complex query.
What am I trying to achieve?
I want to calculate following two types of total days between two timestamp fields.
Number of Working Days (Excluding Weekend & Public Holidays)
Number of Total Days (Including Weekend & Public Holidays)
Calculation Condition
If OrderDate time is <= 12:00 PM then start count from 0
If OrderDate Time is > 12:00 PM then start count from -1
If Delivery Date is NULL then count different till Today's Date
Data Model
OrderDate & DeliveryDate resides in 'OrderTable'
PublicHolidayDate resides 'PublicHolidaysTable'

As with many tasks in SQL, this could be solved in multiple ways.
You can use COUNT aggregate operations on the date range with the BETWEEN operator to give you aggregate totals of the weekend days and holidays from a start date (OrderDate) to an end date (DeliveryDate).
This functionality can be combined with CTEs (Common Table Expressions) to give you the end result set you are looking for.
I've put together a query that illustrates one way you could go about doing it. I've also put together some test data and results to illustrate how the query works.
DECLARE #DateRangeBegin DATETIME = '2016-01-01'
, #DateRangeEnd DATETIME = '2016-07-01'
DECLARE #OrderTable TABLE
(OrderNum INT, OrderDate DATETIME, DeliveryDate DATETIME)
INSERT INTO #OrderTable VALUES
(1, '2016-02-12 09:30', '2016-03-01 13:00')
, (2, '2016-03-15 13:00', '2016-03-30 13:00')
, (3, '2016-03-22 14:00', NULL)
, (4, '2016-05-06 10:00', '2016-05-19 13:00')
DECLARE #PublicHolidaysTable TABLE
(PublicHolidayDate DATETIME, Description NVARCHAR(255))
INSERT INTO #PublicHolidaysTable VALUES
('2016-02-15', 'President''s Day')
, ('2016-03-17', 'St. Patrick''s Day')
, ('2016-03-25', 'Good Friday')
, ('2016-03-27', 'Easter Sunday')
, ('2016-05-05', 'Cinco de Mayo')
Some considerations you may of already thought of are that you don't want to count both weekend days and holidays that occur on a weekend, unless your company observes the holiday on the next Monday. For simplicity, I've excluded any holiday that occurs on a weekend day in the query.
You'll also want to limit this type of query to a specific date range.
The first CTE (cteAllDates) gets all dates between the start and end date range.
The second CTE (cteWeekendDates) gets all weekend dates from the first CTE (cteAllDates).
The third CTE (ctePublicHolidays) gets all holidays that occur on weekdays from your PublicHolidaysTable.
The last CTE (cteOrders) fulfills the requirement that the count of total days and working days must begin from the next day if the OrderDate is after 12:00PM and the requirement that the DeliveryDate should use today's date if it is null.
The select statement at the end of the CTE statements gets your total day count, weekend count, holiday count, and working days for each order.
;WITH cteAllDates AS (
SELECT 1 [DayID]
, #DateRangeBegin [CalendarDate]
, DATENAME(dw, #DateRangeBegin) [NameOfDay]
UNION ALL
SELECT cteAllDates.DayID + 1 [DayID]
, DATEADD(dd, 1 ,cteAllDates.CalendarDate) [CalenderDate]
, DATENAME(dw, DATEADD(d, 1 ,cteAllDates.CalendarDate)) [NameOfDay]
FROM cteAllDates
WHERE DATEADD(d,1,cteAllDates.CalendarDate) < #DateRangeEnd
)
, cteWeekendDates AS (
SELECT CalendarDate
FROM cteAllDates
WHERE NameOfDay IN ('Saturday','Sunday')
)
, ctePublicHolidays AS (
SELECT PublicHolidayDate
FROM #PublicHolidaysTable
WHERE DATENAME(dw, PublicHolidayDate) NOT IN ('Saturday', 'Sunday')
)
, cteOrders AS (
SELECT OrderNum
, OrderDate
, CASE WHEN DATEPART(hh, OrderDate) >= 12 THEN DATEADD(dd, 1, OrderDate)
ELSE OrderDate
END [AdjustedOrderDate]
, CASE WHEN DeliveryDate IS NOT NULL THEN DeliveryDate
ELSE GETDATE()
END [DeliveryDate]
FROM #OrderTable
)
SELECT o.OrderNum
, o.OrderDate
, o.DeliveryDate
, DATEDIFF(DAY, o.AdjustedOrderDate, o.DeliveryDate) [TotalDayCount]
, (SELECT COUNT(*) FROM cteWeekendDates w
WHERE w.CalendarDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [WeekendDayCount]
, (SELECT COUNT(*) FROM ctePublicHolidays h
WHERE h.PublicHolidayDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [HolidayCount]
, DATEDIFF(DAY, o.AdjustedOrderDate, o.DeliveryDate)
- (SELECT COUNT(*) FROM cteWeekendDays w
WHERE w.CalendarDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate)
- (SELECT COUNT(*) FROM ctePublicHolidays h
WHERE h.PublicHolidayDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [WorkingDays]
FROM cteOrders o
WHERE o.OrderDate BETWEEN #DateRangeBegin AND #DateRangeEnd
OPTION (MaxRecursion 500)
Results from the above query using the test data...
What I'd probably do is simplify the above by adding a Calendar table populated with sufficiently wide date ranges. Then I'd take some of the CTE statements and turn them into views.
I think specifically valuable to you would be a view that gets you the work days without weekends or holidays. Then you could just simply get the date difference between the two dates and count the work days in the same range.

Related

Split Hours into Multiple Months using SQL

CREATE TABLE EventLog
(
EventID INT
, EventName VARCHAR(50) NOT NULL
, EventStartDateTime DATETIME NOT NULL
, EventEndDateTime DATETIME NULL
)
INSERT INTO EventLog(EventID, EventName, EventStartDateTime, EventEndDateTime)
VALUES(100, 'Planting', '20210620 10:34:09 AM','20211018 10:54:49 PM')
,(200, 'Foundation', '20200420 10:34:09 AM','20211018 10:54:49 PM')
,(300, 'Seeding', '20210410 10:27:19 AM','')
,(400, 'Spreading', '20220310 10:24:09 PM','')
I have a requirement to split hours into multiple months and even years depending on the length of the event. In some cases, the event may have an end date but if the event is still ongoing, there will be no end date.
The result or output of the solution is to be held by another table:
CREATE TABLE EventSummary
(
EventID INT
, EventName VARCHAR(50) NOT NULL
, [Year] INT
, [MonthName] VARCHAR(25)
, [Hours] DECIMAL(12,2)
)
The image above is the output of the first row.
If the event runs over multiple years, the values should be spread across the multiple years and months likewise.
In cases where there is no end date I am to use GETUTCDATE() to do the calculation.
Some events span across months or event years. I would like to be able to break down the total duration into individual month's duration (or individual duration by month) in hours respectively and populate it into a table
Consider that I have an event with start and end date: '20210620 10:34:09 AM','20211018 10:54:49 PM'
For the first month which basically is not a full month, I am to calculate the remaining hours of that month and store it against that month.
I do the same for the next month. If the event runs for the entire month which is now the month of July, I store the entire hours for that month which is 744 hours against July. I keep doing that till the end of the event. But if the event is still open(blank or empty) I use
GETUTCDATE() as the end date
The sum of Hours is grouped by EventID, EventName, Year and Months
It is expected that the first and or last month may be decimals as they may not be fully formed.
I have tried to work this out but do not know how to get the best result with SQL Server.
I kindly will appreciate your help with this.
Thanks
If I understand correctly, you can try to use CTE recursive, get each month startdate then use antoher cte2 get the starttime and endtime between each month.
;WITH CTE AS (
SELECT EventID,EventName,EventStartDateTime,IIF(EventEndDateTime = '',GETUTCDATE(),EventEndDateTime) EventEndDateTime
FROM EventLog
UNION ALL
SELECT EventID,EventName, DATEADD(month, DATEDIFF(month, 0, DATEADD(month , 1 , EventStartDateTime)), 0) , EventEndDateTime
FROM CTE
WHERE DATEADD(month, DATEDIFF(month, 0, DATEADD(month , 1 , EventStartDateTime)), 0) <= EventEndDateTime
), CTE2 AS (
SELECT EventID,EventName,EventStartDateTime,LEAD(EventStartDateTime,1,EventEndDateTime) OVER(PARTITION BY EventID,EventName ORDER BY EventStartDateTime) n_EventStartDateTime
FROM CTE
)
INSERT INTO EventSummary(EventID,EventName,Year,MonthName,Hours)
SELECT EventID,EventName,YEAR(EventStartDateTime),DATENAME(MONTH,EventStartDateTime),DATEDIFF(second, EventStartDateTime, n_EventStartDateTime) / 3600.0
FROM CTE2
option (maxrecursion 0)
sqlfiddle

Count # of Saturdays given a date range

I have a datetime field and a net field. The Sat Count field is done by =IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0)
I want to total the count of the Saturdays given a starting date and end date (typically a month).
I tried =Sum(IIf(DatePart("w",Fields!DespatchDate.Value)=7,1,0) but the total is wrong.
I also want to count Saturdays for rest of the month, e.g there's a missing 3rd Saturday in the picture.
I also want to do a total of the Net for Saturdays.
Can you point me in the direction. I can do it in SQL or in SSRS
Considering that we do not have any Input or desired output provided, I am assuming that You just want to count Saturdays in a given range:
Select COUNT(*), SUM(Net)
FROM table
WHERE Day# = 7 AND Date BETWEEN '2021-02-16' AND '2021-02-23'
Assuming you want to count saturdays even if it is not part of your dataset, what you need to do is pad out all your dates for the given range and then join it to your base data set.
This would ensure that it accounts for ALL days of the week regardless of a dispatch event occuring on that date / day.
Below is some SQL code that might help you make a start.
declare #startdate date = '2021-02-01'
declare #enddate date = '2021-02-28'
if OBJECT_ID ('tempdb..#dates') is not null
drop table #dates
;WITH mycte AS
(
SELECT CAST(#startdate AS DATETIME) DateValue
UNION ALL
SELECT DateValue + 1
FROM mycte
WHERE DateValue + 1 < #enddate
)
SELECT DateValue into #dates
FROM mycte
OPTION (MAXRECURSION 0)
select
d.DateValue
, datepart(weekday,d.DateValue) as day_no
,case when datepart(weekday,d.DateValue) = 7 then isnull(t.net,0) else 0 end as sat_net
,case when datepart(weekday,d.DateValue) = 1 then isnull(t.net,0) else 0 end as sun_net
from #dates d
left join your_table t
on d.DateValue = t.some_date
drop table #dates
Since I don't know what your required output is, I cannot summarise this any further. But you get the idea!

column values are not showing up

Why can't I get values 0 in the column when #processdate date = null or a Sunday (the transactions are not occurring on Sundays)
This was my original query that only gives me values in the table when #processdate is declared as any day of the week that is not Sunday or null
I need It to show me a 0 in the table when #processdate= null or any sunday
alter procedure USP_CTG_VolumeReport
#processdate date = '1/8/2017'
as
declare #date date = coalesce(#processdate, cast( getdate() as date))
select 'apa' as Source,
cast(AccountID_AssignDT as date)as 'Date',
count(*) as Total_Count
from apa_2000
where
cast(AccountID_AssignDT as date) = #date
group by
cast(AccountID_AssignDT as date)
I should be able to get the proper table with a subquery but for some reason --it is not working
Below is my subquery that is giving me the same result as my previous query
select
'apa' as Source,
cast(AccountID_AssignDT as date)as 'Date',
isnull((select count(*)
from apa_2000
where cast(AccountID_AssignDT as date) = #date)
, 0 ) as Total_Count
from apa_2000
where
cast(AccountID_AssignDT as date) = #date
group by
cast(AccountID_AssignDT as date)
The issue here is that those dates where no processing happens, so the counts would be zero, simply don't exist in the database. You are expecting the query to generate data which isn't there. What you need is a table which includes a list of all the dates you want to report against, and then to match the counts against those dates.
If you are always passing in a single date, the following would work, since the input date is the only date you need:
declare #date as date = '20170416'
select
'apa' as Source,
#date as 'Date',
count(apa_2000.AccountID_AssignDT) as Total_Count
from
(select 1 as x)x
LEFT OUTER JOIN
apa_2000 ON cast(AccountID_AssignDT as date) = #date

Query to check number of records created in a month.

My table creates a new record with timestamp daily when an integration is successful. I am trying to create a query that would check (preferably automated) the number of days in a month vs number of records in the table within a time frame.
For example, January has 31 days, so i would like to know how many days in january my process was not successful. If the number of records is less than 31, than i know the job failed 31 - x times.
I tried the following but was not getting very far:
SELECT COUNT (DISTINCT CompleteDate)
FROM table
WHERE CompleteDate BETWEEN '01/01/2015' AND '01/31/2015'
Every 7 days the system executes the job twice, so i get two records on the same day, but i am trying to determine the number of days that nothing happened (failures), so i assume some truncation of the date field is needed?!
One way to do this is to use a calendar/date table as the main source of dates in the range and left join with that and count the number of null values.
In absence of a proper date table you can generate a range of dates using a number sequence like the one found in the master..spt_values table:
select count(*) failed
from (
select dateadd(day, number, '2015-01-01') date
from master..spt_values where type='P' and number < 365
) a
left join your_table b on a.date = b.CompleteDate
where b.CompleteDate is null
and a.date BETWEEN '01/01/2015' AND '01/31/2015'
Sample SQL Fiddle (with count grouped by month)
Assuming you have an Integers table*. This query will pull all dates where no record is found in the target table:
declare #StartDate datetime = '01/01/2013',
#EndDate datetime = '12/31/2013'
;with d as (
select *, date = dateadd(d, i - 1 , #StartDate)
from dbo.Integers
where i <= datediff(d, #StartDate, #EndDate) + 1
)
select d.date
from d
where not exists (
select 1 from <target> t
where DATEADD(dd, DATEDIFF(dd, 0, t.<timestamp>), 0) = DATEADD(dd, DATEDIFF(dd, 0, d.date), 0)
)
Between is not safe here
SELECT 31 - count(distinct(convert(date, CompleteDate)))
FROM table
WHERE CompleteDate >= '01/01/2015' AND CompleteDate < '02/01/2015'
You can use the following query:
SELECT DATEDIFF(day, t.d, dateadd(month, 1, t.d)) - COUNT(DISTINCT CompleteDate)
FROM mytable
CROSS APPLY (SELECT CAST(YEAR(CompleteDate) AS VARCHAR(4)) +
RIGHT('0' + CAST(MONTH(CompleteDate) AS VARCHAR(2)), 2) +
'01') t(d)
GROUP BY t.d
SQL Fiddle Demo
Explanation:
The value CROSS APPLY-ied, i.e. t.d, is the ANSI string of the first day of the month of CompleteDate, e.g. '20150101' for 12/01/2015, or 18/01/2015.
DATEDIFF uses the above mentioned value, i.e. t.d, in order to calculate the number of days of the month that CompleteDate belongs to.
GROUP BY essentially groups by (Year, Month), hence COUNT(DISTINCT CompleteDate) returns the number of distinct records per month.
The values returned by the query are the differences of [2] - 1, i.e. the number of failures per month, for each (Year, Month) of your initial data.
If you want to query a specific Year, Month then just simply add a WHERE clause to the above:
WHERE YEAR(CompleteDate) = 2015 AND MONTH(CompleteDate) = 1

Select Start date that is 60 business days old

I need the ability to select a start date that is 60business days prior to the current date. I have a calendar built with dates and business dates, etc. My current (and unsuccessful) method is below:
DECLARE #Mode int
SET #Mode = 0
SET #StartDate = CASE WHEN #Mode = 0
THEN (SELECT BusDate FROM Leads.dbo.Calendar
WHERE Date = DATEADD(DAY,DATEDIFF (DAY,0,GETDATE())-60,0))
WHEN #Mode = 1
THEN DATEADD(mm,DATEDIFF (mm,0,GETDATE())-2,0)
END
This method goes back 60 calendar days, not 60 business days. I'm having a challenge with getting this down to Business days. Disregard the Mode = 1 portion (this calculates by month).
Any help would be appreciated.
The Calendar table has the following fields:
SELECT [Date]
,[MMDDYYYY]
,[Year]
,[QTR]
,[Month]
,[Week]
,[YTDDay]
,[QTDDay]
,[MTDDay]
,[WeekDayNbr]
,[Quarter]
,[MonthLName]
,[MonthName]
,[DayOfWeekS]
,[DayOfWeek]
,[KindOfDay]
,[Description]
,[Period]
,[YrMo]
,[YrWk]
,[StartDate]
,[EndDate]
,[BusPeriod]
,[Holiday]
,[NonBus]
,[BusDaysInMonth]
,[BusDay]
,[BusDaysRemain]
,[BusDate]
,[YYYYMMDD]
BusDay is the business day for a month (1, 2, 3, etc). There currently is no column that says 1 for yes and 0 for No to indicate it is a business day, although I could add that. The NonBus and Holiday fields operate that way: 1 is Holiday or Non-business day (holidays and weekends) and 0 is not.
We don't know much about your calendar table but you could do something like this. Use a derived table that only consists of business days that are less than today's date and give each a row number ordered by date desc. Then select the date from 60th row.
select busdate
from
(
select *,
row_number() over (order by date desc) as dayNo
from leads.dbo.calendar
where nonBus = 0 -- Only business days
and date < cast(getdate() as date)
) busDays
where dayno = 60