Split Hours into Multiple Months using SQL - 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

Related

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!

How to count number of work days and hours extracting public holidays between two dates in 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.

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

Table driven payment schedule

My Payment Schedule holds a row for a payment schedule that gets run on a specific day, based on an 'effective date'.
CREATE TABLE [dbo].[PaymentSchedule] (
[PaymentScheduleId] INT IDENTITY (1, 1) NOT NULL,
[EffectiveDate] DATE NOT NULL,
[EffectiveDays] INT NOT NULL,
CONSTRAINT [pk_PaymentSchedule] PRIMARY KEY CLUSTERED ([PaymentScheduleId] ASC)
);
So, if the effectivedate is '01-JAN-2013', and the 'EffectiveDays' is 7, then payment get made on the 1st of January, and then every 7 days after that. So, on the 8th of January, a payment must be made. On the 15th, a payment must be made.. etc etc.
If the effectivedate was '01-JAN-2013', and the EffectiveDays was 20, then the first payment is the 1st of Jan, the next payment day is the 21th of Jan, and the next after that would be 9th Feb, 2013.. etc etc.
What I am trying to do, is make a function that uses the above table, or a stored proc for that matter, that returns 'Next Payment Date', and takes in a DATE type. So, based on the date passed in, what is the next payment date? And also, 'Is today a payment date'.
Can this be done efficiently? In 7 years time, would I be able to tell if a date is a payment day, for example?
Your description of the problem is wrong. If the first payment is on Jan 1, the subsequent payments would be on the Jan 8, Jan 15 and so on.
The answer to your question about the current date is datediff() along with the modulus operator. To see if today is a payment date, take the difference and see if it is an exact multiple of the period you are looking at:
select getdate()
from PaymentSchedule ps
where datediff(day, ps.EffectiveDate, getdate()) % ps.EffectiveDays = 0;
The % is the modulus operator that takes the remainder between two values. So, 3%2 is 1 and 10%5 is 0.
For the next date, the answer is similar:
select dateadd(day,
ps.EffectiveDays - datediff(day, ps.EffectiveDate, today) % ps.EffectiveDays,
today) as NextDate
from PaymentSchedule ps cross join
(select cast(getdate() as date) as today) const
I've structured this as a subquery that defines the current date as today. This makes it easier to substitute in any other date that you might want.
You can use the method DATEDIFF(datepart, startdate, enddate) setting as datepart "dayofyear" to the result of this method will give you the number of days between the two dates, and divide as Modulo (%) this result by the EffectiveDays and if the result is 0 there is payment day; and if not you will have the days passed from the last payment day (if you sustract it from the EffectivedDays you must have the restant days to the next payment day).
Here is some doc for the DATEDIFF method:
http://msdn.microsoft.com/en-us/library/ms189794.aspx
I may be answering the wrong question, but I think the following code returns payment schedules that hit the selected payment date, if that's what you're looking for?
IF OBJECT_ID('tempdb..#PaymentSchedules') IS NOT NULL
DROP TABLE #PaymentSchedules;
CREATE TABLE #PaymentSchedules
( PaymentScheduleID INT NOT NULL IDENTITY(1,1)
CONSTRAINT PK_PaymentSchedules_PaymentScheduleID PRIMARY KEY
, EffectiveDate DATE NOT NULL
, EffectiveDays INT NOT NULL )
;
INSERT #PaymentSchedules (EffectiveDate, EffectiveDays)
VALUES
('20120401', 3)
, ('20120401', 2)
, ('20120401', 1)
, ('20120401', 7)
, ('20120401', 14)
;
DECLARE #PaymentDate DATE = '20140310';
WITH myCTE AS
(
SELECT PaymentScheduleID, PaymentDate = EffectiveDate, EffectiveDays
FROM #PaymentSchedules
UNION ALL
SELECT PaymentScheduleID, PaymentDate = DATEADD(DAY, EffectiveDays, PaymentDate), EffectiveDays
FROM myCTE
WHERE DATEADD(DAY, EffectiveDays, PaymentDate) <= #PaymentDate
)
SELECT * FROM myCTE
WHERE PaymentDate = #PaymentDate
OPTION (MAXRECURSION 10000)
;

How to handle NULL in a SQL subquery?

I'm pulling my hair out over such a simple thing...
I'm recording the number of days a member attends a gym club. By default, I assume the member attends every day. When they are sick, I record the dates and total number of days absent in a table (ie DateFrom, DateEnd, TotalDays). The total days absent is the difference between DateFrom and DateEnd.
Now sometimes I don't know when the member is coming back to gym. Just that they have stopped attending on a certain day. Hence the DateEnd and TotalDays are unknown. So the total number days are calculated by taking the difference between DateFrom and today's date.
Table: InactiveOnProgram
Columns: PersonId, DateFrom, DateEnd, TotalDays
Data:
1,01/01/2012,05/01/2012,5
1,05/01/2012,08/01/2012,3
2,01/02/2012,05/02/2012,5
2,05/02/2012,08/02/2012,3
2,20/02/2012,null,null
My below query works fine for personId=2. The total days absent is 8+2=10 days (2 days being 20/02/2012 till 22/02/2012 = today ). But for personId=1, it returns null , instead of 8 days!
sql:
(SELECT
case ( isnull(sum(TotalDays), 0) )
when 0 then 0
else CAST(SUM(TotalDays) as DECIMAL(20,2))
end
FROM InactiveOnProgram
)
+
(SELECT
case ( isnull( DateFrom, 0) )
when null then 0
when 0 then 0
else CAST(datediff(day,DateFrom, getdate()) as DECIMAL(20,2))
end
FROM InactiveOnProgram
WHERE (TotalDays is null or TotalDays =0)
AND DateTo is null
)
Any idea what I'm missing here?! As far as I can guess the second part of sql returns null and because of this it ignores the first part!
Any help is much appreciated.
Thanks
You can write it as a single query:
declare #InactiveOnProgram table
(PersonId int, DateFrom datetime, DateEnd datetime, TotalDays int)
insert into #InactiveOnProgram (PersonId , DateFrom , DateEnd , TotalDays)
select 1,'20120101','20120105',5 union all
select 1,'20120105','20120108',3 union all
select 2,'20120201','20120205',5 union all
select 2,'20120205','20120208',3 union all
select 2,'20120220',null,null
select PersonId,SUM(COALESCE(TotalDays,DATEDIFF(day,DateFrom,CURRENT_TIMESTAMP)))
from #InactiveOnProgram group by PersonId
I'm not really happy with the storing of TotalDays, but given your data set, it seems necessary, since apparently, from 1st - 5th = 5 days, but from 5th - 8th = 3 days.
Do you only guess that second part returns null or do you know that? Because as far as I can see, first part is returning something undefined.
You need to use SUM() and ISNULL() in different order, like:
select cast(sum(isnull(TotalDays, 0)) as decimal(20,2)) as totdays
And in second case you can use next:
datediff(day, isnull(DateFrom, getdate()), getdate())
This way you can eliminate null values before calculation/conversion.
Maybe this is your solution:
select personid, sum( closed + unclosed)
from (
SELECT personid
, CAST(SUM(isnull(nullif(TotalDays,0),0)) as DECIMAL(20,2)) as closed
, case when min(isnull(nullif(DateFrom,0),0))=0 OR (SUM(isnull(nullif(TotalDays,0),0)) >0 AND min(isnull(nullif(dateend,0),0)) >0) then 0 else min(CAST(datediff(day,DateFrom, getdate()) as DECIMAL(20,2))) end as unclosed
FROM test
group by personid
--WITH ROLLUP
) as test
group by personid
WITH ROLLUP
The problem is basically that in SQL a null term in a calculation results in a null.
Make sure null is not possible in your results.
BTW, your logic is waaaaay too complicated - simplify it