List dates between two date range - sql-server-2008

I have table 1 with 3 columns id, startdate and enddate. With order id being the primary key how do I list the dates between the date range Startdate and Enddate?
What I have:
id Startdate EndDate
1 2/11/2014 2/13/2014
2 2/15/2014 2/17/2014
What I need:
id Date
1 2/11/2014
1 2/12/2014
1 2/13/2014
2 2/15/2014
2 2/16/2014
2 2/17/2014
How do I do this?

Use recursive CTE:
WITH tmp AS (
SELECT id, StartDate AS [Date], EndDate
FROM MyTable
UNION ALL
SELECT tmp.id, DATEADD(DAY,1,tmp.[Date]), tmp.EndDate
FROM tmp
WHERE tmp.[Date] < tmp.EndDate
)
SELECT tmp.ID, tmp.[Date]
FROM tmp
ORDER BY tmp.id, tmp.[Date]
OPTION (MAXRECURSION 0) -- For long intervals
If you have to use cursor/loop, most times you are doing it wrong.

If you do a one-off setup of an auxiliary calendar table as shown at Why should I consider using an auxiliary calendar table?, possibly omitting a lot of the columns if you don't need them, like this:
CREATE TABLE dbo.Calendar
(
dt SMALLDATETIME NOT NULL
PRIMARY KEY CLUSTERED,
Y SMALLINT,
M TINYINT,
D TINYINT
)
GO
SET NOCOUNT ON
DECLARE #dt SMALLDATETIME
SET #dt = '20000101'
WHILE #dt < '20300101'
BEGIN
INSERT dbo.Calendar(dt) SELECT #dt
SET #dt = #dt + 1
END;
UPDATE dbo.Calendar SET
Y = YEAR(dt),
M = MONTH(dt),
D = DAY(dt);
(You may well not need the Y, M, D columns at all, but I left those in to show that more data can be stored for fast access - the article I linked to shows how that could be used.)
Then if your table is named "so", your code would simply be
SELECT A.id, C.dt
FROM so AS A
JOIN Calendar AS C
ON C.dt >= A.StartDate AND C.dt<= A.EndDate
An advantage of using an auxiliary table like that is that your queries can be faster: the work done in setting one up is a one-time cost which doesn't happen during usage..

Instead of using CTE (to over come recursive and performance when date range is large) below query can be used to get the list of dates between two date range.
DECLARE #StartDateSTR AS VARCHAR(32); DECLARE #EndDateSTR AS
VARCHAR(32); DECLARE #EndDate AS DATE; DECLARE #StartDate AS DATE;
SET #StartDateSTR = '01/01/1990'; SET #EndDateSTR = '03/31/2025'; SET
#StartDate = CAST(#StartDateSTR AS date); SET #EndDate =
cast(#EndDateSTR AS date); SELECT
DATEADD(DAY, n1.rn - 1, #StartDate) AS dt FROM (SELECT rn=Row_number() OVER( ORDER BY (SELECT NULL)) FROM sys.objects a
CROSS JOIN sys.objects b CROSS JOIN sys.objects c CROSS JOIN
sys.objects d) as n1 WHERE n1.[rn] <= Datediff(dd, #StartDate,
#EndDate)+1;

Related

Mysql select counters grouped by time with gaps filled [duplicate]

How i can fill date gaps in MySQL? Here is my query:
SELECT DATE(posted_at) AS date,
COUNT(*) AS total,
SUM(attitude = 'positive') AS positive,
SUM(attitude = 'neutral') AS neutral,
SUM(attitude = 'negative') AS negative
FROM `messages`
WHERE (`messages`.brand_id = 1)
AND (`messages`.`spam` = 0
AND `messages`.`duplicate` = 0
AND `messages`.`ignore` = 0)
GROUP BY date ORDER BY date
It returns proper result set - but i want to fill gaps between dates start and end by zeros. How i can do this?
You'll need to create a helper table and fill it with all dates from start to end, then just LEFT JOIN with that table:
SELECT d.dt AS date,
COUNT(*) AS total,
SUM(attitude = 'positive') AS positive,
SUM(attitude = 'neutral') AS neutral,
SUM(attitude = 'negative') AS negative
FROM dates d
LEFT JOIN
messages m
ON m.posted_at >= d.dt
AND m.posted_at < d.dt + INTERVAL 1 DAYS
AND spam = 0
AND duplicate = 0
AND ignore = 0
GROUP BY
d.dt
ORDER BY
d.dt
Basically, what you need here is a dummy rowsource.
MySQL is the only major system which lacks a way to generate it.
PostgreSQL implements a special function generate_series to do that, while Oracle and SQL Server can use recursion (CONNECT BY and recursive CTEs, accordingly).
I don't know whether MySQL will support the following/similar syntax; but if not, then you could just create and drop a temporary table.
--Inputs
declare #FromDate datetime, /*Inclusive*/
#ToDate datetime /*Inclusive*/
set #FromDate = '20091101'
set #ToDate = '20091130'
--Query
declare #Dates table (
DateValue datetime NOT NULL
)
set NOCOUNT ON
while #FromDate <= #ToDate /*Inclusive*/
begin
insert into #Dates(DateValue) values(#FromDate)
set #FromDate = #FromDate + 1
end
set NOCOUNT OFF
select dates.DateValue,
Col1...
from #Dates dates
left outer join SourceTableOrView data on
data.DateValue >= dates.DateValue
and data.DateValue < dates.DateValue + 1 /*NB: Exclusive*/
where ...?

Calculate Date by number of working days from a certain Startdate

I've got two tables, a project table and a calendar table. The first containts a startdate and days required. The calendar table contains the usual date information, like date, dayofweek, and a column is workingday, which shows if the day is a saturday, sunday, or bank holiday (value = 0) or a regular workday (value = 1).
For a certain report I need write a stored procedure that calculates the predicted enddate by adding the number of estimated workddays needed.
Example:
**Projects**
Name Start_Planned Work_days_Required
Project A 02.05.2016 6
Calendar (04.05 is a bank holdiday)
Day Weekday Workingday
01.05.2016 7 0
02.05.2016 1 1
03.05.2016 2 1
04.05.2016 3 0
05.05.2016 4 1
06.05.2016 5 1
07.05.2016 6 0
08.05.2016 7 0
09.05.2016 1 1
10.05.2016 2 1
Let's say, the estimated number of days required is given as 6 (which leads to the predicted enddate of 10.05.2016). Is it possible to join the tables in a way, which allows me to put something like
select date as enddate_predicted
from calendar
join projects
where number_of_days = 6
I would post some more code, but I'm quite stuck on how where to start.
Thanks!
You could get all working days after your first date, then apply ROW_NUMBER() to get the number of days for each date:
SELECT Date, DayNum = ROW_NUMBER() OVER(ORDER BY Date)
FROM Calendar
WHERE IsWorkingDay = 1
AND Date >= #StartPlanned
Then it would just be a case of filtering for the 6th day:
DECLARE #StartPlanned DATE = '20160502',
#Days INT = 6;
SELECT Date
FROM ( SELECT Date, DayNum = ROW_NUMBER() OVER(ORDER BY Date)
FROM Calendar
WHERE WorkingDay = 1
AND Date >= #StartPlanned
) AS c
WHERE c.DayNum = #Days;
It's not part of the question, but for future proofing this is easier to acheive in SQL Server 2012+ with OFFSET/FETCH
DECLARE #StartPlanned DATE = '20160502',
#Days INT = 6;
SELECT Date
FROM dbo.Calendar
WHERE Date >= #StartPlanned
AND WorkingDay = 1
ORDER BY Date
OFFSET (#Days - 1) ROWS FETCH NEXT 1 ROWS ONLY
ADDENDUM
I missed the part earlier about having another table, and the comment about putting it into a cursor has prompted me to amend my answer. I would add a new column to your calendar table called WorkingDayRank:
ALTER TABLE dbo.Calendar ADD WorkingDayRank INT NULL;
GO
UPDATE c
SET WorkingDayRank = wdr
FROM ( SELECT Date, wdr = ROW_NUMBER() OVER(ORDER BY Date)
FROM dbo.Calendar
WHERE WorkingDay = 1
) AS c;
This can be done on the fly, but you will get better performance with it stored as a value, then your query becomes:
SELECT p.Name,
p.Start_Planned,
p.Work_days_Required,
EndDate = c2.Date
FROM Projects AS P
INNER JOIN dbo.Calendar AS c1
ON c1.Date = p.Start_Planned
INNER JOIN dbo.Calendar AS c2
ON c2.WorkingDayRank = c1.WorkingDayRank + p.Work_days_Required - 1;
This simply gets the working day rank of your start date, and finds the number of days ahead specified by the project by joining on WorkingDayRank (-1 because you want the end date inclusive of the range)
This will fail, if you ever plan to start your project on a non working day though, so a more robust solution might be:
SELECT p.Name,
p.Start_Planned,
p.Work_days_Required,
EndDate = c2.Date
FROM Projects AS P
CROSS APPLY
( SELECT TOP 1 c1.Date, c1.WorkingDayRank
FROM dbo.Calendar AS c1
WHERE c1.Date >= p.Start_Planned
AND c1.WorkingDay = 1
ORDER BY c1.Date
) AS c1
INNER JOIN dbo.Calendar AS c2
ON c2.WorkingDayRank = c1.WorkingDayRank + p.Work_days_Required - 1;
This uses CROSS APPLY to get the next working day on or after your project start date, then applies the same join as before.
This query returns a table with a predicted enddate for each project
select name,min(day) as predicted_enddate from (
select c.day,p.name from dbo.Calendar c
join dbo.Calendar c2 on c.day>=c2.day
join dbo.Projects p on p.start_planned<=c.day and p.start_planned<=c2.day
group by c.day,p.work_days_required,p.name
having sum(c2.workingday)=p.work_days_required
) a
group by name
--This gives me info about all projects
select p.projectname,p.Start_Planned ,c.date,
from calendar c
join
projects o
on c.date=dateadd(days,p.Work_days_Required,p.Start_Planned)
and c.isworkingday=1
now you can use CTE like below or wrap this in a procedure
;with cte
as
(
Select
p.projectnam
p.Start_Planned ,
c.date,datediff(days,p.Start_Planned,c.date) as nooffdays
from calendar c
join
projects o
on c.date=dateadd(days,p.Work_days_Required,p.Start_Planned)
and c.isworkingday=1
)
select * from cte where nooffdays=6
use below logic
CREATE TABLE #proj(Name varchar(50),Start_Planned date,
Work_days_Required int)
insert into #proj
values('Project A','02.05.2016',6)
CReATE TABLE #Calendar(Day date,Weekday int,Workingday bit)
insert into #Calendar
values('01.05.2016',7,0),
('02.05.2016',1,1),
('03.05.2016',2,1),
('04.05.2016',3,0),
('05.05.2016',4,1),
('06.05.2016',5,1),
('07.05.2016',6,0),
('08.05.2016',7,0),
('09.05.2016',1,1),
('10.05.2016',2,1)
DECLARE #req_day int = 3
DECLARE #date date = '02.05.2016'
--SELECT #req_day = Work_days_Required FROM #proj where Start_Planned = #date
select *,row_number() over(order by [day] desc) as cnt
from #Calendar
where Workingday = 1
and [Day] > #date
SELECT *
FROM
(
select *,row_number() over(order by [day] desc) as cnt
from #Calendar
where Workingday = 1
and [Day] > #date
)a
where cnt = #req_day

Select Status That Last for More Than 1 Second

I have got a problem looks simple, but I could not find the solution.
So, I have got a table with two cols like this:
Time Status
00:00:00.111 Off
00:00:00.222 On
00:00:00.345 On
00:00:01.555 On
00:00:01.666 Off
00:00:02.222 On
00:00:02.422 On
00:00:02.622 Off
00:00:05.888 Off
00:00:05.999 Off
I want to select all statuses of On which lasted for more than 1 second,
in this example, I want the sequence:
00:00:00.222 On
00:00:00.345 On
00:00:01.555 On
Could you guys give me any clue? Many thanks!
A simple GROUP BY and SUM can not do this on your current dataset, so my idea is to add a helper column:
CREATE TABLE someTable(
`time` DATETIME,
status CHAR(3),
helperCol INT
);
The helperCol is an INT and will be set as follows:
CREATE PROCEDURE setHelperCol()
BEGIN
DECLARE finished,v_helperCol INT;
DECLARE status CHAR(3);
DECLARE ts DATETIME;
DECLARE CURSOR st FOR SELECT `time`,status,helperCol FROM someTable WHERE helperCol IS NOT NULL; -- Handy for re-use: No need to go over all data, so you can save the helperCol as permanent value.
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
SELECT #maxVal:=MAX(helperCol) FROM helperCol;
SET finished=0;
SET helperCol=#maxVal;
IF(!helperCol>0) SET helperCol=1;
OPEN st;
FETCH ts,status,v_helperCol FROM st;
WHILE(finished=0) DO
IF(status='Off') v_helperCol=v_helperCol+1;
UPDATE someTable SET helperCol=v_helperCol WHERE `time`=ts; -- Assuming `time` is unique;
FETCH ts,status,v_helperCol FROM st;
END WHILE;
CLOSE st;
END;
Execute the procedure and the result is:
Time Status helperCol
00:00:00.111 Off 2
00:00:00.222 On 2
00:00:00.345 On 2
00:00:01.555 On 2
00:00:01.666 Off 3
00:00:02.222 On 3
00:00:02.422 On 3
00:00:02.622 Off 4
This can now be grouped and processed:
SELECT MAX(`time`)-MIN(`time`) AS diffTime
FROM someTable
WHERE status='ON'
GROUP BY helperCol
HAVING MAX(`time`)-MIN(`time`)>1;
The result of that is (you need to search for the correct datetime functions to apply in the MAX-MIN part):
1.333
Alternative:
You can also process the MAX-MIN in the stored procedure, but that would not be efficiently repeatable as the helperColumn solution is.
SELECT a.time start
, MIN(c.time) end
, TIMEDIFF(MIN(c.time),a.time) duration
FROM
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) a
LEFT
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) b
ON b.status = a.status
AND b.rank = a.rank - 1
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) c
ON c.rank >= a.rank
LEFT
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) d
ON d.status = c.status
AND d.rank = c.rank + 1
WHERE b.rank IS NULL
AND d.rank IS NULL
AND a.status = 1
GROUP
BY a.time
HAVING duration >= 1;
Another, faster, method might be along these lines - unfortunately I don't think the data types and functions in my version of MySQL support fractions of a second, so this is probably a little bit wrong (there may also be a logical error)...
SELECT time
, status
, cumulative
FROM
( SELECT *
, CASE WHEN #prev = status THEN #i:=#i+duration ELSE #i:=0 END cumulative
, #prev:=status
FROM
( SELECT x.*
, TIME_TO_SEC(MIN(y.time))-TIME_TO_SEC(x.time) duration
FROM my_table x
JOIN my_table y
ON y.time > x.time
GROUP
BY x.time
) n
ORDER
BY time
) a
WHERE cumulative >= 1
AND status = 1;

Unrolling periodical events in MySQL

I've got a database with two tables, that I want to combine. One of the tables contains "incidental events", which just occur once. Next to this, I also have "periodical events". Now I want to combine these two in a view.
The incidental one simply has two columns, one called changes, the other one called date. The periodical one has three columns, changes, startDate and endDate. The difference between these two can be a maximum of 50 years, so manually typing out one case for every day is not going to work. Both views also have an AI ID. In this view I want to have a column date and a column changes.
To achieve this I want to unroll the periodical changes table, so that it shows one entry for every day in between the startDate and endDate. For instance:
incidental changes:
date | change
09/08/2015 | 5
11/08/2015 | 10
periodical changes:
startDate | endDate | change
09/08/2015 | 12/08/2015 | 7
These two I want combined into:
changes view:
date | change
09/08/2015 | 5
09/08/2015 | 7
10/08/2015 | 7
11/08/2015 | 10
11/08/2015 | 7
12/08/2015 | 7
My idea is to use something like this:
SELECT * FROM incidental_changes,(
SET #id = (SELECT min(ID) AS min FROM periodical_changes WHERE 1)
SET #maxID = (SELECT max(ID) AS max FROM periodical_changes WHERE 1)
WHILE (#id <= #maxID) DO
SET #firstDate = (SELECT startDate FROM periodical_changes WHERE id = #id)
SET #lastDate = (SELECT endDate FROM periodical_changes WHERE id = #id)
WHILE (#firstDate <= #lastDate) DO
SELECT #firstDate AS date, change FROM periodical_changes WHERE id = #id
#firstDate = #firstDate + INTERVAL 1 DAY
END
#id = #id + 1
END
) WHERE 1
This gives me an error,
CREATE ALGORITHM = UNDEFINED VIEW all_periodicals AS SELECT * FROM
incidental_changes,( SET #id = (SELECT min(ID) AS min FROM
periodical_changes WHERE 1) SET #maxID = (SELECT max(ID) AS max FROM
periodical_changes WHERE 1) WHILE (#id <= #maxID) DO SET #firstDate =
(SELECT startDate FROM periodical_changes WHERE id = #id) SET
#lastDate = (SELECT endDate FROM periodical_changes WHERE id = #id)
WHILE (#firstDate <= #lastDate) DO SELECT #firstDate AS date, change
FROM periodical_changes WHERE id = #id #firstDate = #firstDate +
INTERVAL 1 DAY END #id = #id + 1 END ) WHERE 1
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use
near 'SET #id = (SELECT min(ID) AS min FROM periodical_changes WHERE
1) SET #' at line 5
and I'm guessing that if I'd manage to fix this error there'd be more. So, is there any way to do this the way I want, or do I have to look for a different approach?
EDIT:
Okay, so far I have not found a way to do this in a view or so. So instead I am now using a routine. This routine has one parameter, account INT. The definition I am using so far is as followed:
BEGIN
DECLARE periodicalID int;
DECLARE v_finished INTEGER DEFAULT 0;
DECLARE periodicalCursor CURSOR
FOR SELECT periodicals.periodicalID FROM periodicals WHERE periodicals.accountID = account;
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET v_finished = 1;
CREATE TEMPORARY TABLE results LIKE incidentials;
ALTER TABLE results DROP INDEX date;
SET #periodicalID = -1;
OPEN periodicalCursor;
allPeriodicals: LOOP
FETCH periodicalCursor INTO periodicalID;
IF (v_finished) THEN
LEAVE allPeriodicals;
END IF;
SELECT periodicals.startDate,periodicals.numberOfPeriods,periodicals.period,periodicals.endDate,periodicals.money FROM periodicals WHERE periodicals.periodicalID = periodicalID AND periodicals.accountID = account INTO #startDate, #numberOfPeriods, #period,#endDate,#money;
SET #intervalStatement = "SELECT ? + INTERVAL ? ";
SET #intervalStatement = CONCAT(#intervalStatement,#period," INTO #res");
PREPARE intervalStatement FROM #intervalStatement;
WHILE #startDate <= #endDate DO
EXECUTE intervalStatement USING #startDate,#numberOfPeriods;
SET #startDate = #res;
INSERT INTO results(accountID,date,money) VALUES (account,#startDate,#money);
END WHILE;
END LOOP allPeriodicals;
INSERT INTO results(accountID,date,money) SELECT accountID,date, money FROM incidentials WHERE incidentials.accountID = account;
SELECT * FROM results ORDER BY date;
END
This poses the problem of performance though. With only one periodical entry spread over a year this query already takes about 16 seconds. So even though this approach works, I either did something wrong causing it to take this long or this is not the right way to go.
Let me presume you have a numbers table. Then you can do:
select i.date, i.change
from incidental
union all
select date_add(p.startDate, interval n.n - 1 day), p.change
from periodic p join
numbers n
on date_add(p.startDate, interval n.n - 1 day) <= p.endDate;
For a select query, you can generate the numbers using a subquery, if you know the maximum length. Something like:
select i.date, i.change
from incidental
union all
select date_add(p.startDate, interval n.n - 1 day), p.change
from periodic p join
(select 1 as n union all select 2 union all select 3 union all select 4 union all
select 5 union all select 6 union all select 7
) n
on date_add(p.startDate, interval n.n - 1 day) <= p.endDate;
This doesn't work in a view, however. For that, you really do need a numbers table.

Optimizing query for compressing tick price data into O-H-L-C intervals

I've been working on optimizing a query which compresses tick price data into O-H-L-C intervals. I'm trying to accomplish this with a single query, instead of having to use multiple queries with row partitioning to determine the Open and Close.
With the help of a great response to a question I posted yesterday, I've come up with this so far:
DECLARE #Interval INT = 5
DECLARE #InstrumentId INT = 36
DECLARE #Start_Date DATETIME = '2015-01-01'
DECLARE #End_Date DATETIME = '2015-03-30'
DECLARE #OffsetTime DATETIME = 0
SELECT INSTRUMENT_ID,
DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime) INTERVAL_TIME,
SUBSTRING(MIN(CONVERT(VARCHAR(24),[TIME_STAMP],21) + '_' +
CAST(RATE_BID AS VARCHAR(10))),25,8) [OPEN],
MAX(RATE_BID) HIGH,
MIN(RATE_BID) LOW,
SUBSTRING(MAX(CONVERT(VARCHAR(24),[TIME_STAMP],21) + '_' +
CAST(RATE_BID AS VARCHAR(10))),25,8) [CLOSE]
FROM dbo.TICKS
WHERE INSTRUMENT_ID = #InstrumentId AND TIME_STAMP BETWEEN #Start_Date AND #End_Date
GROUP BY DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime),
INSTRUMENT_ID
ORDER BY INTERVAL_TIME
Is there a more efficient way to concatenate the date/time to the price, then extract only the price once the MIN and MAX is evaluated? I was thinking about a binary representation of the date/time , adding it to the price , then some bit operation to extract the price. I'm not quite sure where to begin.
To test the performance of a query, it would be useful to have DDL and sample data. I am assuming the following table structure:
CREATE TABLE TICKS (
INSTRUMENT_ID INT,
TIME_STAMP DATETIME,
PRIMARY KEY (INSTRUMENT_ID, TIME_STAMP),
RATE_BID INT NOT NULL
)
To generate some sample data, I used the following code:
CREATE FUNCTION dbo.Numbers(#N int)
RETURNS TABLE AS RETURN
WITH
L0 AS(SELECT 1 AS C UNION ALL SELECT 1 AS O), -- 2 rows
L1 AS(SELECT 1 AS C FROM L0 AS A CROSS JOIN L0 AS B), -- 4 rows
L2 AS(SELECT 1 AS C FROM L1 AS A CROSS JOIN L1 AS B), -- 16 rows
L3 AS(SELECT 1 AS C FROM L2 AS A CROSS JOIN L2 AS B), -- 256 rows
L4 AS(SELECT 1 AS C FROM L3 AS A CROSS JOIN L3 AS B), -- 65,536 rows
L5 AS(SELECT 1 AS C FROM L4 AS A CROSS JOIN L4 AS B), -- 4,294,967,296 rows
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Number FROM L5)
SELECT Number FROM Nums WHERE Number<=#N
GO
--DELETE dbo.TICKS
SET NOCOUNT ON
DECLARE #INSTRUMENT_ID INT
SET #INSTRUMENT_ID=1
WHILE #INSTRUMENT_ID<50 BEGIN
DECLARE RandomData CURSOR LOCAL READ_ONLY FOR
SELECT TIME_STAMP, CONVERT(INT,RAND(CHECKSUM(NEWID()))*10) AS Delta,
CONVERT(BIT,CONVERT(INT,RAND(CHECKSUM(NEWID()))*1.1)) AS ChangeDirection
FROM (
--SELECT DATEADD(MINUTE,Number,'20150101') AS TIME_STAMP FROM dbo.Numbers(150000)
SELECT DATEADD(SECOND,Number*10,'20150101') AS TIME_STAMP FROM dbo.Numbers(900000)
) x
WHERE DATEPART(HOUR,TIME_STAMP) BETWEEN 8 AND 15
AND (DATEPART(WEEKDAY,TIME_STAMP)+##DATEFIRST)%7>1
AND TIME_STAMP>'20150103'
OPEN RandomData
DECLARE #TIME_STAMP DATETIME, #Delta INT, #ChangeDirection BIT
DECLARE #RATE_BID INT, #Direction SMALLINT
SET #RATE_BID=CONVERT(INT,RAND(CHECKSUM(NEWID()))*10000)+100
SET #Direction=1
WHILE 1=1 BEGIN
FETCH NEXT FROM RandomData INTO #TIME_STAMP, #Delta, #ChangeDirection
IF ##FETCH_STATUS<>0 BREAK
SET #Direction=CASE WHEN #ChangeDirection=1 THEN -#Direction ELSE #Direction END
IF #RATE_BID<100 AND #Direction<0 SET #Direction=1
SET #RATE_BID=#RATE_BID+#Delta*#Direction
INSERT INTO dbo.TICKS VALUES (#INSTRUMENT_ID, #TIME_STAMP, #RATE_BID)
END
CLOSE RandomData
DEALLOCATE RandomData
SET #INSTRUMENT_ID=#INSTRUMENT_ID+1
END
SET NOCOUNT OFF
Then I tested your original query against a variation using binary data types instead of strings:
DECLARE #Interval INT = 5
DECLARE #InstrumentId INT = 36
DECLARE #Start_Date DATETIME = '2015-01-01'
DECLARE #End_Date DATETIME = '2015-03-30'
DECLARE #OffsetTime DATETIME = 0
DECLARE #StartTime DATETIME
SET #StartTime=GETDATE()
SELECT INSTRUMENT_ID,
DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime) INTERVAL_TIME,
SUBSTRING(MIN(CONVERT(VARCHAR(24),[TIME_STAMP],21) + '_' +
CAST(RATE_BID AS VARCHAR(10))),25,8) [OPEN],
MAX(RATE_BID) HIGH,
MIN(RATE_BID) LOW,
SUBSTRING(MAX(CONVERT(VARCHAR(24),[TIME_STAMP],21) + '_' +
CAST(RATE_BID AS VARCHAR(10))),25,8) [CLOSE]
FROM dbo.TICKS
WHERE INSTRUMENT_ID = #InstrumentId AND TIME_STAMP BETWEEN #Start_Date AND #End_Date
GROUP BY DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime),
INSTRUMENT_ID
ORDER BY INTERVAL_TIME
PRINT CONVERT(NUMERIC(10,3),DATEDIFF(MS,#StartTime,GETDATE())/1000.)
SET #StartTime=GETDATE()
SELECT INSTRUMENT_ID,
DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime) INTERVAL_TIME,
CONVERT(INT,SUBSTRING(MIN(CONVERT(BINARY(8),[TIME_STAMP]) +
CAST(RATE_BID AS BINARY(4))),9,4)) [OPEN],
MAX(RATE_BID) HIGH,
MIN(RATE_BID) LOW,
CONVERT(INT,SUBSTRING(MAX(CONVERT(BINARY(8),[TIME_STAMP]) +
CAST(RATE_BID AS BINARY(4))),9,4)) [CLOSE]
FROM dbo.TICKS
WHERE INSTRUMENT_ID = #InstrumentId AND TIME_STAMP BETWEEN #Start_Date AND #End_Date
GROUP BY DATEADD(minute,(DATEDIFF(minute,#OffsetTime,[TIME_STAMP])/#Interval)*#Interval,#OffsetTime),
INSTRUMENT_ID
ORDER BY INTERVAL_TIME
PRINT CONVERT(NUMERIC(10,3),DATEDIFF(MS,#StartTime,GETDATE())/1000.)
On my system, I the string version executes in 490ms, and the binary version in 293ms.