Why is there an Overflow in this DateTime? - sql-server

I am trying to create a dynamic calendar where I can ignore weekends and skip ahead to 45 days from now
Declare #StartDate Date = '12/01/2015'
;With NumberList AS
(
Select *
From
(
Select Rank() Over(Order By S1.Id, S2.Id) Number
From master.dbo.SysObjects S1, master.dbo.SysObjects S2
) XX
Where Number < 1000
)
,FullCalendar as
(
select Cast (dateadd(day, number, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
from Numberlist
)
,CalendarWithNumbers As
(
Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
From FullCalendar
Where ((DATEPART(dw, CalendarDate) + ##DATEFIRST) % 7) NOT IN (0, 1)
)
,StartingDate as
(
Select *
From CalendarWithNumbers
Where 1=1
And CalendarDate = #StartDate
)
Select #StartDate StartDate, CalendarDate NDaysFromNow, DayNumber
From CalendarWithNumbers
Where DayNumber = (Select DayNumber + 45 from StartingDate)
However, I get an an Overflow Error
StartDate NDaysFromNow DayNumber
---------- ------------ --------------------
Msg 517, Level 16, State 1, Line 3
Adding a value to a 'datetime' column caused an overflow.
However, I am only choosing the first 1000 numbers in my first CTE, and my starting date is the first day of the year. 1/1/2015 + 1000 days only goes up to 9/26/2017.
Why I am getting an Overflow Error?
EDIT - Comments added to explain code
I added a comments to the code to make it easier to understand.
Declare #StartDate Date = '12/01/2015'
;With NumberList AS
(
/* Get a list of 1000 Numbers*/
Select *
From
(
/*
Get a sequence of Numbers (I get 2071*2071 = 4289041)
I may need to go up to 50 years in the future so I have to do a cartesian product (same as Cross Join)
However, this is where it fails
*/
Select Rank() Over(Order By S1.Id, S2.Id) Number
From master.dbo.SysObjects S1, master.dbo.SysObjects S2
/*
comment out the Select ABOVE
Uncomment the Select BELOW
It works!!
Probably because now the sequencing only goes up to 2071
*/
--Select Rank() Over(Order By S1.Id) Number
--From master.dbo.SysObjects S1
) XX
Where Number < 1000
)
--Select * From NumberList
--UnComment just the line above, and comment everything below to see the result just up to there
,FullCalendar as
(
/* Take the first day of the year, and add 1 to 1000 days to it
It starts from 2015-01-02 (bug needs to be fixed so it starts from 2015-01-01)
It ends at 2017-09-26
*/
select Cast (dateadd(day, number, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
from Numberlist
)
--Select * From FullCalendar
--UnComment just the line above, and comment everything below to see the result just up to there
,CalendarWithNumbers As
(
/*
WHERE CLAUSE exclude the weekend days
ROW_NUMBER now number each day sequentially and save it in the DayNumber column
*/
Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
From FullCalendar
Where ((DATEPART(dw, CalendarDate) + ##DATEFIRST) % 7) NOT IN (0, 1)
)
--Select * From CalendarWithNumbers
--UnComment just the line above, and comment everything below to see the result just up to there
,StartingDate as
(
/* Get the DayNumber (from the query above) for the starting day*/
Select *
From CalendarWithNumbers
Where 1=1
And CalendarDate = #StartDate
)
--Select * From StartingDate
--UnComment just the line above, and comment everything below to see the result just up to there
/*
Now calculate 45 days from the daynumber in starting date
that gives you the value for NDays from StartDate
*/
Select
SD.CalendarDate,
Sd.DayNumber,
CWN.CalendarDate NDaysFromNow,
CWN.DayNumber
From CalendarWithNumbers CWN
Inner Join StartingDate SD
On CWN.DayNumber = SD.DayNumber + 345

My guess is that SQL Server decides to do things in different order than you think, and actually this:
;With NumberList AS
(
Select *
From
(
Select Rank() Over(Order By S1.Id, S2.Id) Number
From master.dbo.SysObjects S1, master.dbo.SysObjects S2
) XX
Where Number < 1000
)
Doesn't get filtered to 999 rows before this happens;
select Cast (dateadd(day, number, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
Even though that's what you expect to happen. If you change it to this, the error will go away:
;With NumberList AS
(
Select top 999 row_number() Over(Order By (select null)) Number
From master.dbo.SysObjects S1 cross join master.dbo.SysObjects S2
)
Using top is usually a lot better option, because that gives the optimizer a better understanding about how many rows will be actually coming from there, instead of having to decide when to filter out rows with Number >= 1000 (or what the distribution of the numbers might be)

This works ,,,, just replaced the query that generates the NumberList
Declare #StartDate Date = '12/01/2015' --<-- I would use '20151201'
;With NumberList AS
(
Select *
From
(
Select ROW_NUMBER() Over(Order By (SELECT NULL)) Number
From master..spt_values S1
) XX
Where Number < 1000
)
,FullCalendar as
(
select Cast (dateadd(day, number, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
from Numberlist
)
,CalendarWithNumbers As
(
Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
From FullCalendar
Where ((DATEPART(dw, CalendarDate) + ##DATEFIRST) % 7) NOT IN (0, 1)
)
,StartingDate as
(
Select *
From CalendarWithNumbers
Where 1=1
And CalendarDate = #StartDate
)
Select #StartDate StartDate, CalendarDate NDaysFromNow, DayNumber
From CalendarWithNumbers
Where DayNumber = (Select DayNumber + 45 from StartingDate)

Related

Add a row for every day between two dates with number of hours in SQL Server

Starting data:
Desired results something like this:
So it calculated the number of hours until the end of StartDateTime, if the EndDateTime is greater than end of day for StartDateTime. Then for every full day in between, it calculates 24 hours (this could stretch numerous days). And then when it gets to the EndDateTime - it calculates time from midnight (morning) to EndDateTime
I'm reading that I will probably need to use a recursive CTE, but I don't have any experience with recursions and am struggling.
this might get tricky, but I guess it can be solved using so called number table - i.e. table which has only one column populated with number sequence. In our case 0 based sequence.
The trick here is to get the number of days between start and end datetime. This value used in join between the data table and the numbers table will create the needed extra rows for each per day interval.
Of course we also have to setup properly start and end datetime of each day interval (CASE terms in the CTE)
Then we get for each per day interval number of minutes and divide by 60 to get proper decimal value.
Hope this helps.
Lets see the code:
-- input data
DECLARE #v_Dates TABLE
(
id varchar(20),
StartDateTime SMALLDATETIME,
EndDateTime SMALLDATETIME
)
INSERT INTO #v_Dates (id, StartDateTime, EndDateTime)
VALUES ('example 1', '02-17-2019 0:45', '02-19-19 12:30'),
('example 2', '02-21-2019 18:00', '02-22-19 12:15'),
('example 3', '02-22-2019 20:15', '02-22-19 20:30');
-- so called Number table which holds numbers 0 - 9999 in this case
DECLARE #v_Numbers TABLE
(
Number INT
);
-- populating the number table
INSERT INTO #v_Numbers
SELECT TOP 10000 ROW_NUMBER() OVER(ORDER by t1.number) - 1 as Number
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
-- we parse the dates into the per day intervals
;WITH IntervalsParsed(id, StartDateTime, EndDateTime, Number, IntervalStartDateTime, IntervalEndDateTime) AS
(
SELECT id
,StartDateTime
,EndDateTime
,Number
, InervalStartDateTime = CASE
WHEN D.StartDateTime > DATEADD(day, DATEDIFF(day, 0, D.StartDateTime), N.Number) THEN D.StartDateTime
ELSE DATEADD(day, DATEDIFF(day, 0, D.StartDateTime), N.Number)
END
, IntervalEndDateTime = CASE
WHEN D.EndDateTime < DATEADD(day, DATEDIFF(day, 0, D.StartDateTime), N.Number + 1) THEN D.EndDateTime
ELSE DATEADD(day, DATEDIFF(day, 0, D.StartDateTime), N.Number + 1)
END
FROM #v_Dates D
--this join basically creates the needed number of rows
INNER JOIN #v_Numbers N ON DATEDIFF(day, D.StartDateTime, D.EndDateTime) + 1 > N.Number
)
-- final select
SELECT id
, StartDateTime
, EndDateTime
, IntervalStartDateTime
, IntervalEndDateTime
, Number
, DecimalValue = CAST( DATEDIFF(minute, IntervalStartDateTime, IntervalEndDateTime) AS DECIMAL)/60
FROM IntervalsParsed
ORDER BY id, Number
Just another option is an ad-hoc tally table in concert with a CROSS APPLY
Example
Select A.[column1]
,A.[StartDateTime]
,A.[EndDateTime]
,Hours = sum(1) / 60.0
From #YourTable A
Cross Apply (
Select Top (DateDiff(MINUTE,[StartDateTime],[EndDateTime])+1)
D=DateAdd(MINUTE,-1+Row_Number() Over (Order By (Select Null)),[StartDateTime])
From master..spt_values n1,master..spt_values n2
) B
Group By [column1],[StartDateTime],[EndDateTime],cast(D as Date)
Returns
This may be little complicated, but here is one way to use recursive cte to get the output. You can add the start date with one day as long as it is less than end date of your column. Also declared a Static value to make sure we can get difference of 24 hours.
--Create a table
Select 'example1' exm, '2019-02-17 00:45:00' startdate, '2019-02-19 12:30:00' Enddate into #temp union all
Select 'example2' exm, '2019-02-21 18:00:00' startdate, '2019-02-22 12:15:00' Enddate union all
Select 'example3' exm, '2019-02-22 20:15:00' startdate, '2019-02-22 20:30:00' Enddate
Declare #datevalue time = '23:59:59'
;with cte as (select exm, startdate, enddate, case when datediff(day, startdate, enddate) = 0 then datediff(SECOND, startdate, enddate)
when datediff(day, startdate, enddate)>0 then
datediff(SECOND, cast(startdate as time), #datevalue)
end as Hoursn, cast(dateadd(day, 1,cast(startdate as date)) as smalldatetime) valueforhours from #temp
union all
select exm, startdate, enddate, case when datediff(day, valueforhours, enddate) = 0 then datediff(SECOND, valueforhours, enddate)
when datediff(day, valueforhours, enddate)>0 then datediff(SECOND, cast(valueforhours as time), #datevalue) end as Hoursn, case when datediff(day,valueforhours, enddate) > 0 then dateadd(day,1,valueforhours) end as valueforhours
from cte
where
valueforhours <= cast(enddate as date)
)
select exm, startdate, Enddate, round(Hoursn*1.0/3600,2) as [hours] from cte
order by exm
Output:
exm startdate Enddate hours
example1 2019-02-17 00:45:00 2019-02-19 12:30:00 23.250000
example1 2019-02-17 00:45:00 2019-02-19 12:30:00 24.000000
example1 2019-02-17 00:45:00 2019-02-19 12:30:00 12.500000
example2 2019-02-21 18:00:00 2019-02-22 12:15:00 6.000000
example2 2019-02-21 18:00:00 2019-02-22 12:15:00 12.250000
example3 2019-02-22 20:15:00 2019-02-22 20:30:00 0.250000

Calculating every month 8th business day

How to get 8th Business day, start checking to see if the data exists. If it does exist Set and represents more than 50% of the loans - Set the Verified Date to that Day
Thank You Regards Shehroz
Declare #D date = '2012-12-01' -- Supply 1st of Month
Select D=max(D)
From (
Select Top 8 D=DateAdd(DD,N,#D)
From (Select N From (Values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13)) N(N) ) A
Where DatePart(DW,DateAdd(DD,N,#D)) between 2 and 6
) A
Returns
2012-12-12
This will give you the result, however my answer lists all 8th business days from specific date.
It does not consider holidays though. With small changes you can consider holidays too.
You can use top 1 on the last select to get only one date.
DECLARE #start date
SELECT #start = '20160101'
;WITH n AS (
SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects
), dates AS (
SELECT DATEADD(DAY, n - 1, #start) Dt
FROM n
), dayNum AS (
SELECT Dt, DATENAME(WEEKDAY, Dt) WeekDayName
, ROW_NUMBER() OVER (ORDER BY Dt) DayNumber
FROM dates
WHERE DATENAME(WEEKDAY, Dt) NOT IN ('Saturday', 'Sunday')
)
SELECT Dt, DATENAME(WEEKDAY, Dt) WeekDayName
FROM dayNum
WHERE DayNumber % 8 = 0
ORDER BY Dt

TSQL get last day of previous months upto a specified month

I need to get last day of all previous months including current month, upto a specified month. For example, I need last days of september, aug, july, june, may, april, march, feb, jan, dec 2015 like so:
temptable_mytable:
last_day_of_month
-----------------
2016-09-30
2016-08-31
2016-07-31
2016-06-30
2016-05-31
2016-04-30
2016-03-31
2016-02-30
2016-01-31
2015-12-31
I need to specify the month and year to go back to - in above case it's December 2015, but it could also be September 2015 and such. Is there a way that I can do a loop and do this instead of having to calculate separately for each month end?
Use a recursive CTE with the EOMONTH function.
DECLARE #startdate DATE = '2016-01-01'
;WITH CTE
AS
(
SELECT EOMONTH(GETDATE()) as 'Dates'
UNION ALL
SELECT EOMONTH(DATEADD(MONTH, -1, [Dates]))
FROM CTE WHERE Dates > DATEADD(MONTH, 1, #startdate)
)
SELECT * FROM CTE
with temp as (select -1 i union all
select i+1 i from temp where i < 8)
select DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+i*-1,0)) from temp
declare #LASTMONTH date = '2018-10-01';
WITH MTHS AS (
SELECT dateadd(month,month(getdate()),dateadd(year,year(getdate()) - 1900, 0)) aday
UNION ALL
SELECT DATEADD(month,1,aday) from MTHS WHERE aday <= #LASTMONTH
),
LASTDAYS AS (SELECT DATEADD(day,-1,aday) finaldayofmonth from MTHS)
select * from LASTDAYS
Here is a version that goes forward or backwards as appropriate
declare #LASTMONTH date = '2013-10-01';
WITH DIF AS (SELECT CASE WHEN
YEAR(#LASTMONTH) * 12 + MONTH(#LASTMONTH)
>= YEAR(GETDATE()) * 12 + MONTH(getdate()) THEN 1 ELSE -1 END x),
MTHS AS (
SELECT dateadd(month,month(getdate()),dateadd(year,year(getdate()) - 1900, 0)) aday
UNION ALL
SELECT DATEADD(month,(SELECT X from dif),aday) from MTHS
WHERE month(aday) != month(dateadd(month,1,#LASTMONTH)) or YEAR(aday) != YEAR(dateadd(month,1,#LASTMONTH))
),
LASTDAYS AS (SELECT DATEADD(day,-1,aday) finaldayofmonth from MTHS)
select * from LASTDAYS order by finaldayofmonth
Here's one approach, using a CTE to generate a list of incrementing numbers to allow us to then have something to select from and use in a DATEADD to go back for the appropriate number of months.
Typically, if you're doing this quite frequently, instead of generating numbers on the fly like this with the CROSS JOIN, I'd recommend just creating a "Numbers" table that just holds numbers from 1 to "some number high enough to meet your needs"
DECLARE #Date DATE = '20151201'
DECLARE #MonthsBackToGo INTEGER
SELECT #MonthsBackToGo = DATEDIFF(mm, #Date, GETDATE()) + 1;
WITH _Numbers AS
(
SELECT TOP (#MonthsBackToGo) ROW_NUMBER() OVER (ORDER BY o.object_id) AS Number
FROM sys.objects o
CROSS JOIN sys.objects o2
)
SELECT EOMONTH(DATEADD(mm, -(Number- 1), GETDATE())) AS last_day_of_month
FROM _Numbers
This should scale out no matter how far you go back or forward for your originating table or object.
SET NOCOUNT ON;
DECLARE #Dates TABLE ( dt DATE)
DECLARE #Start DATE = DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()), 0)
DECLARE #End DATE = DATEADD(YEAR, 1, #Start)
WHILE #Start <= #End
BEGIN
INSERT INTO #Dates (dt) VALUES (#Start)
SELECT #Start = DATEADD(DAY, 1, #Start)
END
; With x as
(
Select
dt
, ROW_NUMBER() OVER(PARTITION BY DATEPART(YEAR, Dt), DATEPART(MONTH, Dt) ORDER BY Dt Desc) AS rwn
From #Dates
)
Select *
From x
WHERE rwn = 1
ORDER BY Dt
This was cribbed together quick based on a couple different SO answers for the parts:
DECLARE #startdate datetime, #enddate datetime
set #startdate = '2015-12-01'
set #enddate = getdate()
;WITH T(date)
AS
(
SELECT #startdate
UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE T.date < #enddate
)
SELECT DISTINCT
DATEADD(
day,
-1,
CAST(CAST(YEAR(date) AS varchar) + '-' + CAST(MONTH(date)AS varchar) + '-01' AS DATETIME))
FROM T OPTION (MAXRECURSION 32767);

Unexpected results with SELECT INTO

I've got a table valued function created in MSSQL that takes in 2 paramateres
1. End Date
2. Number of weeks to go back
to generate a table of dates with the week start date.
The PERIOD table simply is a table with 1 column (called pPeriod) with all the dates between '19971229' and '20201231'
CREATE FUNCTION [dbo].[Get_Week_Rank]
(
-- Add the parameters for the function here
#PERIOD_END DATETIME,
#NUM_WEEKS INT
)
RETURNS TABLE
AS
RETURN
(
SELECT A.PPERIOD TY_PPERIOD, B.PPERIOD TY_PWKSTART, DATEADD(YY, -1, A.PPERIOD) LY_PPERIOD, DATEADD(YY, -1, B.PPERIOD) LY_PWKSTART, B.CRNK WEEK_RANK FROM (
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY PPERIOD) CRNK FROM PERIODS
WHERE PPERIOD BETWEEN DATEADD(WW, #NUM_WEEKS - 1, #PERIOD_END) + 1 AND #PERIOD_END
) AS A
JOIN (
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY CRNK % 7) CRNK FROM (
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY PPERIOD) CRNK FROM PERIODS
WHERE PPERIOD BETWEEN DATEADD(WW, #NUM_WEEKS, #PERIOD_END) + 1 AND #PERIOD_END
) AS A
WHERE CRNK % 7 = 1
) AS B ON (A.CRNK - 1)/7 = B.CRNK
)
I noticed then when #NUM_WEEKS is between -1 and -130, the results are correct when running this query:
SELECT * INTO #WEEKS FROM GET_WEEK_RANK('20160401', -104)
SELECT * FROM #WEEKS ORDER BY 1
However, any number below -130 (eg -156, -208), the returned results are all wrong.
Wrong results
You can see that TY_PWKSTART is all jumbled up and not in sync with TY_PPERIOD.
If I run the query directly, the results return fine:
SELECT * FROM GET_WEEK_RANK('20160401', -140)
What could be the issue? I am using Microsoft SQL Server 2014
EDIT: Posting images of results
As you can see, both queries essentially do the same thing, but the results returned are different. The order of pWkStart in the first query when using SELECT INTO is wrong.
Wrong results:
SELECT * INTO #WEEK_WRONG FROM GET_WEEK_RANK('20160410', -140)
SELECT * FROM #WEEK_WRONG ORDER BY 1
Correct results:
CREATE TABLE #WEEK_CORRECT (TY_PPERIOD DATETIME, TY_PWKSTART DATETIME, LY_PPERIOD DATETIME, LY_PWKTART DATETIME, WEEK_RANK INT)
INSERT INTO #WEEK_CORRECT
SELECT * FROM GET_WEEK_RANK('20160410', -140)
SELECT * FROM #WEEK_CORRECT ORDER BY 1
EDIT2:
Turns out that my initial query was producing unexpected results. I've fixed my query and managed to get consistent results from SELECT INTO and INSERT INTO.
Just sharing the code here:
CREATE FUNCTION [dbo].[Get_Week_Rank]
(
#PERIOD_END DATETIME,
#NUM_WEEKS INT
)
RETURNS TABLE
AS
RETURN
(
SELECT A.PPERIOD TY_PPERIOD, B.PPERIOD TY_PWKSTART, DATEADD(YY, -1, A.PPERIOD) LY_PPERIOD, DATEADD(YY, -1, B.PPERIOD) LY_PWKSTART, B.CRNK + 1 WEEK_RANK FROM (
SELECT PPERIOD, (ROW_NUMBER() OVER (ORDER BY PPERIOD)-1)/7 CRNK FROM PERIODS
WHERE PPERIOD BETWEEN DATEADD(WW, #NUM_WEEKS, #PERIOD_END) + 1 AND #PERIOD_END
) AS A
JOIN (
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY PPERIOD)-1 CRNK FROM (
SELECT PPERIOD , ROW_NUMBER() OVER (PARTITION BY CRNK ORDER BY CRNK) CRNK FROM (
SELECT PPERIOD, (ROW_NUMBER() OVER (ORDER BY PPERIOD)-1)/7 CRNK FROM PERIODS
WHERE PPERIOD BETWEEN DATEADD(WW, #NUM_WEEKS, #PERIOD_END) + 1 AND #PERIOD_END
) AS A
) AS A
WHERE CRNK = 1
) AS B ON A.CRNK = B.CRNK
)
This part of your query is broken:
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY CRNK % 7) CRNK FROM (
...
) AS A
WHERE CRNK % 7 = 1
Since the where clause establishes that CRNK % 7 is equal to 1, the ROW_NUMBER() expression is free to assign row numbers in any order1. I would guess that you still would want to assign row numbers in the order in which PPERIOD or CRNK values work, and so the expression should instead by:
SELECT PPERIOD, ROW_NUMBER() OVER (ORDER BY CRNK) CRNK FROM (
...
) AS A
WHERE CRNK % 7 = 1
1Since you haven't provided enough expressions in the ORDER BY for row numbers to be assigned unambiguously, there's no guarantee on the values assigned to each row.

How to insert all dates in 2015 in Date field?

I am using SQL Server and have a table (Table_Date) with a 'date' field. I want to insert all 2015 dates in this field.
It should have 365 distinct rows, 1 row for each day of 2015.
One method is with a recursive CTE:
with dates as (
select cast('2015-01-01' as date) as thedate
union all
select dateadd(day, 1, thedate)
from dates
where thedate < '2015-12-31'
)
select *
from dates
option (maxrecursion 0);
An alternative is to use a table that has at least 365 rows. master..spt_values is often used for this purpose:
select dateadd(day, seqnum - 1, '2015-01-01')
from (select row_number() over (order by ()) as seqnum
from master..spt_values
) t
where seqnum <= 365;
Here's one way:
CREATE TABLE #nums(num INT);
INSERT INTO #nums VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
WITH cteDays AS
(
SELECT 100*d100.num + 10*d10.num + d1.num AS YearDay
FROM #nums AS d1
CROSS JOIN #nums AS d10
CROSS JOIN #nums AS d100
WHERE d100.num <=3
)
SELECT CAST('2015-01-01' AS DATETIME) + YearDay AS YearDate
FROM cteDays
WHERE YEAR(CAST( CAST('2015-01-01' AS DATETIME) + YearDay AS DATETIME)) = 2015
Something like this could work as well:
declare #count int = 0
while (#count < 365)
begin
--make this the insert
select DATEADD(DD, #count, getdate())
set #count = #count + 1
end
Not sure what context this will applied to though... This is very basic, but if this is a one-time event it won't matter.

Resources