Slow performance on Recursive CTE in T-SQL - sql

I have the following code, (based on the community member help):
use [Credible];
WITH DataSource AS
(
SELECT
ROW_NUMBER() OVER(ORDER BY epi.[client_id]) AS [row_id]
,epi.[client_id]
,CONVERT(date, epi.[admission_date]) AS [admission_date]
,CONVERT(date, DATEADD(MONTH, 3, epi.[admission_date])) AS [3Month Date]
,CONVERT(date, ISNULL(epi.[discharge_date], GETDATE())) AS [discharge_date]
FROM
[dbo].[ClientEpisode] epi
WHERE DATEADD(MONTH, 3, [admission_date]) <= ISNULL([discharge_date], GETDATE())
),
RecursiveDataSource AS
(
SELECT
[row_id]
,[client_id]
,[admission_date]
,[3Month Date]
,[discharge_date]
,0 AS [level]
FROM
DataSource
UNION ALL
SELECT
ds.[row_id]
,ds.[client_id]
,ds.[admission_date]
,DATEADD(MONTH, 3, rds.[3Month Date])
,ds.[discharge_date]
,[level] + 1
FROM
RecursiveDataSource rds
INNER JOIN DataSource ds ON
rds.[row_id] = ds.[row_id] AND DATEADD(MONTH, 3, rds.[3Month Date]) < ds.[discharge_date]
)
SELECT *
FROM RecursiveDataSource
ORDER BY [row_id]
,[level]
-- OPTION (MAXRECURSION 32767);
This code works with the speed around 30 sec if there are up to 1 000 records in the table.
But my table is over 14 000 records, and will grow more,
and the code works 10+++ min
Is there way to make its performance in 30 sec or so?
Thank you for help

As here you are looking for performance solution, I will advice to skip using recursive CTE and solve the issue in another way.
For example, a variant of this:
DECLARE #DataSource TABLE
(
[client_id] INT
,[adm_date] DATE
,[disch_date] DATE
);
INSERT INTO #DataSource ([client_id], [adm_date], [disch_date])
VALUES (1002, '3/11/2005 ', '5/2/2005')
,(1002, '8/30/2005 ', '2/16/2007')
,(1002, '3/16/2017 ', NULL);
DROP TABLE IF EXISTS #DataSource;
DROP TABLE IF EXISTS #DataNumers;
CREATE TABLE #DataSource
(
[client_id] INT
,[adm_date] DATE
,[disch_date] DATE
,[diff_in_months] INT
);
CREATE TABLE #DataNumers
(
[number] INT
);
DECLARE #max_diff_in_months INT;
INSERT INTO #DataSource ([client_id], [adm_date], [disch_date], [diff_in_months])
SELECT [client_id]
,[adm_date]
,ISNULL([disch_date], GETUTCDATE()) AS [disch_date]
,CEILING(DATEDIFF(MONTH, [adm_date], ISNULL([disch_date], GETUTCDATE())) * 1.0 / 3.0) - 1
FROM #DataSource
WHERE DATEADD(MONTH, 3, [adm_date]) <= ISNULL([disch_date], GETUTCDATE());
SELECT #max_diff_in_months = MAX([diff_in_months])
FROM #DataSource;
INSERT INTO #DataNumers ([number])
SELECT TOP (#max_diff_in_months) ROW_NUMBER() OVER(ORDER BY (SELECT 1))
FROM [master]..[spt_values];
SELECT DS.[client_id]
,DS.[adm_date]
,DATEADD(MONTH, DN.[number] * 3, [adm_date]) AS [3Month Date]
,DS.[disch_date]
FROM #DataSource DS
CROSS APPLY #DataNumers DN
WHERE DS.[diff_in_months] >= DN.[number];
The algorithm is the following:
select only the rows we needed
calculate the differences in months/3 for each row
generate numbers in helper table
generate the new rows using cross apply
You can use CROSS JOIN if you have more then 2.5k numbers to generate but I remove it in my example. Also, we are using [master]..[spt_values] to generate the total periods. But it's just one way for generating numbers in T-SQL. You can check the link and use another technique if you like.
The idea is to find the periods between each [adm_date] and [disch_date] - and a period is measure by 3 months - that's why we are using DATEDIFF with MONTH and then dividing by 3. We are using ceiling to get the upper value. Of course, you can generate more periods than needed and exclude them with WHERE clause in the final result.

Related

Create View in SQL Server to create multiple rows for each date,with calculated date and identifier column

I need to create a VIEW/Select statement that will take a start date, and create 3 different rows for each date. One row calculates 30 days, from the start date, another 60 days, and another 90 days. Also each row needs to have an identifier that states whether the date is 30 days, 60 days or 90 days from the start date. So say that the start date is 09/01/2020. Then the View will return this for each start date:
Row Header : Start Date, AdditionalDate, AdditionalDays
Row 1 : 01/01/2020, 02/01/2020, 30
Row 2 : 01/02/2020, 03/01/2020, 60
Row 3 : 01/01/2020, 04/01/2020, 90
Sorry, forgot to mention, but start date is from a table. Like (Select startDate from Appointment)
I am using Microsoft SQL Server and a new SQL user. Really appreciate any help and advice.
Thank you!
I am unsure why what do you expect from a view for that - views don't take parameters.
Here is, however, a query that, from a given date parameter, generates three rows, at 30, 60 and 90 days later:
declare #start_date date = '2020-01-01';
select
#start_date,
dateadd(day, additional_days, #start_date) additional_date,
additional_days
from (values (30), (60), (90)) x(additional_days)
I am unsure whether you really mean 30 days or a month. If you want months, then:
declare #start_date date = '2020-01-01';
select
#start_date,
dateadd(month, additional_months, #start_date) additional_date,
additional_months
from (values (1), (2), (3)) x(additional_months)
On the other hand, if you are starting from an existing table, then that's a cross join:
select
t.*,
dateadd(day, x.additional_days, t.start_date) additional_date,
x.additional_days
from mytable t
cross join (values (30), (60), (90)) x(additional_days
You cannot use a view for this purpose, but you can use an inline table-valued function:
create function dates (
#date date,
#period int,
#num int
)
returns table
as return
with dates as (
select #date as start_date,
dateadd(day, #period, #date) as additional_date,
#period as additional_days, 1 as n
union all
select start_date,
dateadd(day, #period, additional_date),
additional_days + #period, n + 1
from dates
where n < #num
)
select start_date, additional_date, additional_days
from dates;
Here is a db<>fiddle.
You can utilize a recursive cte:
with cte as
( Select 1 as Header
,Start
,dateadd(day, 30, Start) as AdditionalDate
,30 as AdditionalDays
from Appointment
union all
select Header+1
,Start
,dateadd(day, 30, AdditionalDate)
,AdditionalDays + 30
from cte
where Header <= 2
)
Select * from cte
Or for adding months instead of days:
with cte as
( Select 1 as Header
,Start
,dateadd(month, 1, Start) as AdditionalDate
,datediff(day, Start, dateadd(month, 1, Start)) as AdditionalDays
from Appointment
union all
select Header+1
,Start
,dateadd(month, 1, AdditionalDate)
,datediff(day, Start, dateadd(month, 1, AdditionalDate))
from cte
where Header <= 2
)
Select * from cte
See fiddle

T-SQL Query to return the closest values

I've got some data in a SQL Server table as followings:
DateTime Bid Ask
02/10/2017 09:59.323 123.111 123.894
02/10/2017 10:01.432 123.321 124.001
02/10/2017 10:03.132 123.421 124.121
02/10/2017 10:03.983 123.121 123.721
02/10/2017 10:04.342 123.587 124.200
What I'd like to query is what the Bid and Ask values were at each second time period. For example at:
10:00.000 the Bid was 123.111 and the Ask was 123.894
10:01.000 the Bid was 123.111 and the Ask was 123.894
10:02.000 the Bid was 123.321 and the Ask was 124.001
10:03.000 the Bid was 123.321 and the Ask was 124.001
10:04.000 the Bid was 123.121 and the Ask was 123.721
So the SQL needs to return the Bid and Ask values for the Date Time before each second value.
For example for:
10:00.000 use 09:59.323
10:01.000 use 09:59.323
10:02.000 use 10:01.432
10:03.000 use 10:01.432
10:04.000 use 10:03.983
And my query will return values between a start and end date/time, so it'll return multiple rows.
You can try this query:
if object_id('tempdb..#Table1') is not null
drop table #Table1
go
create table #Table1(
DateTime datetime
,Bid float
,Ask float)
insert into #Table1
select '02/10/2017 09:59.323', 123.111, 123.894
union all select '02/10/2017 10:01.432', 123.321, 124.001
union all select '02/10/2017 10:03.132', 123.421, 124.121
union all select '02/10/2017 10:03.983', 123.121, 123.721
union all select '02/10/2017 10:04.342', 123.587, 124.200
declare #start_date datetime
, #end_date datetime
select #start_date = dateadd(mi, datediff(mi, 0, min(DateTime)) + 1, 0)
from #Table1
select #end_date = dateadd(mi, datediff(mi, 0, max(DateTime)), 0)
from #Table1
;with generates_dates as(
select #start_date as dt
union all
select dateadd(mi, 1, dt) as dt
from generates_dates
where dt < #end_date)
select t1.dt
, t2.Bid
, t2.Ask
from generates_dates t1
cross apply(select top 1 Bid, Ask
from #Table1 t2
where t2.DateTime < t1.dt
order by t2.DateTime desc)t2(Bid, Ask)
option (maxrecursion 0)
Order the table by datetime decending, limit the return to 1 and then make sure that any values returned are lower than the supplied datetime.
[edit]
select top 1 bid, ask
from datatable
where itsdatetime <= '02/10/2017 10:00.000'
order by itsdatetime desc
You can get more creative and put that inside of condition subquery.
select *
from requiretimes rt
join datatable dt on dt.itsdatetime = (select top 1 itsdatetime
from datatable
where itsdatetime <= rt.requireddatetime
order by itsdatetime desc)
DECLARE #T TABLE
(
d DateTime,
bid MONEY,
ask MONEY
)
INSERT INTO #T VALUES
('02/10/2017 09:59.323', 123.111, 123.894),
('02/10/2017 10:01.432', 123.321, 124.001),
('02/10/2017 10:03.132', 123.421, 124.121),
('02/10/2017 10:03.983', 123.121, 123.721),
('02/10/2017 10:04.342', 123.587, 124.200),
('03/10/2017 10:04.342', 123.587, 124.200)
;WITH sec AS
(
SELECT TOP (SELECT 60*60*24) ROW_NUMBER() OVER (ORDER BY 1/0) as s
FROM master..spt_values a,master..spt_values m
), dd as
(
SELECT DISTINCT CAST(d as date ) as d
FROM #t
), Tbl as
(
SELECT
DATEADD(ss,b.s,CAST(a.d as datetime)) as dat
FROM
dd a
CROSS JOIN
sec b
)
SELECT
dat
,c.*
FROM tbl
CROSS APPLY
(
SELECT TOP 1 *
FROM #t a
WHERE
a.d >= tbl.dat
ORDER BY
a.d ASC
) as c
WHERE
c.d >= dat AND
ORDER BY dat
There are two parts to this:
Create a projection holding your timestamps: 10:00.000, 10:01.000, 10:02.000 etc. This is hard to show in an answer here, because we don't know what criteria you're using to determine your start and end ranges, and because your question asks about seconds, but your timestamp values are actually showing minutes. If you need help with this, there are lots of results in Google and here on Stack Overflow for creating projections, number tables, or sequences.
Use the OUTER APPLY operator to connect the projection back to your original data. OUTER APPLY makes it easy to show exactly the one right record for each item from your projection.
.
WITH times As (
SELECT cast('2017-02-10 10:00.000' as datetime) "DateTime"
UNION
SELECT cast('2017-02-10 10:01.00' as datetime)
UNION
SELECT cast('2017-02-10 10:02.000' as datetime)
UNION
SELECT cast('2017-02-10 10:03.000' as datetime)
UNION
SELECT cast('2017-02-10 10:04.000' as datetime)
UNION
SELECT cast('2017-02-10 10:05.000' as datetime)
)
SELECT t.[DateTime], u.[DateTime], u.Bid, u.Ask
FROM times t
CROSS APPLY (
SELECT TOP 1 *
FROM [MyTable]
WHERE [DateTime] < t.[DateTime]
ORDER BY [DateTime] DESC
) u
ORDER BY t.[DateTime]
SQLFiddle
You'd generate the seconds with a recursive query. (As SQL Server doesn't support ANSI timestamp literals, you'll need CONVERT for this.) Then join via CROSS APPLY to get the last entry from the table per second.
with secs(sec) as
(
select convert(datetime, '2017-10-02 10:00:00', 20) as sec
union all
select dateadd(second, 1, sec) as sec
from secs
where sec <= convert(datetime, '2017-10-02 10:00:04', 20)
)
select secs.sec, data.bid, data.ask
from secs
cross apply
(
select top(1) *
from mytable
where mytable.datetime <= secs.sec
order by datetime desc
) data;
I am using seconds here as per your description, while your sample uses minutes instead. Decide which you actually need.
Try this:
declare #idate datetime
declare #fdate datetime
select #idate = min(gendate) from BidAsk
select #fdate = max(gendate) from BidAsk
create table #temp (bid float, ask float, Gendate datetime)
while (#idate <= #fdate)
begin
insert into #temp
select top 1 Bid, Ask, #idate from BidAsk where #idate > GenDate
order by GenDate desc
set #idate = DATEADD(second,1,#idate)
end
select * from #temp

What is the best way to find next n week days

I got the following code from the following question I asked:
Passing in Week Day name to get nearest date in SQL
I need to find next 4 Weekdays based on today's date for corresponding Day-Of-Week in my table ie, if today is 2015-01-24 the result should be 1/24, 1/31, 2/7, 2/14 for Saturdays.
TABLE
SAMPLE QUERY
create table #t
(
jobId int,
personId int,
frequencyVal varchar(10)
);
insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');
WITH cte(n) AS
(
SELECT 0
UNION ALL
SELECT n+1 FROM cte WHERE n < 3
)
select #t.jobId, #t.personId, #t.frequencyVal, STUFF(a.d, 1, 1, '') AS FutureDates
from #t
cross apply (SELECT CASE #t.frequencyVal
WHEN 'SUNDAYS' THEN 1
WHEN 'MONDAYS' THEN 2
WHEN 'TUESDAYS' THEN 3
WHEN 'WEDNESDAYS' THEN 4
WHEN 'THURSDAYS' THEN 5
WHEN 'FRIDAYS' THEN 6
WHEN 'SATURDAYS' THEN 7
END)tranlationWeekdays(n)
cross apply (select ',' + CONVERT(varchar(10), CONVERT(date,dateadd(WEEK, cte.n,CONVERT(DATE, DATEADD(DAY, (DATEPART(WEEKDAY, GETDATE()) + tranlationWeekdays.n) % 7, GETDATE()))))) from cte FOR XML PATH('')) a(d);
drop table #t;
EXPECTED RESULT
Gets the first day of current month.
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
Create the table and insert values
create table #t
(
jobId int,
personId int,
frequencyVal varchar(10)
);
insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');
You can use either of the below queries for your situation.
QUERY 1 : Select the first 4 week of days in current month for particular week day
-- Gets the first day of current month
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
;WITH CTE as
(
-- Will find all dates in current month
SELECT #FIRSTDAY as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(MONTH,1,#FIRSTDAY)
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal, DATES,
ROW_NUMBER() OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES) ORDER BY CTE.DATES) DATECNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
WHERE MONTH(DATES)= MONTH(GETDATE())
)
-- Converts to CSV and make sure that only 4 days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND DATECNT<5
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
For example, in Query1 the nearest date(here we take example as Saturday) of
2015-Jan-10 will be 01/03,01/10,01/17,01/24
2015-Jan-24 will be 01/03,01/10,01/17,01/24
QUERY 2 : Select nearest 4 week of days in current month for particular week day
-- Gets the first day in current month
DECLARE #FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)
;WITH CTE as
(
-- Will find all dates in current
SELECT CAST(#FIRSTDAY AS DATE) as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(MONTH,1,#FIRSTDAY)
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal,DATES,
-- Get week difference for each weekday
DATEDIFF(WEEK,DATES,GETDATE()) WEEKDIFF,
-- Count the number of weekdays in a month
COUNT(DATES) OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES)) WEEKCOUNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
WHERE MONTH(DATES)= MONTH(GETDATE())
)
-- Converts to CSV and make sure that only nearest 4 week of days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal AND
((WEEKDIFF<3 AND WEEKDIFF>-3 AND WEEKCOUNT = 5) OR WEEKCOUNT <= 4)
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
For example, in Query2 the nearest date(here we take example as Saturday) of
2015-Jan-10 will be 01/03,01/10,01/17,01/24
2015-Jan-24 will be 01/10,01/17,01/24,01/31
QUERY 3 : Select next 4 week's dates for particular week day irrelevant of month
;WITH CTE as
(
-- Will find all dates in current month
SELECT CAST(GETDATE() AS DATE) as DATES
UNION ALL
SELECT DATEADD(DAY,1,DATES)
FROM CTE
WHERE DATES < DATEADD(DAY,28,GETDATE())
)
,CTE2 AS
(
-- Join the #t table with CTE on the datename+'s'
SELECT jobId,personId,frequencyVal, DATES,
ROW_NUMBER() OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES) ORDER BY CTE.DATES) DATECNT
FROM CTE
JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal
)
-- Converts to CSV and make sure that only 4 days are generated for month
SELECT DISTINCT C2.jobId,C2.personId,frequencyVal,
SUBSTRING(
(SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/' +
CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
FROM CTE2
WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal
AND DATECNT < 5
ORDER BY CTE2.DATES
FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
SQL FIDDLE
The following would be the output if the GETDATE() (if its Saturday) is
2015-01-05 - 1/10, 1/17, 1/24, 1/31
2015-01-24 - 1/24, 1/31, 2/7, 2/14
This is a simpler way I think, and I think it fits your requirements
Note that I have changed your frequency_val column to an integer that represents the day of the week from SQL servers perspective and added a calculated column to illustrate how you can easily derive the day name from that.
/*************************************************/
--Set up our sample table
/*************************************************/
declare #t table
(
jobId int,
personId int,
--frequencyVal varchar(10) -- why store a string when a tiny int will do.
frequency_val tinyint,
frequency_day as datename(weekday,frequency_val -1) + 's'
)
insert into #t
values
(1,100,1),--'Mondays'),
(2,101,6),--'Saturdays');
(3,101,7),--'Sundays');
(4,100,2)--'Tuesdays'),
--select * from #t
/*************************************************/
--Declare & initialise variables
/*************************************************/
declare #num_occurances int = 4
declare #from_date date = dateadd(dd,3,getdate()) -- this will allow you to play with the date simply by changing the increment value
/*************************************************/
-- To get a row for each occurance
/*************************************************/
;with r_cte (days_ahead, occurance_date)
as (select 0, convert(date,#from_date,121)
union all
select r_cte.days_ahead +1, convert(date,dateadd(DD, r_cte.days_ahead+1, #from_date),121)
from r_cte
where r_cte.days_ahead < (7 * #num_occurances) -1
)
select t.*, r_cte.occurance_date
from
#t t
inner join r_cte
on DATEPART(WEEKDAY, dateadd(dd,##DATEFIRST - 1 ,r_cte.occurance_date)) = t.frequency_val
/*************************************************/
--To get a single row with a CSV of every occurance
/*************************************************/
;with r_cte (days_ahead, occurance_date)
as (select 0, convert(date,#from_date,121)
union all
select r_cte.days_ahead +1, convert(date,dateadd(DD, r_cte.days_ahead+1, #from_date),121)
from r_cte
where r_cte.days_ahead < (7 * #num_occurances) -1
)
select
t.*,
STUFF( (select ', '
+ convert(varchar(2),datepart(month,occurance_date),0) + '/'
+ convert(varchar(2),datepart(day,occurance_date),0) as occurance
from r_cte
where DATEPART(WEEKDAY, dateadd(dd,##DATEFIRST - 1 ,r_cte.occurance_date)) = t.frequency_val
FOR XML PATH (''),TYPE).value('.','varchar(30)')
,1,2,'') occurance_date -- rest of STUFF() function
from
#t t

Get every hour for a time range

So what I am trying to is generate all the hours that are inside a specific time range.
So given the range 11 AM to 2:00 PM, I would get:
11:00 AM
12:00 PM
1:00 PM
2:00 PM
I am trying to avoid having to store every specific hour a store might be open and just store the range (I need to compare the hours against other times)
Thanks
No loops, recursive CTEs or numbers table required.
DECLARE
#start TIME(0) = '11:00 AM',
#end TIME(0) = '2:00 PM';
WITH x(n) AS
(
SELECT TOP (DATEDIFF(HOUR, #start, #end) + 1)
rn = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns ORDER BY [object_id]
)
SELECT t = DATEADD(HOUR, n-1, #start) FROM x ORDER BY t;
You could use a recursive CTE. This would generate the hours between 11 and 14:
;with Hours as
(
select 11 as hr
union all
select hr + 1
from Hours
where hr < 14
)
select *
from Hours
Live example at SQL Fiddle.
If you have a numbers table (click the link to create one if you don't)...
create table test(
startTime time
, endTime time
)
insert into test
select '11:00', '14:00'
select
dateadd(hh, n.n, t.startTime) as times
from test t
inner join Numbers n
-- assuming your numbers start at 1 rather than 0
on n.n-1 <= datediff(hh, t.startTime, t.endTime)
If this is specialized, you can create an hours table with just 24 values.
create table HoursInADay(
[hours] time not null
, constraint PK_HoursInADay primary key ([hours])
)
-- insert
insert into HoursInADay select '1:00'
insert into HoursInADay select '2:00'
insert into HoursInADay select '3:00'
insert into HoursInADay select '4:00'
insert into HoursInADay select '5:00'
insert into HoursInADay select '6:00'
insert into HoursInADay select '7:00'
...
select
h.[hours]
from test t
inner join HoursInADay h
on h.[hours] between t.startTime and t.endTime
The easiest way I can think of to do this is to have only 1 permanent table with a list of all hours; 24 entries total.
Create table dbo.Hours (Hourly_Time Time NOT NULL)
Insert into dbo.Hours ...
Then, given times A & B:
select * from dbo.Hours where Hourly_Time<=A and Hourly_Time>=B
#Andomar Thanks a lot, you helped me, there is my add above your code.
*----------------------------
create view vw_hoursalot as
with Hours as
(
select DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , -5 , getDate()
)
)
) as dtHr
union all
select DATEADD (minute , 30 , dtHr )
from Hours
where dtHr < DATEADD(
dd, 0, DATEDIFF(
dd, 0, DATEADD (
year , +5 , getDate()
)
)
)
)
select * from Hours
----------------------------
select * from vw_hoursalot option (maxrecursion 0)
----------------------------*

How to Determine Values for Missing Months based on Data of Previous Months in T-SQL

I have a set of transactions occurring at specific points in time:
CREATE TABLE Transactions (
TransactionDate Date NOT NULL,
TransactionValue Integer NOT NULL
)
The data might be:
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('1/1/2009', 1)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('3/1/2009', 2)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('6/1/2009', 3)
Assuming that the TransactionValue sets some kind of level, I need to know what the level was between the transactions. I need this in the context of a set of T-SQL queries, so it would be best if I could get a result set like this:
Month Value
1/2009 1
2/2009 1
3/2009 2
4/2009 2
5/2009 2
6/2009 3
Note how, for each month, we either get the value specified in the transaction, or we get the most recent non-null value.
My problem is that I have little idea how to do this! I'm only an "intermediate" level SQL Developer, and I don't remember ever seeing anything like this before. Naturally, I could create the data I want in a program, or using cursors, but I'd like to know if there's a better, set-oriented way to do this.
I'm using SQL Server 2008, so if any of the new features will help, I'd like to hear about it.
P.S. If anyone can think of a better way to state this question, or even a better subject line, I'd greatly appreciate it. It took me quite a while to decide that "spread", while lame, was the best I could come up with. "Smear" sounded worse.
I'd start by building a Numbers table holding sequential integers from 1 to a million or so. They come in really handy once you get the hang of it.
For example, here is how to get the 1st of every month in 2008:
select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12;
Now, you can put that together using OUTER APPLY to find the most recent transaction for each date like so:
with Dates as (
select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12
)
select d.firstOfMonth, t.TransactionValue
from Dates d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.firstOfMonth
order by TransactionDate desc
) t;
This should give you what you're looking for, but you might have to Google around a little to find the best way to create the Numbers table.
here's what i came up with
declare #Transactions table (TransactionDate datetime, TransactionValue int)
declare #MinDate datetime
declare #MaxDate datetime
declare #iDate datetime
declare #Month int
declare #count int
declare #i int
declare #PrevLvl int
insert into #Transactions (TransactionDate, TransactionValue)
select '1/1/09',1
insert into #Transactions (TransactionDate, TransactionValue)
select '3/1/09',2
insert into #Transactions (TransactionDate, TransactionValue)
select '5/1/09',3
select #MinDate = min(TransactionDate) from #Transactions
select #MaxDate = max(TransactionDate) from #Transactions
set #count=datediff(mm,#MinDate,#MaxDate)
set #i=1
set #iDate=#MinDate
while (#i<=#count)
begin
set #iDate=dateadd(mm,1,#iDate)
if (select count(*) from #Transactions where TransactionDate=#iDate) < 1
begin
select #PrevLvl = TransactionValue from #Transactions where TransactionDate=dateadd(mm,-1,#iDate)
insert into #Transactions (TransactionDate, TransactionValue)
select #iDate, #prevLvl
end
set #i=#i+1
end
select *
from #Transactions
order by TransactionDate
To do it in a set-based way, you need sets for all of your data or information. In this case there's the overlooked data of "What months are there?" It's very useful to have a "Calendar" table as well as a "Number" table in databases as utility tables.
Here's a solution using one of these methods. The first bit of code sets up your calendar table. You can fill it using a cursor or manually or whatever and you can limit it to whatever date range is needed for your business (back to 1900-01-01 or just back to 1970-01-01 and as far into the future as you want). You can also add any other columns that are useful for your business.
CREATE TABLE dbo.Calendar
(
date DATETIME NOT NULL,
is_holiday BIT NOT NULL,
CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED (date)
)
INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-01', 1) -- New Year
INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-02', 1)
...
Now, using this table your question becomes trivial:
SELECT
CAST(MONTH(date) AS VARCHAR) + '/' + CAST(YEAR(date) AS VARCHAR) AS [Month],
T1.TransactionValue AS [Value]
FROM
dbo.Calendar C
LEFT OUTER JOIN dbo.Transactions T1 ON
T1.TransactionDate <= C.date
LEFT OUTER JOIN dbo.Transactions T2 ON
T2.TransactionDate > T1.TransactionDate AND
T2.TransactionDate <= C.date
WHERE
DAY(C.date) = 1 AND
T2.TransactionDate IS NULL AND
C.date BETWEEN '2009-01-01' AND '2009-12-31' -- You can use whatever range you want
John Gibb posted a fine answer, already accepted, but I wanted to expand on it a bit to:
eliminate the one year limitation,
expose the date range in a more
explicit manner, and
eliminate the need for a separate
numbers table.
This slight variation uses a recursive common table expression to establish the set of Dates representing the first of each month on or after from and to dates defined in DateRange. Note the use of the MAXRECURSION option to prevent a stack overflow (!); adjust as necessary to accommodate the maximum number of months expected. Also, consider adding alternative Dates assembly logic to support weeks, quarters, even day-to-day.
with
DateRange(FromDate, ToDate) as (
select
Cast('11/1/2008' as DateTime),
Cast('2/15/2010' as DateTime)
),
Dates(Date) as (
select
Case Day(FromDate)
When 1 Then FromDate
Else DateAdd(month, 1, DateAdd(month, ((Year(FromDate)-1900)*12)+Month(FromDate)-1, 0))
End
from DateRange
union all
select DateAdd(month, 1, Date)
from Dates
where Date < (select ToDate from DateRange)
)
select
d.Date, t.TransactionValue
from Dates d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.Date
order by TransactionDate desc
) t
option (maxrecursion 120);
If you do this type of analysis often, you might be interested in this SQL Server function I put together for exactly this purpose:
if exists (select * from dbo.sysobjects where name = 'fn_daterange') drop function fn_daterange;
go
create function fn_daterange
(
#MinDate as datetime,
#MaxDate as datetime,
#intval as datetime
)
returns table
--**************************************************************************
-- Procedure: fn_daterange()
-- Author: Ron Savage
-- Date: 12/16/2008
--
-- Description:
-- This function takes a starting and ending date and an interval, then
-- returns a table of all the dates in that range at the specified interval.
--
-- Change History:
-- Date Init. Description
-- 12/16/2008 RS Created.
-- **************************************************************************
as
return
WITH times (startdate, enddate, intervl) AS
(
SELECT #MinDate as startdate, #MinDate + #intval - .0000001 as enddate, #intval as intervl
UNION ALL
SELECT startdate + intervl as startdate, enddate + intervl as enddate, intervl as intervl
FROM times
WHERE startdate + intervl <= #MaxDate
)
select startdate, enddate from times;
go
it was an answer to this question, which also has some sample output from it.
I don't have access to BOL from my phone so this is a rough guide...
First, you need to generate the missing rows for the months you have no data. You can either use a OUTER join to a fixed table or temp table with the timespan you want or from a programmatically created dataset (stored proc or suchlike)
Second, you should look at the new SQL 2008 'analytic' functions, like MAX(value) OVER ( partition clause ) to get the previous value.
(I KNOW Oracle can do this 'cause I needed it to calculate compounded interest calcs between transaction dates - same problem really)
Hope this points you in the right direction...
(Avoid throwing it into a temp table and cursoring over it. Too crude!!!)
-----Alternative way------
select
d.firstOfMonth,
MONTH(d.firstOfMonth) as Mon,
YEAR(d.firstOfMonth) as Yr,
t.TransactionValue
from (
select
dateadd( month, inMonths - 1, '1/1/2009') as firstOfMonth
from (
values (1), (2), (3), (4), (5), (7), (8), (9), (10), (11), (12)
) Dates(inMonths)
) d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.firstOfMonth
order by TransactionDate desc
) t