Returning date range of overlapping date ranges - sql

I have a scenario where I have a SQL table that looks like the below screenshot. Is it possible to retrieve the start and end date of just the portion that overlaps between multiple records? I've reviewed similar questions from StackOverflow, and what I found was finding the records that has overlaps, but not the actual overlap range.
The desired result will show that the start date is 1/1/2017 and the end date is 6/30/2017 since that range is in both records.

Let's use variables to keep it simple. I assume you're using some join of two rows in your table where the ON condition determines that the dates overlap. But, since the part you're asking about is displaying the overlapping dates, we can just use variables:
DECLARE #start1 date = '20160701', #end1 date = '20170630'
,#start2 date = '20170101', #end2 date = '20171231'
SELECT CASE WHEN #start2 >= #start1 THEN #start2 ELSE #start1 END AS startdate
, CASE WHEN #end2 <= #end1 THEN #end2 ELSE #end1 END AS enddate
If you're on Azure SQL where GREATEST and LEAST are supported you can use:
SELECT GREATEST(#start1, #start2) AS startdate
, LEAST(#end1, #end2) AS enddate

The key is to understand the logic on the overlap.
if we are searching for the overlap period of many rows then it will ALWAYS start at the MAX(startDate).
the end point will be the minimum of the end dates where it is larger than our start date.
But we need to think about checking if they have an overlap period or not.
So the code should be:
declare #start datetime = (select MAX(PlanYearStart) start from #t);
if exists(select PlanYearEnd from #t where PlanYearEnd<#start)
begin
select null OverlapStart, null OverlapEnd;
return;
end;
select
#start OverlapStart,
MIN(PlanYearEnd) OverlapEnd
from #t;

Related

How to get WeekDay in between two days in SQL server

Please help, I have a below sample data. How to find week days "Tuesday" and count between two days.
CREATE TABLE EmpDetails1 (id INT, name VARCHAR(25),startdate datetime,enddate datetime)
INSERT INTO EmpDetails1 VALUES(1,'TEST','01/01/2016','01/10/2016');
INSERT INTO EmpDetails1 VALUES(2,'TEST','01/01/2016','01/25/2016');
id name startdate enddate
1 Test 1 1/1/16 1/10/16
2 Test 2 1/1/16 1/25/16
output:
date count
1/5/16 1
1/12/16 3
I have tried with below query but not getting correct result
SELECT name,
DATENAME(WEEKDAY, startdate) as w1,
DATENAME(WEEKDAY, enddate) as w2,
startdate,enddate, count(*) OVER(PARTITION BY startdate,enddate) AS CountOfOrders from EmpDetails1 group by startdate , enddate,name
As mentioned already, you need a calendar. You can generate one dynamically but far better to create a static one since it serves so many useful purposes. This is but one of many discussions about how to generate one.
A calendar is just a special form of a tally table and I use Itzik's discussion as a basis for that. Both of these concepts are things you need to understand in most SQL query writing environments.
Once you have a calendar, you simply join your data table to the calendar and filter as needed. I did not understand exactly what you were trying to accomplish so I simply create the set of rows for "Tuesday".
declare #EmpDetails1 table (id int, name varchar(20), startdate date, enddate date);
insert #EmpDetails1 (id, name, startdate, enddate) values
(1, 'Test 1', '20210101', '20210110'),
(2, 'Test 2', '20210116', '20210126');
select emp.*, cal.*, datename(weekday, cal.caldt) as [day of week(eng)]
from #EmpDetails1 as emp
inner join calendar as cal
on cal.caldt between emp.startdate and emp.enddate
and datename(weekday, cal.caldt) = 'Tuesday'
order by emp.id, cal.caldt
;
Fiddle here to demonstrate. I must highlight the lazy usage of * as the column list but this is just a simple demo. Production code should generally always specify the columns needed completely.
If you examine the calendar table discussion, you will see that the day of week can be easily added to the table - it will never change. This will avoid the effort to calculate it in the query.
T-SQL Code:
CREATE PROCEDURE tuesdayCount #id_number INT AS
BEGIN
DECLARE #S_Date DATETIME
DECLARE #E_Date DATETIME
SET #S_Date = (SELECT startdate FROM EmpDetails1 WHERE id = #id_number)
SET #E_Date = (SELECT enddate FROM EmpDetails1 WHERE id = #id_number)
WHILE #S_Date <= #E_Date
IF (FORMAT(#S_Date, '%a') = 'Tue')
INSERT INTO TuesdayDates VALUES(#S_Date, 1)
SET #S_Date = DATEADD(DAY,1, #S_Date)
END
EXECUTE tuesdayCount #id_number=1;
EXECUTE tuesdayCount #id_number=2;
Code Steps:
First line I created a procedure named tuesdayCount with an input parameter(id_number)
Then I declared 2 variables (S_Date and E_Date) with DATETIME data type, then set them equal to startdate and endnote column values in the first row.(Please take note of id_number sp parameter in the where clause.)
Then I defined a while loop and if_ test to ensure the date is really a Tuesday. If that's so, then I inserted the date value(S_Date) into TuesdayDates table which I created myself beforehand to put the result set there. (maybe not so logical; but I did it anyway.)
After defining the sp proc, I called my functions with id_number parameter which is in fact the row number of your data set (EmpDetails1 Table)
Not a perfect solution :) but I hope It helps somehow.

Converting a time excel formula to t-sql

So I'm new to SQL (I believe it's T-SQL) and I'm trying to convert a function I used in Excel to SQL.
L2 becomes Column 1
G2 becomes Column 2
=(INT(L2)-INT(G2))*("17:00"-"08:45")+MEDIAN(MOD(L2,1),"17:00","08:45")-MEDIAN(MOD(G2,1),"17:00","08:45")
What this does is calculate the business hours worked between 8:45AM and 05:00PM.
If work goes from 4:00PM to 9:00AM the next day, the result should be 01:15:00.
If it goes over several days (4:00PM on the 1st to 9:00AM on the 4th) it should be 17:45:00.
I'd prefer not to have a separate function because I don't know how to use them as I'm quite new to this - I'd prefer to have it as something I can write within the SELECT * , <code here here> FROM db.name section.
Thanks in advance
I know you said you don't want this in a function, but they really aren't hard to use and the logic you require for this is too complex in SQL Server to be sensibly contained inline (Though it can be, if you really want to be that guy).
This function has no error handling if any of your parameters are not suitable, though I will leave that up to you as a learning exercise on NULL values, process flows and fully thinking through all the possibilities that you may need to deal with:
-- This bit creates your function. You can rename the function from fnWorkingDays to anything you want, though try to keep your naming conventions sensible:
create function fnWorkingDays(#Start datetime
,#End datetime
)
returns decimal(10,2)
as
begin
-- Declare the start and end times of your working day:
declare #WorkingStart time = '08:45:00.000'
declare #WorkingEnd time = '17:00:00.000'
-- Work out the number of minutes outside the working day in 24 Hour Notation:
declare #OvernightMinutes int = datediff(minute -- Work out the difference in minutes,
,cast(#workingend as datetime) -- between the end of the working day (CASTing a TIME as DATETIME gives you 1900-01-01 17:00:00)
,dateadd(d,1,cast(#WorkingStart as datetime)) -- and the start of the next working day (CAST the TIME value as DATETIME [1900-01-01 08:45:00] and then add a day to it [1900-01-02 08:45:00])
)
-- There is no need to retain the minutes that fall outside your Working Day, to if the very start or very end of your given period fall outside your Working Day, discard those minutes:
declare #TrueStart datetime = (select case when cast(#Start as time) < #WorkingStart
then dateadd(d,datediff(d,0,#Start),0) + cast(#WorkingStart as datetime)
else #Start
end
)
declare #TrueEnd datetime = (select case when cast(#End as time) > #WorkingEnd
then dateadd(d,datediff(d,0,#End),0) + cast(#WorkingEnd as datetime)
else #End
end
)
-- You can now calculate the number of minutes in your true working period, and then subtract the total overnight periods in minutes to get your final value.
-- So firstly, if your Working Period is not long enough to stretch over two days, there is not need to do any more than calculate the difference between the True Start and End:
return (select case when datediff(minute,#Start,#End) < #OvernightMinutes
then datediff(minute,#TrueStart,#TrueEnd)
-- If you do need to calculate over more than one day, calculate the total minutes between your True Start and End, then subtract the number of Overnight Minutes multiplied by the number of nights.
-- This works because DATEDIFF calculated the number of boundaries crossed, so when using DAYS, it actually counts the number of midnights between your two dates:
else (datediff(minute,#TrueStart,#TrueEnd) - (datediff(d,#TrueStart,#TrueEnd) * #OvernightMinutes))/1440.
-- If you want to return your value in a slightly different format, you could use variations of these two, though you will need to change the RETURNS DECIMAL(10,2) at the top to RETURNS NVARCHAR(25) if you use the last one:
-- else datediff(minute,#TrueStart,#TrueEnd) - (datediff(d,#TrueStart,#TrueEnd) * #OvernightMinutes)
-- else cast((datediff(minute,#TrueStart,#TrueEnd) - (datediff(d,#TrueStart,#TrueEnd) * #OvernightMinutes))/60 as nvarchar(5)) + ' Hours ' + cast((datediff(minute,#TrueStart,#TrueEnd) - (datediff(d,#TrueStart,#TrueEnd) * #OvernightMinutes))%60 as nvarchar(5)) + ' Minutes'
end
)
end
go
And this is how you call the function:
select dbo.fnWorkingDays('2016-09-04 12:00:00.000', '2016-09-06 12:10:00.000') as WorkingDays
You can replace the two DATETIME values about with the appropriate column names to get your desired result inline:
select dbo.fnWorkingDays(Dates.StartDate, Dates.EndDate) as WorkingDays
from (select '2016-09-04 12:00:00.000' as StartDate
,'2016-09-06 12:10:00.000' as EndDate
) as Dates

SQL, Stock movements where no zero stock records

I'm trying to create a stock movement file from historical stock records, see picture:
I've been able to get the green movements and the orange value (had some luck union all to a 0 stock record for max(day) + 1 and removing anything less than today). Movements are then generated with a lag function.
The difficulty is there are no 0 stock records. i.e. fifth row in the example above there is no record so unable to create the correct movement to 0 with the -2. It would create movements of -10, -4, -4, 5, -3, -1, -3
Is there any easier way to identify the gaps and create phantom 0's, I know the dates (these are a mix of weekly that will become daily)?
There are several ways to get the missing gaps in this case it looks like you have a date every week (or 7 days) and in some case there is no date. So you can build a date table with a while loop and do a join and assinging Stock Opening Balance as 0 when null in your table. Here is something similar I am using table variables to simply but using a temp table would probably be better for you and if you do you don't need 2 tables just insert the missing to your temp table.
DECLARE #TableWithGaps AS TABLE ([Date] DATE, StockOpeningBalance INT)
INSERT INTO #TableWithGaps ([Date],StockOpeningBalance)
VALUES ('2016-04-04',10)
,('2016-04-11',6)
,('2016-04-18',2)
,('2016-05-09',7)
,('2016-05-16',4)
,('2016-05-23',3)
DECLARE #Dates AS TABLE ([Date] DATE)
DECLARE #MinDate DATE = '3/28/2016'
DECLARE #MaxDate DATE = '6/20/2016'
--Could also get first record as minimum date and last record, or set #maxdate = GETDATE()
--SELECT #MinDate = DATEADD(DAY,7,MIN([Date])), #MaxDate = MAX([Date]) FROM #TableWithGaps
WHILE #MinDate < DATEADD(DAY,1,#MaxDate)
BEGIN
INSERT INTO #Dates ([Date]) VALUES (#MinDate)
SET #MinDate = DATEADD(DAY,7,#MinDate)
END
SELECT
d.[Date]
,StockOpeningBalance = ISNULL(StockOpeningBalance,0)
,LAG(ISNULL(StockOpeningBalance,0),1,0) OVER (ORDER BY d.[Date])
,Movements = ISNULL(StockOpeningBalance,0) - LAG(ISNULL(StockOpeningBalance,0),1,0) OVER (ORDER BY d.[Date])
FROM
#Dates d
LEFT JOIN #TableWithGaps s
ON d.[Date] = s.[Date]
Also note, I prefer temp tables over table variables and they temp tables will perform better with a larger data set. If you start doing a ton with dates you may want to look at a date dimension table for purposes like this. If you are using Microsoft SSAS it has a built in tool to generate one that you can google/search for pretty quickly.

using a loop in a stored procedure

i am trying to write a procedure that inserts rows into a temp table. the basis of the table is an insurance policy table listing the amount of the premium earned over the life of the policys. the original data consists of the trans_date (date sold) and the policy_start and policy_end dates. i.e. if the policy is 12 months long, we give each month 1/12 of the premium collected.
so something like
while trans_month < policy_end month
insert to tblUEPtmp
select dateadd(mm, 1, trans_date), earned_premium from tblpolicys
set trans_date = dateadd(mm, 1, trans_date)
(i know this is rubbush code but i completely baffled at the moment)
My problem is that i need to create the extra 11 rows of data and modify the transaction date to add 1 month each time until the modified transaction date = policy_end date.
i've researched using a CTE, but while loops aren't posible within a CTE..
is this something a multistatement table function could do?
Many thanks.
You can defo do this with a CTE, for example this little snippet will demonstrate how to do recursion using dates:
declare #start DATETIME = '2012-02-01'
declare #end DATETIME = '2013-02-01'
;with cte (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(mm,1,cte.date)
FROM cte WHERE DATEADD(mm,1,cte.date)<#end
)
select * from cte
That will generate a list of dates between #start & #end with month gaps.
You can
Use your real tables in place of the dummy dates
Perform an insert into...select ... from cte to insert your required data
If you can provide more detail about your table schema, I can probably help out with a more concrete example.
Something like this?
set #trans_date = ...
while #trans_date < #policy_end
begin
insert to tblUEPtmp
select trans_date, earned_premium
from tblpolicys
where {whatever}
set #trans_date = dateadd(mm, 1, #trans_date)
end

How can I create a list of weeks in access?

How do I create a query which breaks down a frequency of counts based on a list of weeks between 2 different dates in Access?
At the moment I have the following code in t-sql, but would like to have it run in Access.
declare #fromdate smalldatetime
declare #todate smalldatetime
declare #toptr smalldatetime
declare #fromptr smalldatetime
set #fromdate = '1/11/2010'
set #todate = '27/12/2010'
set #fromptr = dateadd(dd,1 - datepart(weekday,#fromdate), #fromdate)
while #fromptr < #todate
begin
print 'from: ' + cast(#fromptr as nvarchar) + ' --> ' + cast(#toptr as nvarchar)
set #fromptr = dateadd(dd,7, #fromptr)
set #toptr = dateadd(dd,7, #fromptr)
insert into #weeks values (#fromptr, #toptr)
end
I want to somehow bind some rows with lots of dates in them and aggregate them per 'week- ending date' from the dates creating in the table variable. Access doesn't seem to allow this kind of sql query, so was wondering if there was another way of doing this:
1) either by not using an intermediate table at all, 2) and/or converting the above code into access compatible
This will group by week (starting with Sunday) and be faster than other date calculation methods like DateAdd, DateDiff, DatePart, and Format.
SELECT
CDate((([DateColumn] - 1) \ 7) * 7 + 1) AS WeekStartingDate,
Sum([OrderCount]) AS SumOfOrders
FROM
Orders
GROUP BY
CDate((([DateColumn] - 1) \ 7) * 7 + 1);
If you want to see week ending date, add 7 at the end instead of 1. The GROUP BY expression can probably be just ([DateColumn] - 1) \ 7 but I'm not sure.
The backslash performs integer division, dividing by 7 converts a week of dates to a single integer, and the -1 adjusts for the fact that the "zero date" is a Saturday rather than a Sunday. To use a different starting day of the week, adjust the -1 and +1 by the same amount. To use Monday, for example, it would be -2 and +2.
This is language and region independent by depending on VB's internal representation of dates as numbers.
You can use Format in Access queries: http://msdn.microsoft.com/en-us/library/aa159657(v=office.10).aspx
SELECT Format(Date,"ww") FROM Table
GROUP BY Format(Date,"ww")
The plain-vanilla solution is to introduce a Calendar table which may look something like
Calendar
------------------------
FullDate date
CalendarYear integer
DayNumberInWeek integer
DayNumberInMonth integer
DayNumberInYear integer
DayNumberInEpoch integer
WeekNumberInYear integer
WeekNumberInEpoch integer
MonthNumberInYear integer
MonthNumberInEpoch integer
... and many more that you may need to group by
Then if you have table Counters
Counters
-----------
FullDate date
Value integer -- cumulative for one day
You can:
select
WeekNumberInYear
, sum(Value)
from Calendar as a
join Counters as b on b.FullDate = a.FullDate
where CalendarYear = 2010
group by WeekNumberInYear ;
The easiest way to populate the Calendar is to spend some time in Excel, create 10-20 years worth of rows and simply import in a DB.
Nothing specific to Access here, but hope you get the idea.