Faster approach to enter rows in a time table in ms sql - sql-server

I need to fill a time table to use it for joining the data in reporting services.
Generally I do this with this code:
TRUNCATE TABLE tqTimeTable
DECLARE #CNT int
DECLARE #DATE datetime
DECLARE #END int
SET #CNT = 1
SET #DATE = 25567 -- 01.01.1970
SET #END = 20000 -- + 20k days => years 2024
WHILE(#CNT < #END)
BEGIN
INSERT INTO tqTimeTable (Tag, Monat, Jahr)
VALUES (DATEADD(day,#CNT,#DATE), MONTH(DATEADD(day,#CNT,#DATE)), YEAR(DATEADD(day,#CNT,#DATE)))
SET #CNT = #CNT + 1
END;
But this takes a while (on my test system around 2 minutes) so I hope someone had the same issue and solved it better then me.
As I fire this statement from a .NET connection I need a faster solution or if there isn't one to raise the timeout of my connection.

Simply adding
BEGIN TRAN
WHILE(#CNT < #END)
BEGIN
INSERT INTO tqTimeTable (Tag, Monat, Jahr)
VALUES (DATEADD(day,#CNT,#DATE), MONTH(DATEADD(day,#CNT,#DATE)), YEAR(DATEADD(day,#CNT,#DATE)))
SET #CNT = #CNT + 1
END;
COMMIT
will speed it up as you are doing many individual commits (that all require the log to be written to disc). I would do a set based insert in a single statement though.
WITH E1(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 UNION ALL SELECT 1 UNION ALL SELECT 1
) -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b) -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b) -- 1*10^4 or 10,000 rows
, E8(N) AS (SELECT 1 FROM E4 a, E4 b) -- 1*10^8 or 100,000,000 rows
, NUMS(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) FROM E8)
INSERT INTO tqTimeTable
(Tag,
Monat,
Jahr)
SELECT DATEADD(day, N, #DATE),
MONTH(DATEADD(day, N, #DATE)),
YEAR(DATEADD(day, N, #DATE))
FROM NUMS
WHERE N <= 20000

Related

Translating SQL Server Cursor to Azure Synapse

I have the following code that loops through a table with unique model numbers and creates a new table that contains, for each model numbers, a row based on the year and week number. How can I translate this so it doesn't use a cursor?
DECLARE #current_model varchar(50);
--declare a cursor that iterates through model numbers in ItemInformation table
DECLARE model_cursor CURSOR FOR
SELECT model from ItemInformation
--start the cursor
OPEN model_cursor
--get the next (first value)
FETCH NEXT FROM model_cursor INTO #current_model;
DECLARE #year_counter SMALLINT;
DECLARE #week_counter TINYINT;
WHILE (##FETCH_STATUS = 0) --fetch status returns the status of the last cursor, if 0 then there is a next value (FETCH statement was successful)
BEGIN
SET #year_counter = 2019;
WHILE (#year_counter <= Datepart(year, Getdate() - 1) + 2)
BEGIN
SET #week_counter = 1;
WHILE (#week_counter <= 52)
BEGIN
INSERT INTO dbo.ModelCalendar(
model,
sales_year,
sales_week
)
VALUES(
#current_model,
#year_counter,
#week_counter
)
SET #week_counter = #week_counter + 1
END
SET #year_counter = #year_counter + 1
END
FETCH NEXT FROM model_cursor INTO #current_model
END;
CLOSE model_cursor;
DEALLOCATE model_cursor;
If ItemInformation contains the following table:
model,invoice
a,4.99
b,9.99
c,1.99
d,8.99
then the expected output is:
model,sales_year,sales_week
A,2019,1
A,2019,2
A,2019,3
...
A,2019,52
A,2020,1
A,2020,2
A,2020,3
...
A,2020,51
A,2020,52
A,2020,53 (this is 53 because 2020 is leap year and has 53 weeks)
A,2021,1
A,2021,2
...
A,2022,1
A,2022,2
...
A,2022,52
B,2019,1
B,2019,2
...
D, 2022,52
Using CTE's you can get all combinations of weeks and years within the range required. Then join your data table on.
declare #Test table (model varchar(1), invoice varchar(4));
insert into #Test (model, invoice)
values
('a', '4.99'),
('b', '9.99'),
('c', '1.99'),
('d', '8.99');
with Week_CTE as (
select 1 as WeekNo
union all
select 1 + WeekNo
from Week_CTE
where WeekNo < 53
), Year_CTE as (
select 2019 YearNo
union all
select 1 + YearNo
from Year_CTE
where YearNo <= datepart(year, current_timestamp)
)
select T.model, yr.YearNo, wk.WeekNo
from Week_CTE wk
cross join (
select YearNo
-- Find the last week of the year (52 or 53) -- might need to change the start day of the week for this to be correct
, datepart(week, dateadd(day, -1, dateadd(year, 1, '01 Jan ' + convert(varchar(4),YearNo)))) LastWeek
from Year_CTE yr
) yr
cross join (
-- Assuming only a single row per model is required, and the invoice column can be ignored
select model
from #Test
group by model
) T
where wk.WeekNo <= yr.LastWeek
order by yr.YearNo, wk.WeekNo;
As you have advised that using a recursive CTE is not an option, you can try using a CTE without recursion:
with T(N) as (
select X.N
from (values (0),(0),(0),(0),(0),(0),(0),(0)) X(N)
), W(N) as (
select top (53) row_number() over (order by ##version) as N
from T T1
cross join T T2
), Y(N) as (
-- Upper limit on number of years
select top (12) 2018 + row_number() over (order by ##version) AS N
from T T1
cross join T T2
)
select W.N as WeekNo, Y.N YearNo, T.model
from W
cross join (
select N
-- Find the last week of the year (52 or 53) -- might need to change the start day of the week for this to be correct
, datepart(week, dateadd(day, -1, dateadd(year, 1, '01 Jan ' + convert(varchar(4),N)))) LastWeek
from Y
) Y
cross join (
-- Assuming only a single row per model is required, and the invoice column can be ignored
select model
from #Test
group by model
) T
-- Filter to required number of years.
where Y.N <= datepart(year, current_timestamp) + 1
and W.N <= Y.LastWeek
order by Y.N, W.N, T.model;
Note: If you setup your sample data in future with the DDL/DML as shown here you will greatly assist people attempting to answer.
I don't like to see a loop solution where a set solution can be found. So here goes Take II with no CTE, no values and no row_number() (the table variable is just to simulate your data so not part of the actual solution):
declare #Test table (model varchar(1), invoice varchar(4));
insert into #Test (model, invoice)
values
('a', '4.99'),
('b', '9.99'),
('c', '1.99'),
('d', '8.99');
select Y.N + 2019 YearNumber, W.WeekNumber, T.Model
from (
-- Cross join 5 * 10, then filter to 52/53 as required
select W1.N * 10 + W2.N + 1 WeekNumber
from (
select 0 N
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
) W1
cross join (
select 0 N
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9
) W2
) W
-- Cross join number of years required, just ensure its more than will ever be needed then filter back
cross join (
select 0 N
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9
) Y
cross join (
-- Assuming only a single row per model is required, and the invoice column can be ignored
select model
from #Test
group by model
) T
-- Some filter to restrict the years
where Y.N <= 3
-- Some filter to restrict the weeks
and W.WeekNumber <= 53
order by YearNumber, WeekNumber;
I created a table to temporary calendar table containing all the weeks and years. To account for leap years, I took the last 7 days of a year and got the ISO week for each day. To know how many weeks are in a year, I put these values into another temp table and took the max value of it. Azure Synapse doesn't support multiple values in one insert so it looks a lot longer than it should be. I also have to declare each as variable since Synapse can only insert literal or variable. I then cross-joined with my ItemInformation table.
CREATE TABLE #temp_dates
(
year SMALLINT,
week TINYINT
);
CREATE TABLE #temp_weeks
(
week_num TINYINT
);
DECLARE #year_counter SMALLINT
SET #year_counter = 2019
DECLARE #week_counter TINYINT
WHILE ( #year_counter <= Datepart(year, Getdate() - 1) + 2 )
BEGIN
SET #week_counter = 1;
DECLARE #day_1 TINYINT
SET #day_1 = Datepart(isowk, Concat('12-25-', #year_counter))
DECLARE #day_2 TINYINT
SET #day_2 = Datepart(isowk, Concat('12-26-', #year_counter))
DECLARE #day_3 TINYINT
SET #day_3 = Datepart(isowk, Concat('12-27-', #year_counter))
DECLARE #day_4 TINYINT
SET #day_4 = Datepart(isowk, Concat('12-28-', #year_counter))
DECLARE #day_5 TINYINT
SET #day_5 = Datepart(isowk, Concat('12-29-', #year_counter))
DECLARE #day_6 TINYINT
SET #day_6 = Datepart(isowk, Concat('12-30-', #year_counter))
DECLARE #day_7 TINYINT
SET #day_7 = Datepart(isowk, Concat('12-31-', #year_counter))
TRUNCATE TABLE #temp_weeks
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_1)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_2)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_3)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_4)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_5)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_6)
INSERT INTO #temp_weeks
(week_num)
VALUES (#day_7)
DECLARE #max_week TINYINT
SET #max_week = (SELECT Max(week_num)
FROM #temp_weeks)
WHILE ( #week_counter <= #max_week )
BEGIN
INSERT INTO #temp_dates
(year,
week)
VALUES ( #year_counter,
#week_counter )
SET #week_counter = #week_counter + 1
END
SET #year_counter = #year_counter + 1
END
DROP TABLE #temp_weeks;
SELECT i.model,
d.year,
d.week
FROM dbo.iteminformation i
CROSS JOIN #temp_dates d
ORDER BY model,
year,
week
DROP TABLE #temp_dates

SQL unable to alter view having DECLARE statement

I'm trying to alter the view below:
ALTER VIEW [dbo].[Win.v_TodayMin365]
AS
declare #tblOut table(Dates Date)
declare #cnt INT = -365
while #cnt < 0
Begin
set #cnt = #cnt + 1;
insert into #tblOut (Dates) values(cast(dateadd(day,#cnt,getdate()) as Date))
END
select * from #tblOut
deallocate #tblOut
deallocate #cnt
GO
The code as such works (if I highlight all between AS and GO and hit Execute, I get the expected output), but I can't run it as ALTER VIEW. Then, I get the following error:
Incorrect syntax near the keyword 'declare'.
Expecting '(', SELECT or WITH
Thanks in advance for any idea!
You cannot Declare a variable inside a View
Actually you don't need to use While loop for this. Use a tally table trick to generate dates much efficient than the while loop approach
ALTER VIEW [dbo].[Win.v_TodayMin365]
AS
WITH E1(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 UNION ALL SELECT 1 UNION ALL
SELECT 1), --10E+1 or 10 rows
E2(N)
AS (SELECT 1 FROM E1 a,E1 b), --10E+2 or 100 rows
E4(N)
AS (SELECT 1 FROM E2 a,E2 b), --10E+4 or 10,000 rows max
calendar
AS (SELECT Dateadd(dd, Row_number()OVER(ORDER BY n), Dateadd(yy, -1, Cast(Getdate() + 1 AS DATE))) AS dates
FROM E4 l)
SELECT *
FROM calendar
WHERE dates <= Cast(Getdate() AS DATE)
This even can be converted to Table valued function or a Stored Procedure.

Improve running time in T-sql code

I would like to Improve running time in T-sql code.
The Insert query is taking a long time. Is there any chance to use a different method ?
Here is my Code
Create PROCEDURE [dbo].[CreateTableDemog]
#Age int,
#RetiredAge int
as
begin
declare #t as int=1;
declare #tlast as int=#RetiredAge -#Age
declare #Period as int=0;
BEGIN
while #t<=#tlast
begin
while #Period <=#t-1
begin
INSERT INTO dbo.Table (t, Age,Period,Prob)
VALUES (#t,#Age+#t,#Period,1);
set #Period=#Period+1
end
set #Period=0
set #t=#t+1
end
end
Here is how you could do this using a tally table instead of the nested while loops. This is a strange requirement but easy enough to follow.
This article by Jeff Moden does a great job of explaining what a tally is and how you can use it. http://www.sqlservercentral.com/articles/T-SQL/62867/
Create PROCEDURE [dbo].[CreateTableDemog]
(
#Age int
, #RetiredAge int
) as
set nocount on;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
INSERT INTO dbo.Table
(
t
, Age
, Period
, Prob
)
select t1.N as T
, #Age + t1.N as AgePlusT
, abs(t2.N - t1.N) as Period
, 1
from cteTally t1
join cteTally t2 on t2.N <= t1.N
where t1.N <= #RetiredAge - #Age
order by t1.N
, #Age + t1.N
, abs(t2.N - t1.N);
GO

Sql UDF Optimization

I have written the following function that takes in two strings (comma-separated), splits them into two different temp tables and then uses those temp tables to find what percentage of words match in those two temp tables. The problem is that when I am using it per row basis on a data set of about 200k rows, the query times out!
Are there any optimizations that you can see that can be done?
ALTER FUNCTION [GetWordSimilarity](#String varchar(8000),
#String2 varchar(8000),#Delimiter char(1))
returns decimal(16,2)
as
begin
declare #result as decimal (16,2)
declare #temptable table (items varchar(8000))
declare #temptable2 table (items varchar(8000))
declare #numberOfCommonWords decimal(16,2)
declare #countTable1 decimal(16,2)
declare #countTable2 decimal(16,2)
declare #denominator decimal(16,2)
set #result = 0.0 --dummy value
declare #idx int
declare #slice varchar(8000)
select #idx = 1
if len(#String)<1 or #String is null or len(#String2) = 0 or #String2 is null return 0.0
--populating #temptable
while #idx!= 0
begin
set #idx = charindex(#Delimiter,#String)
if #idx!=0
set #slice = left(#String,#idx - 1)
else
set #slice = #String
if(len(#slice)>0)
insert into #temptable(Items) values(ltrim(rtrim(#slice)))
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end
select #idx = 1
----populating #temptable2
while #idx!= 0
begin
set #idx = charindex(#Delimiter,#String2)
if #idx!=0
set #slice = left(#String2,#idx - 1)
else
set #slice = #String2
if(len(#slice)>0)
insert into #temptable2(Items) values(ltrim(rtrim(#slice)))
set #String2 = right(#String2,len(#String2) - #idx)
if len(#String2) = 0 break
end
--calculating percentage of words match
if (((select COUNT(*) from #temptable) = 0) or ((select COUNT(*) from #temptable2) = 0))
return 0.0
select #numberOfCommonWords = COUNT(*) from
(
select distinct items from #temptable
intersect
select distinct items from #temptable2
) a
select #countTable1 = COUNT (*) from #temptable
select #countTable2 = COUNT (*) from #temptable2
if(#countTable1 > #countTable2) set #denominator = #countTable1
else set #denominator = #countTable2
set #result = #numberOfCommonWords/#denominator
return #result
end
Thanks a bunch !
There is no way to write a T SQL UDF with heavy string manipulation inside that will behave OK on large number of rows. You will get some gain if you use the Numbers table, though:
declare
#col_list varchar(1000),
#sep char(1)
set #col_list = 'TransactionID, ProductID, ReferenceOrderID, ReferenceOrderLineID, TransactionDate, TransactionType, Quantity, ActualCost, ModifiedDate'
set #sep = ','
select substring(#col_list, n, charindex(#sep, #col_list + #sep, n) - n)
from numbers where substring(#sep + #col_list, n, 1) = #sep
and n < len(#col_list) + 1
Your best course of action would be to write the whole thing in SQLCLR.
The problem of course is with the design. You shouldn't be storing comma-separated data in a SQL database to start with.
But, I guess we're stuck with it for now.
One thing to consider is converting the function to SQLCLR; SQL by itself is not very good with string operations. (Well, in fact, no language is good with string operations IMHO but SQL really is bad at it =)
The splitter you use to fill #Temptables 1 & 2 can be optimized by using the code from Jeff Moden who wrote several fantastic articles of which the last one can be found here : http://www.sqlservercentral.com/articles/Tally+Table/72993/
Taking his splitter + optimizing the rest of the code a bit I managed to get from 771 seconds to 305 seconds on a 200K random data sample.
Some things to note: the results aren't quite the same. I checked some manually and I actually think the new results are more accurate but don't really have time to go bughunting on both versions.
I tried to convert this to a more set-based approach where I first load all the words in a table that has all words for all row_id's and then join them back together. Although the joining is quite fast, it simply takes too long to create the initial tables so it even loses out on the original function.
Maybe I'll try to figure out another way to make it faster but for now I hope this will help you out a bit.
ALTER FUNCTION [GetWordSimilarity2](#String1 varchar(8000),
#String2 varchar(8000),#Delimiter char(1))
returns decimal(16,2)
as
begin
declare #temptable1 table (items varchar(8000), row_id int IDENTITY(1, 1), PRIMARY KEY (items, row_id))
declare #temptable2 table (items varchar(8000), row_id int IDENTITY(1, 1), PRIMARY KEY (items, row_id))
declare #numberOfCommonWords decimal(16,2)
declare #countTable1 decimal(16,2)
declare #countTable2 decimal(16,2)
-- based on code from Jeff Moden (http://www.sqlservercentral.com/articles/Tally+Table/72993/)
--populating #temptable1
;WITH E1(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 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#String1),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#String1,t.N,1) = #Delimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#Delimiter,#String1,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
INSERT #temptable1 (items)
SELECT Item = SUBSTRING(#String1, l.N1, l.L1)
FROM cteLen l
SELECT #countTable1 = ##ROWCOUNT
----populating #temptable2
;WITH E1(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 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#String2),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#String2,t.N,1) = #Delimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#Delimiter,#String2,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
INSERT #temptable2 (items)
SELECT Item = SUBSTRING(#String2, l.N1, l.L1)
FROM cteLen l
SELECT #countTable2 = ##ROWCOUNT
--calculating percentage of words match
if #countTable1 = 0 OR #countTable2 = 0
return 0.0
select #numberOfCommonWords = COUNT(DISTINCT t1.items)
from #temptable1 t1
JOIN #temptable2 t2
ON t1.items = t2.items
RETURN #numberOfCommonWords / (CASE WHEN (#countTable1 > #countTable2) THEN #countTable1 ELSE #countTable2 END)
end

How to make this sql query

I have 2 SQL Server tables with the following structure
Turns-time
cod_turn (PrimaryKey)
time (datetime)
Taken turns
cod_taken_turn (Primary Key)
cod_turn
...
and several other fields which are irrelevant to the problem. I cant alter the table structures because the app was made by someone else.
given a numeric variable parameter, which we will assume to be "3" for this example, and a given time, I need to create a query which looking from that time on, it looks the first 3 consecutive records by time which are not marked as "taken". For example:
For example, for these turns, starting by the time of "8:00" chosen by the user
8:00 (not taken)
9:00 (not taken)
10:00 (taken)
11:00 (not taken)
12:00 (not taken)
13:00 (not taken)
14:00 (taken)
The query it would have to list
11:00
12:00
13:00
I cant figure out how to make the query in pure sql, if possible.
with a cursor
declare #GivenTime datetime,
#GivenSequence int;
select #GivenTime = cast('08:00' as datetime),
#GivenSequence = 3;
declare #sequence int,
#code_turn int,
#time datetime,
#taked int,
#firstTimeInSequence datetime;
set #sequence = 0;
declare turnCursor cursor FAST_FORWARD for
select turn.cod_turn, turn.[time], taken.cod_taken_turn
from [Turns-time] as turn
left join [Taken turns] as taken on turn.cod_turn = taken.cod_turn
where turn.[time] >= #GivenTime
order by turn.[time] asc;
open turnCursor;
fetch next from turnCursor into #code_turn, #time, #taked;
while ##fetch_status = 0 AND #sequence < #GivenSequence
begin
if #taked IS NULL
select #firstTimeInSequence = coalesce(#firstTimeInSequence, #time)
,#sequence = #sequence + 1;
else
select #sequence = 0,
#firstTimeInSequence = null;
fetch next from turnCursor into #code_turn, #time, #taked;
end
close turnCursor;
deallocate turnCursor;
if #sequence = #GivenSequence
select top (#GivenSequence) * from [Turns-time] where [time] >= #firstTimeInSequence
order by [time] asc
WITH Base AS (
SELECT *,
CASE WHEN EXISTS(
SELECT *
FROM Taken_turns taken
WHERE taken.cod_turn = turns.cod_turn) THEN 1 ELSE 0 END AS taken
FROM [Turns-time] turns)
, RecursiveCTE As (
SELECT TOP 1 cod_turn, [time], taken AS run, 0 AS grp
FROM Base
WHERE [time] >= #start_time
ORDER BY [time]
UNION ALL
SELECT R.cod_turn, R.[time], R.run, R.grp
FROM (
SELECT T.*,
CASE WHEN T.taken = 0 THEN 0 ELSE run+1 END AS run,
CASE WHEN T.taken = 0 THEN grp + 1 ELSE grp END AS grp,
rn = ROW_NUMBER() OVER (ORDER BY T.[time])
FROM Base T
JOIN RecursiveCTE R
ON R.[time] < T.[time]
) R
WHERE R.rn = 1 AND run < #run_length
), T AS(
SELECT *,
MAX(grp) OVER () AS FinalGroup,
COUNT(*) OVER (PARTITION BY grp) AS group_size
FROM RecursiveCTE
)
SELECT cod_turn,time
FROM T
WHERE grp=FinalGroup AND group_size=#run_length
I think there is not a simple way to achieve this.
But probably there are many complex ways :). This is an approach that should work in Transact-SQL:
CREATE TABLE #CONSECUTIVE_TURNS (id int identity, time datetime, consecutive int)
INSERT INTO #CONSECUTIVE_TURNS (time, consecutive, 0)
SELECT cod_turn
, time
, 0
FROM Turns-time
ORDER BY time
DECLARE #i int
#n int
SET #i = 0
SET #n = 3 -- Number of consecutive not taken records
while (#i < #n) begin
UPDATE #CONSECUTIVE_TURNS
SET consecutive = consecutive + 1
WHERE not exists (SELECT 1
FROM Taken-turns
WHERE id = cod_turn + #i
)
SET #i = #i + 1
end
DECLARE #firstElement int
SELECT #firstElement = min(id)
FROM #CONSECUTIVE_TURNS
WHERE consecutive >= #n
SELECT *
FROM #CONSECUTIVE_TURNS
WHERE id between #firstElement
and #firstElement + #n - 1
This is untested but I think it will work.
Pure SQL
SELECT TOP 3 time FROM [turns-time] WHERE time >= (
-- get first result of the 3 consecutive results
SELECT TOP 1 time AS first_result
FROM [turns-time] tt
-- start from given time, which is 8:00 in this case
WHERE time >= '08:00'
-- turn is not taken
AND cod_turn NOT IN (SELECT cod_turn FROM taken_turns)
-- 3 consecutive turns from current turn are not taken
AND (
SELECT COUNT(*) FROM
(
SELECT TOP 3 cod_turn AS selected_turn FROM [turns-time] tt2 WHERE tt2.time >= tt.time
GROUP BY cod_turn ORDER BY tt2.time
) AS temp
WHERE selected_turn NOT IN (SELECT cod_turn FROM taken_turns)) = 3
) ORDER BY time
Note: I tested it on Postgresql (with some code modification), but not MS SQL Server. I'm not sure about performance compared to T-SQL.
Another set-based solution (tested):
DECLARE #Results TABLE
(
cod_turn INT NOT NULL
,[status] TINYINT NOT NULL
,RowNumber INT PRIMARY KEY
);
INSERT #Results (cod_turn, [status], RowNumber)
SELECT a.cod_turn
,CASE WHEN b.cod_turn IS NULL THEN 1 ELSE 0 END [status] --1=(not taken), 0=(taken)
,ROW_NUMBER() OVER(ORDER BY a.[time]) AS RowNumber
FROM [Turns-time] a
LEFT JOIN [Taken_turns] b ON a.cod_turn = b.cod_turn
WHERE a.[time] >= #Start;
--SELECT * FROM #Results r ORDER BY r.RowNumber;
SELECT *
FROM
(
SELECT TOP(1) ca.LastRowNumber
FROM #Results a
CROSS APPLY
(
SELECT SUM(c.status) CountNotTaken, MAX(c.RowNumber) LastRowNumber
FROM
(
SELECT TOP(#Len)
b.RowNumber, b.[status]
FROM #Results b
WHERE b.RowNumber <= a.RowNumber
ORDER BY b.RowNumber DESC
) c
) ca
WHERE ca.CountNotTaken = #Len
ORDER BY a.RowNumber ASC
) x INNER JOIN #Results y ON x.LastRowNumber - #Len + 1 <= y.RowNumber AND y.RowNumber <= x.LastRowNumber;

Resources