I am working on a CTE which calculate the recurrences of a Week but I am having some problems when the pattern cross the year.
The CTE should
Calculate all occurrences based on the following parameters:
Recurrence Count - how many times it will happen
Week Days - in which day of the week it will happen
Start Date - when the pattern calculation will start
Periodicity - How often in terms of weeks, i.e. 1 every week, 2 every 2 weeks
Starting Week - The Week number of the First Occurrence, which reflects the Start Date column
This is how I store the patterns:
/*
Pattern Table
*/
CREATE TABLE Pattern (
[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
, [Subject] [nvarchar](100) NULL
, [RecurrenceCount] [int] NULL
, [WeekDays] [varchar](max) NULL
, [StartDate] [datetime] NULL
, [EndDate] [datetime] NULL
, [Periodicity] [int] NULL
, [StartingWeek] [int] NULL
);
Below is a couple of patterns I am using to test my CTE:
/*
Pattern samples for test
*/
Insert into Pattern Values (N'Every 5 Weeks Fri, Sat, Sun', 72, 'Friday, Saturday, Sunday', N'2016-12-02', N'2016-12-02', 5, datepart(wk, N'2016-12-02'));
Insert into Pattern Values (N'Every 3 Weeks Tue, Wed, Thu', 20, 'Tuesday, Wednesday, Thursday', N'2016-11-01', N'2016-11-01', 3, datepart(wk, N'2016-11-01'));
I start counting considering first day of week Monday
SET DATEFIRST 1
And this is the CTE I am using to run this calculations:
/*
Display Patterns
*/
select * from Pattern
DECLARE #mindate DATE = (SELECT MIN(StartDate) FROM Pattern)
DECLARE #maxmindate DATE = (SELECT MAX(StartDate) FROM Pattern)
DECLARE #maxcount INT = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE #maxdate DATE = DATEADD(WK, #maxcount + 10, #maxmindate)
/*
CTE to generate required occurrences
*/
;With cteKeyDate As (
Select
KeyStartDate = #MinDate,
KeyDOW = DateName(WEEKDAY, #MinDate),
KeyWeek = datepart(WK,#MinDate)
Union All
Select
KeyStartDate = DateAdd(DD, 1, df.KeyStartDate) ,
KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)),
KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
From cteKeyDate DF
Where DF.KeyStartDate <= #MaxDate
)
SELECT
Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate)
FROM
(Select
A.Id
,A.StartDate
,A.EndDate
,Count = A.RecurrenceCount
,Days = A.WeekDays
,Every = A.Periodicity
,KeyStartDate = CASE
/*
if no periodicity (1) then it is sequential
if periodicity, first week doesn't apply (MIN KeyWeek)
*/
WHEN A.Periodicity = 1
OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
THEN KeyStartDate
/* Otherwise formula ADD WEEKS => Current Week Min Week */
ELSE
DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
END
,KeyDow
,KeyWeek
,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
,Periodicity = A.Periodicity
from
Pattern A
Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
) Final
Where
RowNr <= Count AND Id = 1
Option (maxrecursion 32767)
Now, if I test again my patterns, for example, the first one, I get this result, which has the bug when the occurrences happen in the next year. The RowNr 15 is wrong because it should happen on 23 of April (Sunday) and not the next week.
Id KeyStartDate KeyDow KeyWeek RowNr OccNr
1 02.12.2016 Friday 49 1 1
2 03.12.2016 Saturday 49 2 2
3 04.12.2016 Sunday 49 3 3
4 06.01.2017 Friday 50 4 4
5 07.01.2017 Saturday 50 5 5
6 08.01.2017 Sunday 50 6 6
7 10.02.2017 Friday 51 7 7
8 11.02.2017 Saturday 51 8 8
9 12.02.2017 Sunday 51 9 9
10 17.03.2017 Friday 52 10 10
11 18.03.2017 Saturday 52 11 11
12 19.03.2017 Sunday 52 12 12
13 21.04.2017 Friday 53 13 13
14 22.04.2017 Saturday 53 14 14
15 28.04.2013 Sunday 1 15 15
16 31.05.2013 Friday 2 16 16
17 01.06.2013 Saturday 2 17 17
While the second pattern is calculated just fine. I think I have a problem in the logic when the pattern cross the year and the number of weeks reset to 0 in SQL but I cannot find a solution, I struggled now for few days.
You can execute the code with the samples here.
Spend some time on this. Your calculation used is flawed. Unless there is special rules not indicated in your question I do not see why some dates are special. I prefer to use variable tables.
/*
Pattern Table
*/
DECLARE #Pattern TABLE(
[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
,[Subject] [nvarchar](100) NULL
,[RecurrenceCount] [int] NULL
,[WeekDays] [varchar](max) NULL
,[StartDate] [datetime] NULL
,[EndDate] [datetime] NULL
,[Periodicity] [int] NULL
,[StartingWeek] [int] NULL
);
/*
Populate with values based on Recurreance and Startdate. The startdate will give the start week, which make the start week obsolete.
*/
DECLARE #PreferredDate TABLE(
[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
,[PreferredDate] [datetime] NULL
,[PreferredWeek] [int] NULL
,[PreferredYear] [int] NULL
)
It is very important to always retrieve the current setting for datefirst. You will break someone else's calculation if they use another setting. I have also added the pattern id for obvious reasons.
DECLARE #DateFirst int = ##dateFirst --DATEFIRST is a global setting
DECLARE #mindate DATE = (SELECT MIN(StartDate) FROM #Pattern WHERE id=#PreferredSubjectID)
DECLARE #maxmindate DATE = (SELECT MAX(StartDate) FROM #Pattern WHERE Id=#PreferredSubjectID)
DECLARE #maxcount INT = (SELECT MAX(RecurrenceCount) FROM #Pattern WHERE Id=#PreferredSubjectID)
DECLARE #maxdate DATE = DATEADD(WK, #maxcount + 50, #maxmindate)
SET DATEFIRST 1
DECLARE #PreferredSubjectID int = 1
The #preferreddate table is populated using the following:
/*
CTE to generate required preferred dates
*/
;With ctePreferredDate AS (
Select PreferredDate = #MinDate, PreferredWeek = DATEPART(WK, #MinDate), PreferredYear = DATEPART(YYYY, #MinDate)
Union All
SELECT PreferredDate = DATEADD(WK,(SELECT Periodicity FROM #Pattern WHERE Id=#PreferredSubjectID), PreferredDate)
,PreferredWeek = DATEPART(WK,DATEADD(WK,(SELECT Periodicity FROM #Pattern WHERE Id=#PreferredSubjectID), PreferredDate))
,PreferredYear = DATEPART(yyyy,DATEADD(WK,(SELECT Periodicity FROM #Pattern WHERE Id=#PreferredSubjectID), PreferredDate))
From ctePreferredDate pFD
Where pFD.PreferredDate <= #MaxDate
)
INSERT INTO #PreferredDate (PreferredDate, PreferredWeek, PreferredYear)
SELECT PreferredDate, PreferredWeek, PreferredYear
FROM ctePreferredDate
The final CTE table is populated using the following:
/*
CTE to generate required occurrences
*/
;With cteKeyDate As (
Select KeyStartDate = #MinDate
,KeyDOW = DateName(WEEKDAY, #MinDate)
,KeyWeek = datepart(WK,#MinDate)
,id = #PreferredSubjectID
,KeyOccurrance = #maxcount
Union All
Select KeyStartDate = DateAdd(DD, 1, df.KeyStartDate)
,KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate))
,KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
,id=#PreferredSubjectID
,KeyOccurrance = #maxcount
From cteKeyDate DF
Where DF.KeyStartDate <= #MaxDate
)
SELECT StartDate
,[DayOfWeek]
,[Week]
,OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate)
FROM
(
SELECT cte.KeyStartDate AS StartDate
,cte.KeyDOW AS [DayOfWeek]
,cte.KeyWeek AS [Week]
,cte.id
,cte.KeyOccurrance AS Occurrance
,RowNr = ROW_NUMBER() OVER (PARTITION BY KeyOccurrance ORDER BY KeyStartDate)
FROM cteKeyDate cte
INNER JOIN
#PreferredDate pfd
ON cte.KeyWeek = pfd.PreferredWeek
AND YEAR(cte.KeyStartDate) = pfd.PreferredYear
WHERE cte.KeyDOW IN (SELECT LTRIM(RTRIM(Item)) FROM fn_SplitString((SELECT weekdays from #Pattern WHERE Id=1),','))
)cte
WHERE cte.RowNr <= cte.Occurrance
ORDER BY cte.StartDate
Option (maxrecursion 32767)
SET DATEFIRST #DateFirst --Quite important
Results
2016/12/02 Friday 49 1
2016/12/03 Saturday 49 2
2016/12/04 Sunday 49 3
2017/01/06 Friday 2 4
2017/01/07 Saturday 2 5
2017/01/08 Sunday 2 6
2017/02/10 Friday 7 7
2017/02/11 Saturday 7 8
2017/02/12 Sunday 7 9
2017/03/17 Friday 12 10
2017/03/18 Saturday 12 11
2017/03/19 Sunday 12 12
2017/04/21 Friday 17 13
2017/04/22 Saturday 17 14
2017/04/23 Sunday 17 15
2017/05/26 Friday 22 16
2017/05/27 Saturday 22 17
2017/05/28 Sunday 22 18
2017/06/30 Friday 27 19
2017/07/01 Saturday 27 20
2017/07/02 Sunday 27 21
2017/08/04 Friday 32 22
2017/08/05 Saturday 32 23
2017/08/06 Sunday 32 24
2017/09/08 Friday 37 25
2017/09/09 Saturday 37 26
2017/09/10 Sunday 37 27
2017/10/13 Friday 42 28
2017/10/14 Saturday 42 29
2017/10/15 Sunday 42 30
2017/11/17 Friday 47 31
2017/11/18 Saturday 47 32
2017/11/19 Sunday 47 33
2017/12/22 Friday 52 34
2017/12/23 Saturday 52 35
2017/12/24 Sunday 52 36
2018/01/26 Friday 4 37
2018/01/27 Saturday 4 38
2018/01/28 Sunday 4 39
2018/03/02 Friday 9 40
2018/03/03 Saturday 9 41
2018/03/04 Sunday 9 42
2018/04/06 Friday 14 43
2018/04/07 Saturday 14 44
2018/04/08 Sunday 14 45
2018/05/11 Friday 19 46
2018/05/12 Saturday 19 47
2018/05/13 Sunday 19 48
2018/06/15 Friday 24 49
2018/06/16 Saturday 24 50
2018/06/17 Sunday 24 51
2018/07/20 Friday 29 52
2018/07/21 Saturday 29 53
2018/07/22 Sunday 29 54
2018/08/24 Friday 34 55
2018/08/25 Saturday 34 56
2018/08/26 Sunday 34 57
2018/09/28 Friday 39 58
2018/09/29 Saturday 39 59
2018/09/30 Sunday 39 60
2018/11/02 Friday 44 61
2018/11/03 Saturday 44 62
2018/11/04 Sunday 44 63
2018/12/07 Friday 49 64
2018/12/08 Saturday 49 65
2018/12/09 Sunday 49 66
2019/01/11 Friday 2 67
2019/01/12 Saturday 2 68
2019/01/13 Sunday 2 69
2019/02/15 Friday 7 70
2019/02/16 Saturday 7 71
2019/02/17 Sunday 7 72
The splitstring function:
ALTER FUNCTION [dbo].[fn_SplitString](
#InputStr varchar(Max),
#Seperator varchar(10))
RETURNS #OutStrings TABLE (ItemNo int identity(1,1), Item varchar(256))
AS
BEGIN
DECLARE #Str varchar(2000),
#Poz int, #cnt int
--DECLARE #OutStrings TABLE (Item varchar(2000))
SELECT #Poz = CHARINDEX (#Seperator, #InputStr), #cnt = 0
WHILE #Poz > 0 AND #cnt <= 10000
BEGIN
SELECT #Str = SubString(#InputStr, 1, #Poz - 1)
INSERT INTO #OutStrings(Item) VALUES(#Str)
SELECT #InputStr = Right(#Inputstr, Len(#InputStr) - (len(#Str) + len(#Seperator)))
SELECT #Poz = CHARINDEX (#Seperator, #InputStr), #cnt = #cnt + 1
END
IF #InputStr <> ''
BEGIN
INSERT INTO #OutStrings(Item) VALUES(#InputStr)
END
RETURN
END
The reason you are getting that issue is you are using the datepart for getting the week numbers, so once the year changes the datepart goes back to 1. This should be changed...
I made this change and The dates now get in the order of sequence, check this out.
The change is the first CTE by the way.
/*
Display Patterns
*/
select * from Pattern
DECLARE #mindate DATE = (SELECT MIN(StartDate) FROM Pattern)
DECLARE #maxmindate DATE = (SELECT MAX(StartDate) FROM Pattern)
DECLARE #maxcount INT = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE #maxdate DATE = DATEADD(WK, #maxcount + 10, #maxmindate)
declare #minWeekPart INT = DATEPART(WK,#MinDate)
/*
CTE to generate required occurrences
*/
;With cteKeyDate As (
Select
KeyStartDate = #MinDate,
KeyDOW = DateName(WEEKDAY, #MinDate),
KeyWeek = #minWeekPart
Union All
Select
KeyStartDate = DateAdd(DD, 1, df.KeyStartDate) ,
KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)),
KeyWeek= #minWeekPart + datediff(WK,#MinDate,DateAdd(DD, 1, df.KeyStartDate))
From cteKeyDate DF
Where DF.KeyStartDate <= #MaxDate
)
--select * from cteKeyDate
-- order by 1
--Option (maxrecursion 32767)
SELECT
Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate)
FROM
(Select
A.Id
,A.StartDate
,A.EndDate
,Count = A.RecurrenceCount
,Days = A.WeekDays
,Every = A.Periodicity
,KeyStartDate = CASE
/*
if no periodicity (1) then it is sequential
if periodicity, first week doesn't apply (MIN KeyWeek)
*/
WHEN A.Periodicity = 1
OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
THEN KeyStartDate
/* Otherwise formula ADD WEEKS => Current Week Min Week */
ELSE
DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
END
,KeyDow
,KeyWeek
,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
,Periodicity = A.Periodicity
from
Pattern A
Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
) Final
Where
RowNr <= Count AND Id = 1
order by 2
Option (maxrecursion 32767)
I did run it in the rextester and you can find it here --> http://rextester.com/GWEY37271
Related
In SQL Server, I have a table of processes with a starttime and an endtime, from which I can calculate a duration using DATEDIFF.
Name StartTime EndTime
------------------------------------------------
process1 2016-10-10 11:10 2016-10-10 11:20
process2 2016-10-10 11:40 2016-10-10 12:30
How can I select the timespan of the process duration that occured in specific hours of the day (11 and 12) in seconds?
So process1 would be 10 minutes in hour 11.
process2 would be 20 minutes in hour 11, 30 minutes in hour 12.
Here's a sample that will generate hourly records for each source record. It uses a recursive CTE to move from the StartTime of each record through the EndTime. It may need some slight modifications to make it work in your case, but hopefully you can get the idea of how this method works.
Note that, as shown in the sample below, this will work properly even if the time span crosses a date boundary.
--==================================================================================
-- Do some quick setup to get a temporary table populated with data to use:
--==================================================================================
IF OBJECT_ID('tempdb..#ProcessHistory', 'U') IS NOT NULL DROP TABLE #ProcessHistory;
CREATE TABLE #ProcessHistory (
Name VARCHAR(20),
StartTime DATETIME,
EndTime DATETIME
)
INSERT INTO #ProcessHistory
VALUES ('process1', '2016-10-10 11:10', '2016-10-10 11:20'),
('process2', '2016-10-10 11:40', '2016-10-10 12:30'),
('process3', '2016-10-10 22:21', '2016-10-11 02:36');
--==================================================================================
-- Use a recursive CTE to generate hourly data for each record:
--==================================================================================
WITH HourlyData AS (
-- Anchor:
SELECT
ph.Name [ProcessName],
ph.StartTime [StartTime],
ph.EndTime [EndTime],
-- Get the current hour with date:
DATEADD(MINUTE, -DATEPART(MINUTE, ph.StartTime), ph.StartTime) [CurrentHour],
-- Calculate the next hour for use later:
DATEADD(MINUTE, 60 - DATEPART(MINUTE, ph.StartTime), ph.StartTime) [NextHour],
-- Determine how many minutes the process was active this hour:
CASE
WHEN DATEDIFF(MINUTE, ph.StartTime, ph.EndTime) > 60 - DATEPART(MINUTE, ph.StartTime)
THEN 60 - DATEPART(MINUTE, ph.StartTime)
ELSE DATEDIFF(MINUTE, ph.StartTime, ph.EndTime)
END [Minutes]
FROM #ProcessHistory ph
UNION ALL
-- Recurse:
SELECT
hd.ProcessName,
hd.StartTime,
hd.EndTime,
hd.NextHour [CurrentHour],
DATEADD(HOUR, 1, hd.NextHour) [NextHour],
-- Determine how many minutes the process was active this hour:
CASE
WHEN DATEDIFF(MINUTE, hd.NextHour, hd.EndTime) < 60
THEN DATEDIFF(MINUTE, hd.NextHour, hd.EndTime)
ELSE 60
END
FROM HourlyData hd
WHERE hd.NextHour < hd.EndTime
)
SELECT
hd.ProcessName,
hd.CurrentHour [HourWithDate],
CONVERT(DATE, hd.CurrentHour) [Date],
DATEPART(HOUR, hd.CurrentHour) [Hour],
hd.Minutes
FROM HourlyData hd
ORDER BY
hd.ProcessName,
hd.CurrentHour;
The output from the above sample would look like this:
ProcessName HourWithDate Date Hour Minutes
process1 2016-10-10 11:00:00.000 2016-10-10 11 10
process2 2016-10-10 11:00:00.000 2016-10-10 11 20
process2 2016-10-10 12:00:00.000 2016-10-10 12 30
process3 2016-10-10 22:00:00.000 2016-10-10 22 39
process3 2016-10-10 23:00:00.000 2016-10-10 23 60
process3 2016-10-11 00:00:00.000 2016-10-11 0 60
process3 2016-10-11 01:00:00.000 2016-10-11 1 60
process3 2016-10-11 02:00:00.000 2016-10-11 2 36
To handle general cases, you could try something like
--drop table #processes
CREATE TABLE #processes
(
name varchar(50),
StartTime Datetime,
EndTime DateTime
);
insert #processes VALUES('proc1','20161010 11:10','20161010 11:20');
insert #processes VALUES('proc2','20161010 11:40','20161010 12:20');
insert #processes VALUES('proc3','20161010 10:40','20161010 12:20');
;WITH HRS AS (SELECT 0 HR
UNION ALL
SELECT HR + 1 FROM HRS WHERE HR < 23),
MINS AS (SELECT 0 MN
UNION ALL
SELECT MN + 1 FROM MINS WHERE MN < 59),
TIMES AS (SELECT HR,MN FROM HRS CROSS JOIN MINS)
SELECT name,starttime,endtime,Count(0) AS mins FROM #processes
JOIN TIMES
ON (HR > datepart(hh,Starttime)
OR HR = datepart(hh,Starttime) AND MN >= datepart(n,STARTtIME))
AND
(HR < datepart(hh, EndTime)
OR HR = datepart(hh, EndTime) AND MN < datepart(n,EndTime))
WHERE HR = 11 --hour is 11
GROUP BY name,
starttime,
endtime
drop table #processes;
create table #temp (Name varchar(5), starttime datetime, EndTime datetime)
insert into #temp values(1,'2016-10-10 11:10','2016-10-10 11:20' )
insert into #temp values(2,'2016-10-10 11:40','2016-10-10 12:30' )
insert into #temp values(2,'2016-10-10 10:40','2016-10-10 11:30' )
insert into #temp values(2,'2016-10-10 10:40','2016-10-10 12:30' )
DECLARE #firstTime time ,#secondTime time
set #firstTime ='11:00'
set #secondTime ='12:00'
select
CASE WHEN CONVERT(time(0), starttime) < #firstTime AND CONVERT(time(0), EndTime) > #secondTime THEN DATEDIFF(ss,#firstTime,#secondTime)
WHEN CONVERT(time(0), EndTime) > #secondTime THEN DATEDIFF(ss,CONVERT(time(0), starttime),#secondTime)
WHEN CONVERT(time(0), starttime) < #firstTime THEN DATEDIFF(ss,CONVERT(time(0), EndTime),#secondTime)
ELSE DATEDIFF(ss,starttime,EndTime)
END
from #temp
SELECT CAST(DATEADD(MINUTE,DATEDIFF(MINUTE,'2016-10-10 11:10','2016-10-10 12:20'),'2011-01-01 00:00:00.000') AS TIME)
as timeDifference
With timeDifference -
SELECT CAST(DATEADD(MINUTE,DATEDIFF(MINUTE,StartTime,EndTime),'2011-01-01 00:00:00.000') AS TIME)
as timeDifference
from #YourTableName
With Days and TimeDiffernece
declare #start_time as varchar(150);
declare #end_time as varchar(150);
set #start_time='2016-10-10 10:10';
set #end_time='2016-10-12 12:10'
SELECT datediff(day,#start_time,#end_time) as dayDifference,
CAST(DATEADD(MINUTE,DATEDIFF(MINUTE,#start_time,#end_time),'2011-01-01 00:00:00') AS TIME(0))
as timeDifference
I think this does the trick, but it's pretty ugly. Maybe someone can do it more elegantly?
SELECT
case
when HOUR(starttime) < 11 AND HOUR(endtime) = 11 then minute(endtime)
when HOUR(starttime) < 11 AND HOUR(endtime) > 11 then 60
when HOUR(starttime) = 11 AND HOUR(endtime) = 11 then minute(endtime) - minute(starttime)
when HOUR(starttime) = 11 AND HOUR(endtime) > 11 then 60 - minute(starttime)
else 0
end AS ProcessTimeHour_11,
case
when HOUR(starttime) < 12 AND HOUR(endtime) = 12 then minute(endtime)
when HOUR(starttime) < 12 AND HOUR(endtime) > 12 then 60
when HOUR(starttime) = 12 AND HOUR(endtime) = 12 then minute(endtime) - minute(starttime)
when HOUR(starttime) = 12 AND HOUR(endtime) > 12 then 60 - minute(starttime)
else 0
end AS ProcessTimeHour_12
from StuffAndThings
I have this piece of code that I want to use to add an unique identifier to, but I cannot seem to get it to work correctly. Here is the code along with current output and desired output.
Begin
DECLARE #StartDate DATETIME,
#EndDate DATETIME,
#MonthList as Varchar(50),
#NewLeaseID as int,
#LeaseID as int,
#PropertyID as int,
#Amount as int,
#ExpectedID as int
SELECT
#StartDate = '20100501'
,#EndDate = '20100801'
,#leaseID = 6,
#PropertyID = 12,
#Amount = 600,
#ExpectedID = (SELECT ISNULL(MAX(ExpectedPaymentID) + 1, 1) FROM Payments_ExpectedPayments)
INSERT INTO Payments_ExpectedPayments(ExpectedPaymentID, Amount, PropertyID, LeaseID, Month)
SELECT
#ExpectedID as ExpectedPaymentID,
(x.number + 1) * #Amount as Amount,
#PropertyID as PropertyID,
#leaseID as Leaseid,
DATENAME(MONTH, DATEADD(MONTH, x.number, #StartDate)) AS Month
FROM
master.dbo.spt_values x
WHERE
x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate);
End
Output wanted:
ExpecedID PropertyID LeaseID Month Amount
1 12 13 Jan 600
2 12 13 Feb 1200
3 12 13 March 1800
4 12 13 April 2400
Output I'm currently getting:
ExpecedID PropertyID LeaseID Month Amount
1 12 13 Jan 600
1 12 13 Feb 1200
1 12 13 March 1800
1 12 13 April 2400
You have two options:
Your table design needs to be fixed. You can use an identity column for ExpectedId. Just change the CREATE TABLE from Expected int to Expected int identity(1,1).
You can use a ROW_NUMBER() on your INSERT like that: ROW_NUMBER() OVER(ORDER BY x.number) as ExpectedId
This is failing because you set the #ExpectedID once, before your select statement, hence you get all the same values.
Try substituting the #ExpectedID parameter with the following line in your SELECT statement.
(SELECT ISNULL(MAX(ExpectedPaymentID) + 1, 1) FROM Payments_ExpectedPayments) as ExpectedPaymentID
This will execute the select statement once per row, which I guess should work.
Please how may we do this:
1) Generate 24 rows one for each hour from current time back 24 hours
2) Aggregate data from another table over the past 24 hours into these 24 data points.
I have seen solutions suggesting number tables from 0-23, but these might make it difficult if we need this to start from NOW, then run back 24 hours Get every hour for a time range
e.g [5:00am, 4:00am, 3:00am ... 12:am, 11pm ... 7:00am,6:00am]
Source Table:
select d,h,count(1)cnt from msgs
where dt>= DateAdd(hh, -24, sysdatetime())
group by d,h order by 1 desc,2 desc
Sample Data
d h cnt
2015-06-05 16 11
2015-06-05 13 44
2015-06-05 12 16
2015-06-05 11 31
2015-06-05 10 10
2015-06-05 9 12
2015-06-05 8 1
2015-06-04 21 1
2015-06-04 20 2
2015-06-04 18 5
2015-06-04 16 2
I have missing hours, i would need a query that fills out the missing hours with 0
As an alternative solution, you could use this query to provide all 24 hour ranges. Then simply aggregate and sum these values against your original query to return only 24 rows.
;WITH hrs AS
(
SELECT h = 1
UNION ALL
SELECT h + 1
FROM hrs
WHERE h + 1 <= 24
)
SELECT
d = left(convert(varchar(50),DateAdd(hour, -1 * h, getdate()), 21),10),
h = DatePart(hour, DateAdd(hour, -1 * h, getdate())),
cnt = 0
FROM hrs
You could try joining to this function:
CREATE FUNCTION ufn_Last24Hrs
(
#start DateTime2(7)
)
RETURNS #Result TABLE (d char(10), h int)
AS
BEGIN
DECLARE #current DateTime2(7) = #start
WHILE (#current > DateAdd(hour, -24, #start))
BEGIN
INSERT INTO #Result
VALUES
(
REPLACE(CONVERT(char(10), #current, 102) , '.', '-'),
DATEPART(hour, #current)
)
SET #current = DateAdd(hour, -1, #current)
END
RETURN;
END;
GO
SELECT * FROM ufn_Last24Hrs(SYSDATETIME());
SELECT
d,h,COUNT(1)cnt
FROM
ufn_Last24Hrs(SYSDATETIME()) hrs
left join msgs
ON msgs.d = hrs.d
and msgs.h = hrs.h
WHERE dt>= DateAdd(hour, -24, SYSDATETIME())
GROUP BY d,h
ORDER BY 1 DESC, 2 DES
I have a query that groups data by month, but there are some months that do not display simply because there is no data to display/group by. Is it possible to return the months and 0s for those months?
Here is my query
DECLARE #IMPORT_DATE AS DATETIME
SET #IMPORT_DATE = GETDATE()
SELECT COALESCE(COUNT(*),0), RIGHT(YEAR_MONTH_VALUE,2)
FROM VW_CALLS
WHERE CLIENT_ID = 2
AND START_DATETIME BETWEEN DATEADD("m", -5, #IMPORT_DATE) AND #IMPORT_DATE
GROUP BY YEAR_MONTH_VALUE
ORDER BY YEAR_MONTH_VALUE
And it returns this:
(No column name) (No column name)
740 11
1929 12
3864 01
But I would like this:
(No column name) (No column name)
0 08
0 09
0 10
740 11
1929 12
3864 01
You can use a recursive CTE like this:
DECLARE #MonthStart CHAR(2) = '08'
;WITH MonthsRange AS (
SELECT m = #MonthStart, rn = 1
UNION ALL
SELECT m = (CASE WHEN m = '12' THEN CAST('01' AS CHAR(2))
ELSE CAST(REPLICATE('0', 2 - LEN(CAST((CAST(m AS INT) + 1) AS CHAR(2)))) +
CAST((CAST(m AS INT) + 1) AS CHAR(2)) AS CHAR(2))
END),
rn = rn + 1
FROM MonthsRange
WHERE rn < 6
)
SELECT *
FROM MonthsRange
in order to get all months you want to include in your final result set:
m rn
=======
08 1
09 2
10 3
11 4
12 5
01 6
Now you can left join the above CTE against you query to get the result set you want:
;WITH MonthsRange AS (
... cte statements here
)
SELECT COALESCE(vw.cnt, 0) AS cnt, mr.m AS [Month]
FROM MonthsRange AS mr
LEFT JOIN (
SELECT COALESCE(COUNT(*),0) AS cnt, RIGHT(YEAR_MONTH_VALUE,2) AS m,
YEAR_MONTH_VALUE AS ym
FROM VW_CALLS
WHERE CLIENT_ID = 2 AND
START_DATETIME BETWEEN DATEADD("m", -5, #IMPORT_DATE) AND #IMPORT_DATE
GROUP BY YEAR_MONTH_VALUE) AS vw ON mr.m = vw.m
ORDER BY vw.ym
The time is: (m/d/yyyy) => 2009/01/04
Using this command using datepart(wk,'20090104') I can get the week number (for any given date).
So :
SELECT datepart(wk,'20090101') //1
SELECT datepart(wk,'20090102') //1
SELECT datepart(wk,'20090103') //1
SELECT datepart(wk,'20090104') //2
So far so good.
The problem :
Those 3 first dates are not part of a full week, so I can't put them in a fixed 52-week chart.
Our company needs to see information about each whole week in the 52 weeks of a year. (Each year has 52 whole weeks).
So 20090101 doesn't belong to the first week of 2009 !
It belongs to the previous year (which is irrelevant to my question)
So I need a UDF (I've been searching a lot, and ISOWEEK is not answering my needs) which by a given datetime, will give me the Week Number (week = whole week, so partial weeks aren't considered).
Example :
calcweekNum ('20090101') //52 ...from the last year
calcweekNum ('20090102') //52 ...from the last year
calcweekNum ('20090103') //52 ...from the last year
calcweekNum ('20090104') //1
..
..
calcweekNum ('20090110') //1
calcweekNum ('20090111') //2
calcweekNum ('20090112') //2
...
Here's a different approach. All you need to supply is the year:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
x AS
(
SELECT md = MIN(d) FROM n
WHERE DATEPART(WEEKDAY, d) = 1 -- assuming DATEFIRST is Sunday
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, x.md)-1)/7) + 1
FROM n CROSS JOIN x
WHERE n.d >= x.md
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY [date];
Results:
date week
---------- ----
2009-01-04 1
2009-01-05 1
2009-01-06 1
2009-01-07 1
2009-01-08 1
2009-01-09 1
2009-01-10 1
2009-01-11 2
2009-01-12 2
...
2009-12-25 51
2009-12-26 51
2009-12-27 52
2009-12-28 52
2009-12-29 52
2009-12-30 52
2009-12-31 52
Note that week 52 won't necessarily be a full week, and that in some cases (e.g. 2012), the last day or two of the year might fall in week 53, so they're excluded.
An alternative approach is to repeat the MIN expression twice:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, (SELECT MIN(d)
FROM n WHERE DATEPART(WEEKDAY, d) = 1))-1)/7) + 1
FROM n
WHERE n.d >= (SELECT md = MIN(d) FROM n WHERE DATEPART(WEEKDAY, d) = 1)
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY d;
Here's a function for you to calculate it on the fly:
CREATE FUNCTION dbo.WholeWeekFromDate (
#Date datetime
)
RETURNS tinyint
AS BEGIN
RETURN (
SELECT DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1
FROM (SELECT DateAdd(Day, (DateDiff(Day, 0, #Date) + 1) / 7 * 7, 0)) X (CalcDate)
);
END;
I don't recommend you use it, as it may perform badly due to being called once for every row. If you absolutely must have a function to use in real queries, then convert it to an inline function returning a single column and row, and use it as so:
SELECT
OtherColumns,
(SELECT WeekNumber FROM dbo.WholeWeekFromDate(DateColumn)) WeekNumber
FROM
YourTable;
This will allow it to be "inlined" in the execution plan and perform significantly better.
But even better, as others have suggested, is to use a BusinessDate table. Here's a head start on creating one for you:
CREATE TABLE dbo.BusinessDate (
BusinessDate date NOT NULL CONSTRAINT PK_BusinessDate PRIMARY KEY CLUSTERED,
WholeWeekYear smallint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekYear_Valid
CHECK (WholeWeekYear BETWEEN 1900 AND 9999),
WholeWeekNumber tinyint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekNumber_Valid
CHECK (WholeWeekNumber BETWEEN 1 AND 53),
Holiday bit CONSTRAINT DF_BusinessDate_Holiday DEFAULT (0),
Weekend bit CONSTRAINT DF_BusinessDate_Weekend DEFAULT (0),
BusinessDay AS
(Convert(bit, CASE WHEN Holiday = 0 AND Weekend = 0 THEN 1 ELSE 0 END)) PERSISTED
);
And I'll even populate it from 1900-01-01 through 2617-09-22 (Is that enough for the projected life of your product? And it's only 7.8MB so don't fret over size):
WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
B (N) AS (SELECT 1 FROM A F, A A, A L, A C, A O, A N),
C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B),
Dates AS (
SELECT
N,
DateAdd(Day, N, '18991231') Dte,
DateAdd(Day, N / 7 * 7, '19000101') CalcDate
FROM C
)
INSERT dbo.BusinessDate
SELECT
Dte,
Year(CalcDate),
DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1,
0,
(N + 6) % 7 / 5 -- calculate weekends
FROM Dates; -- 3-7 seconds or so on my VM server
Then join to the table on the date, and use the WholeWeekNumber column for your output. You might also consider adding a WeekNumberYear because it's going to be a tad difficult to figure out that the 52 of 2009-01-01 really belongs to 2008 without this... a strange data point in there for sure if you don't (laugh).
Example table contents:
BusinessDate WholeWeekYear WholeWeekNumber Holiday Weekend BusinessDay
------------ ------------- --------------- ------- ------- -----------
1/1/2009 2008 52 0 0 1
1/2/2009 2008 52 0 0 1
1/3/2009 2008 52 0 1 0
1/4/2009 2009 1 0 1 0
1/5/2009 2009 1 0 0 1
1/6/2009 2009 1 0 0 1
1/7/2009 2009 1 0 0 1
1/8/2009 2009 1 0 0 1
1/9/2009 2009 1 0 0 1
1/10/2009 2009 1 0 1 0
1/11/2009 2009 2 0 1 0
If you really don't want to use this as a general business date calculation table, you can drop the last 3 columns, otherwise, update the Holiday column to 1 for company holidays.
Note: if you actually make the above table, and your access to it most often uses JOIN or WHERE conditions on a different column than BusinessDate, then make the primary key nonclustered and add a clustered index starting with the alternate column.
Some of the above scripts require SQL 2005 or higher.
It would be relatively easy to setup a custom calendar table with one row for each date of the year in it, and then have other fields that will allow you to rollup however you want. I do this when I have clients using varying calendars, i.e. fiscal years, and it makes the query logic very simple.
Then you just join date-to-date and get the week-number that you want.
date | reporting year | reporting week
-----------|----------------|---------------
2009-01-01 | 2008 | 52
2009-01-02 | 2008 | 52
2009-01-03 | 2008 | 52
2009-01-04 | 2009 | 01
2009-01-05 | 2009 | 01
etc.
and then to use it ( for example to get total sales rollup by your custom weeks, didn't validated my sql):
select reporting_year, reporting_month, sum(sales)
from sales
inner join custom_date_table cdt on cdt.sysdate = sales.sysdate
group by reporting_year, reporting_month
where report_year=2009
DECLARE #StartDate DATE;
SET #StartDate = '20120101';
WITH Calendar AS (
SELECT #StartDate AS DateValue
,DATEPART(DW, #StartDate) AS DayOfWeek
,CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END AS WeekNumber
UNION ALL
SELECT DATEADD(d, 1, DateValue)
,DATEPART(DW, DATEADD(d, 1, DateValue)) AS DayOfWeek
,CASE WHEN DayOfWeek = 7 THEN WeekNumber + 1 ELSE WeekNumber END
FROM Calendar
WHERE DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
)
SELECT DateValue, WeekNumber
FROM Calendar
WHERE WeekNumber BETWEEN 1 AND 52
AND DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
OPTION (MAXRECURSION 0);
Don't use a UDF, use a calendar table instead, then you can define week numbers exactly as your company requires and simply query them from the table, which will be much easier and possibly much faster than using a UDF.
A calendar table has numerous uses in SQL (search this site or Google) so you should probably create it anyway.
There is no good answer for this.
A year is NOT 52 weeks long.
It is 52 weeks and one day in normal years, and 52 weeks and two days in leap years.