SQL Server - Query With Multiple Date Ranges in subquery - sql

I have used conditional aggregation in another query where I needed to use multiple date ranges. In this case the date ranges are needed in a sub-query.
I would like to know if can I get desired results in one single query (without using UNION).
I need to check if a given record EXISTS in the subquery with date-range. Since I need to use EXISTS rather than a join - I am running into this issue.
Here is a sample script/data. The expected results table is for demonstration.
IF OBJECT_ID('tempdb..#Entity') IS NOT NULL DROP TABLE #Entity
IF OBJECT_ID('tempdb..#EntityDate') IS NOT NULL DROP TABLE #EntityDate
IF OBJECT_ID('tempdb..#ExpectedOutput') IS NOT NULL DROP TABLE #ExpectedOutput
> `DECLARE #FortnightStart DATETIME = '2020/08/01', #FortnightEnd DATETIME = '2020/08/14 23:59:59'
DECLARE #QuarterStart DATE = '2020/04/01', #QuarterEnd DATE = '2020/06/30 23:59:59'
> `SELECT 'Fortnight' DateRange, #FortnightStart 'Start', #FortnightEnd 'End'
UNION
SELECT 'Quarter', #QuarterStart, #QuarterEnd
CREATE TABLE #Entity (
EntityId INT IDENTITY(1, 1),
EntityName VARCHAR(50)
)
CREATE TABLE #EntityDate (
EntityDateId INT IDENTITY(1, 1),
EntityId INT,
SubmittedDate DATETIME
)
ALTER TABLE #EntityDate ADD CONSTRAINT FK_EntityDate_Entity FOREIGN KEY (EntityId) REFERENCES Entity(EntityId)
INSERT INTO #Entity (EntityName)
SELECT 'Alice'
UNION
SELECT 'Bob'
UNION
SELECT 'Cameron'
UNION
SELECT 'Diego'
UNION
SELECT 'Elliot'
SELECT * FROM #Entity
INSERT INTO #EntityDate(EntityId, SubmittedDate)
SELECT 1, '08/01/2020 11:00:00' -- only 1 record is expected in the output for this Entity
UNION
SELECT 1, '08/10/2020 10:00:00'
UNION
SELECT 1, '04/10/2020 10:00:00' -- this record should show up for the quarter date range
UNION
SELECT 2, '06/01/2020 11:00:00' --
UNION
SELECT 3, '05/01/2020' -- only 1 record is expected in the output for this Entity
UNION
SELECT 3, '06/01/2020'
UNION
SELECT 4, '10/01/2021' -- does not fit in any date range
UNION
SELECT 5, '08/02/2020'
SELECT *
FROM #EntityDate d
INNER JOIN #Entity e ON d.EntityId = e.EntityId
SELECT *
FROM #Entity E
WHERE EXISTS ( SELECT 1
FROM #EntityDate d
WHERE SubmittedDate BETWEEN #FortnightStart AND #FortnightEnd AND e.EntityId = D.EntityId
)
SELECT *
FROM #Entity E
WHERE EXISTS ( SELECT 1
FROM #EntityDate d
WHERE SubmittedDate BETWEEN #QuarterStart AND #QuarterEnd AND e.EntityId = D.EntityId
)
CREATE TABLE #ExpectedOutput
(
EntityId INT,
DateRange VARCHAR(50)
)
INSERT INTO #ExpectedOutput (EntityId, DateRange)
SELECT 1, 'Fortnight'
UNION
SELECT 5, 'Fortnight'
UNION
SELECT 1, 'Quarter'
UNION
SELECT 2, 'Quarter'
UNION
SELECT 3, 'Quarter'
SELECT o.*, e.EntityName
FROM #ExpectedOutput o
INNER JOIN #Entity e ON o.EntityId = e.EntityId
ORDER BY O.DateRange, o.EntityId

Using the virtual Dates table you created at the top of your script, you need to join that to Entity, using the EXISTS as the ON condition
DECLARE #FortnightStart DATETIME = '2020/08/01', #FortnightEnd DATETIME = '2020/08/14 23:59:59';
DECLARE #QuarterStart DATE = '2020/04/01', #QuarterEnd DATE = '2020/06/30 23:59:59';
WITH Dates AS (
SELECT 'Fortnight' DateRange, #FortnightStart Start, #FortnightEnd [End]
UNION ALL
SELECT 'Quarter', #QuarterStart, #QuarterEnd
)
SELECT
e.EntityId,
d.DateRange
FROM Dates d
JOIN #Entity E ON EXISTS (SELECT 1
FROM #EntityDate ed
WHERE ed.SubmittedDate BETWEEN d.Start AND d.[End]
AND ed.EntityId = e.EntityId
);
db<>fiddle

try something like this
SELECT * FROM (VALUES(1, '08/01/2020 11:00:00'),
(1, '08/10/2020 10:00:00'),
(1, '04/10/2020 10:00:00'),
(2, '06/01/2020 11:00:00'), --
(3, '05/01/2020'),
(3, '06/01/2020'),
(4, '10/01/2021'),
(5, '08/02/2020')
) EntityIDate(EntityId,SubmittedDate)
Documentation: https://learn.microsoft.com/en-us/u-sql/statements-and-expressions/select/from/select-selecting-from-the-values-table-value-constructor

Why you have such requirement ? What is harm in using multiple UNION ALL ? Performance wise there is no harm.
I hope I understood your requirement correctly.
DECLARE #FortnightStart DATETIME = '2020/08/01', #FortnightEnd DATETIME = '2020/08/14 23:59:59'
DECLARE #QuarterStart DATE = '2020/04/01', #QuarterEnd DATE = '2020/06/30 23:59:59'
;WITH CTE
AS (SELECT 1 AS Orderflg,
'Fortnight' DateRange,
#FortnightStart 'StartDate',
#FortnightEnd 'EndDate'
UNION ALL
SELECT 2,
'Quarter',
#QuarterStart,
#QuarterEnd),
CTE1
AS (SELECT *,
ROW_NUMBER() OVER(PARTITION BY EntityId,
Orderflg
ORDER BY SubmittedDate) rn
FROM #EntityDate d
CROSS APPLY
(
SELECT TOP 1 DateRange,
Orderflg
FROM CTE C
WHERE SubmittedDate >= StartDate
AND SubmittedDate < EndDate
) ca -- e.EntityId = D.EntityId
)
SELECT e.EntityId,
DateRange,
EntityName
FROM CTE1 C1
INNER JOIN #Entity E ON c1.EntityId = e.EntityId
WHERE rn = 1
ORDER BY Orderflg;

Related

How to Auto generate dates between date range using SQL Query?

I just want to generate the date between data range using SQL Query.
Source:
Result:
Thanks,
Lawrance A
Here is how to accomplish this by using a tally table to create a calendar table:
declare #source table
(
user_id int not null primary key clustered,
from_date date not null,
to_date date not null
);
insert into #source
values
(1, '02/20/2019', '02/23/2019'),
(2, '02/22/2019', '02/28/2019'),
(3, '03/01/2019', '03/05/2019');
with
rows as
(
select top 1000
n = 1
from sys.messages
),
tally as
(
select n = row_number() over(order by (select null)) - 1
from rows
),
calendar as
(
select
date = dateadd(dd, n, (select min(from_date) from #source))
from tally
)
select
s.user_id,
c.date
from #source s
cross join calendar c
where c.date between s.from_date and s.to_date;
Result set:

How to get the alternate dates between two dates in the same table and insert those dates into a temporary table?

I have a question. I have a table like this:
Actually, those dates are the start date and the end date of an employee who is working on a task. And in a month, usually they have more than one task.
What I want are the dates when they are idle or they don't have any task. So my question is, how to get those idle dates between those working dates and insert those idle dates into a temporary table?
Thank you :)
Starting with SQL 2012 there's the LEAD function.
It can be used to find gaps between ranges.
For example :
DECLARE #EmployeeAssignments TABLE (Id INT IDENTITY(1, 1), EmployeeId INT, SDate DATE, EDate DATE);
INSERT INTO #EmployeeAssignments (EmployeeId,SDate,EDate) values
(11505, '2016-10-01', '2016-10-05'),
(11505, '2016-10-09', '2016-10-12'),
(11505, '2016-10-14', '2016-10-20'),
(11506, '2016-10-02', '2016-10-05'),
(11506, '2016-10-08', '2016-10-14'),
(11506, '2016-10-15', '2016-10-19');
select *
from (
select EmployeeId,
dateadd(day,1,EDate) as StartDateGap,
dateadd(day,-1,lead(SDate) over (partition by EmployeeId order by SDate)) as EndDateGap
from #EmployeeAssignments
) as q
where StartDateGap <= EndDateGap
order by EmployeeId, StartDateGap, EndDateGap;
Returns:
EmployeeId StartDateGap EndDateGap
11505 2016-10-06 2016-10-08
11505 2016-10-13 2016-10-13
11506 2016-10-06 2016-10-07
To get those ranges as a list of dates?
One way to do that is by joining to a table with dates.
In the example below, a recursive query is used to generate those dates.
Only days between monday and friday are inserted.
Since we can expect that the employees would be idle on those days. ;)
But it's better to have a permanent table that also flags the holidays.
Also note that the first select on the #EmployeeAssignments is grouped.
Since the tasks cause a lot of duplicate date ranges.
DECLARE #EmployeeAssignments TABLE (Id INT IDENTITY(1,1), EmployeeId INT, TaskId int, SDate DATE, EDate DATE);
INSERT INTO #EmployeeAssignments (EmployeeId, TaskId, SDate, EDate) values
(11505,10,'2016-10-01','2016-10-05'),
(11505,12,'2016-10-09','2016-10-12'),
(11505,13,'2016-10-09','2016-10-12'),
(11505,14,'2016-10-14','2016-10-20'),
(11505,15,'2016-10-14','2016-10-20'),
(11506,16,'2016-10-02','2016-10-05'),
(11506,17,'2016-10-08','2016-10-14'),
(11506,18,'2016-10-15','2016-10-19');
DECLARE #Days TABLE (day DATE primary key);
declare #StartDate DATE = (select min(SDate) from #EmployeeAssignments);
declare #EndDate DATE = (select max(EDate) from #EmployeeAssignments);
-- fill up #Days with workingdays
with DAYS as (
select #StartDate as dt
union all
select dateadd(day,1,dt)
from DAYS
where dt < #EndDate
)
insert into #Days (day)
select dt from DAYS
where DATEPART(dw, dt) in (2,3,4,5,6); -- dw 2 to 6 = monday to friday
IF OBJECT_ID('tempdb..#EmployeeIdleDates') IS NOT NULL DROP TABLE #EmployeeIdleDates;
CREATE TABLE #EmployeeIdleDates (Id INT IDENTITY(1,1) primary key, EmployeeId INT, IdleDate DATE);
insert into #EmployeeIdleDates (EmployeeId, IdleDate)
select
a.EmployeeId,
d.day as IdleDate
from
(
select *
from (
select EmployeeId,
dateadd(day,1,EDate) as StartDateGap,
dateadd(day,-1,lead(SDate) over (partition by EmployeeId order by SDate)) as EndDateGap
from (
select EmployeeId, SDate, EDate
from #EmployeeAssignments
group by EmployeeId, SDate, EDate
) t
) as q
where StartDateGap <= EndDateGap
) a
inner join #Days d
on (d.day between a.StartDateGap and a.EndDateGap)
group by a.EmployeeId, d.day;
select * from #EmployeeIdleDates
order by EmployeeId, IdleDate;
What you need to work out is which dates have no corresponding time period in your source table. The easiest way I have found to tackle this problem is with a Dates table. If you don't have one of these in your database already, I highly recommend it as having a table of every date you'll need with relevant metadata such as whether it is the start or end of the month, weekends, holidays, etc is incredibly useful.
If you can't create one of these, you can derive a simple one using a recursive cte and then return all dates that aren't represented in your source table (This assumes you are reporting on one employee at a time):
declare #Tasks table(TaskID int
,EmployeeID int
,Sdate datetime
,Edate datetime
)
insert into #Tasks values
(1,1,'20160101','20160103')
,(2,1,'20160102','20160107')
,(3,1,'20160109','20160109')
,(4,1,'20160112','20160113')
,(5,1,'20160112','20160112')
,(1,2,'20160101','20160102')
,(2,2,'20160103','20160109')
declare #EmployeeID int = 1
declare #MinDate datetime = (select min(Sdate) from #Tasks where EmployeeID = #EmployeeID)
declare #MaxDate datetime = (select max(Edate) from #Tasks where EmployeeID = #EmployeeID)
;with cte as
(
select #MinDate as DateValue
union all
select dateadd(d,1,DateValue) as DateValue
from cte
where DateValue < #MaxDate
)
select #EmployeeID as EmployeeID
,c.DateValue as DatesIdle
from cte c
left join #Tasks t
on(c.DateValue BETWEEN T.Sdate AND T.Edate)
where t.EmployeeID is null
order by DatesIdle
First and foremost, please reconsider your approach to do this within the DB. The best place for data interpretation is at your application layer.
The below code will give you he gaps in the temp table #gaps. Of course, I have ignored irrelevant to the problem; you might want to add them. I used #temp in place of your table and inserted test values.
DECLARE #temp TABLE (
EmployeeID INT ,
Sdate DATE,
Edate DATE);
INSERT INTO #temp
VALUES (11505, '2016-05-26', '2016-05-26'),
(11505, '2016-05-27', '2016-05-31'),
(11505, '2016-06-01', '2016-06-01'),
(11505, '2016-06-02', '2016-06-03'),
(11505, '2016-06-02', '2016-06-03'),
(11505, '2016-06-05', '2016-06-06'),
(11505, '2016-06-05', '2016-06-06'),
(11505, '2016-06-06', '2016-06-06'),
(11505, '2016-06-06', '2016-06-06'),
(11505, '2016-06-07', '2016-06-08'),
(11505, '2016-06-07', '2016-06-07'),
(11505, '2016-06-07', '2016-06-07'),
(11505, '2016-06-07', '2016-06-07'),
(11505, '2016-06-15', '2016-06-15'),
(11505, '2016-06-16', '2016-06-20'),
(21505, '2016-05-26', '2016-05-26'),
(21505, '2016-05-27', '2016-05-31'),
(21505, '2016-06-01', '2016-06-01'),
(21505, '2016-06-02', '2016-06-03'),
(21505, '2016-06-02', '2016-06-03'),
(21505, '2016-06-02', '2016-06-06'),
(21505, '2016-06-02', '2016-06-06'),
(21505, '2016-06-06', '2016-06-06'),
(21505, '2016-06-06', '2016-06-06'),
(21505, '2016-06-07', '2016-06-08'),
(21505, '2016-07-02', '2016-07-02'),
(21505, '2016-07-03', '2016-07-03'),
(21505, '2016-07-07', '2016-07-10'),
(21505, '2016-07-14', '2016-06-14'),
(21505, '2016-06-13', '2016-06-15');
DECLARE #emp AS INT;
DECLARE #start AS DATE;
DECLARE #end AS DATE;
DECLARE #EmployeeID AS INT,
#Sdate AS DATE,
#Edate AS DATE;
DECLARE #gaps TABLE (
EmployeeID INT ,
Sdate DATE,
Edate DATE);
DECLARE RecSet CURSOR
FOR SELECT *
FROM #temp
ORDER BY EmployeeID ASC, Sdate ASC, Edate DESC;
OPEN RecSet;
FETCH NEXT FROM RecSet INTO #EmployeeID, #Sdate, #Edate;
SET #emp = #EmployeeID;
SET #start = #Sdate;
SET #end = dateadd(day, 1, #Edate);
WHILE (##FETCH_STATUS = 0)
BEGIN
IF #Sdate <= #end
BEGIN
IF #Edate > dateadd(day, -1, #end)
BEGIN
SET #end = dateadd(day, 1, #Edate);
END
END
ELSE
BEGIN
INSERT INTO #gaps
VALUES (#EmployeeID, #end, dateadd(day, -1, #Sdate));
SET #start = #Sdate;
SET #end = dateadd(day, 1, #Edate);
END
FETCH NEXT FROM RecSet INTO #EmployeeID, #Sdate, #Edate;
IF #emp != #EmployeeID
BEGIN
SET #emp = #EmployeeID;
SET #start = #Sdate;
SET #end = dateadd(day, 1, #Edate);
END
END
CLOSE RecSet;
DEALLOCATE RecSet;
SELECT *
FROM #gaps;
This gives #gaps as:
11505 2016-06-04 2016-06-04
11505 2016-06-09 2016-06-14
21505 2016-06-09 2016-06-12
21505 2016-06-16 2016-07-01
21505 2016-07-04 2016-07-06
21505 2016-07-11 2016-07-13
I can't see how to solve this without unrolling the days within a Scope.
Hence I use a Tally table in this example.
I provide here an example of two Persons.
For debugging simplicity I use month units.
select top 100000 identity(int, 1, 1) as Id
into #Tally
from master..spt_values as a
cross join master..spt_values as b
declare
#ScopeB date = '2015-01-01',
#ScopeE date = '2015-12-31'
declare #Task table
(
TaskID int identity,
PersonID int,
TaskB date,
TaskE date
)
insert #Task values
(1, '2015-01-01', '2015-04-30'), -- Person 1 mth 1, 2, 3, 4
(1, '2015-03-01', '2015-07-31'), -- Person 1 mth 3, 4, 5, 6, 7
(2, '2015-01-01', '2015-03-31'), -- Person 2 mth 1, 2, 3
(2, '2015-05-01', '2015-05-31'), -- Person 2 mth 5
(2, '2015-09-01', '2015-11-30') -- Person 2 mth 9, 10, 11
-- result: Person 1 free on mth 8, 9, 10, 11, 12
-- result: Person 2 free on mth 4, 6, 7, 8, 12
;
with
Scope as
(
select dateadd(day, ID - 1, #ScopeB) as Dates
from #Tally where ID <= datediff(day, #ScopeB, #ScopeE) + 1
and datename(dw, dateadd(day, ID - 1, #ScopeB)) not in ('Saturday', 'Sunday')
),
Person as
(
select distinct PersonID from #Task
),
Free as
(
select p.PersonID, s.Dates from Scope as s cross join Person as p
except
select distinct t.PersonID, s.Dates from Scope as s cross join #Task as t
where s.Dates between t.TaskB and t.TaskE
)
select PersonID, Dates,
datename(dw, Dates) from Free
order by 1, 2
drop table #Tally
If you have a Holiday table, you can use it at the WHERE condition at the final SELECT as: WHERE Dates NOT IN (SELECT Dates FROM Holiday).

How to use the minimum date from three available - SQL

I'm trying to plug a formula into a query to pull back how much should have run on a particular contract.
The formula itself is quite simple, but I can't find anywhere how to take the minimum date between 3, based on each record separately.
I need to calculate which is the earliest of Term_date, Suspend_date and today's date, some of which may be NULL, on each contract.
And interesting way to approach this is to use cross apply:
select t.contractid, mindte
from table t cross apply
(select min(dte) as mindte
from (values(t.term_date), (t.suspend_date), (getdate())) d(dte)
) d;
CASE
WHEN Term_date < Suspend_date AND Term_date < GETDATE() THEN Term_date
WHEN Suspend_date < GETDATE() THEN Suspend_date
ELSE GETDATE()
END AS MinimumDate
I know a CASE statement will be suggested, but I thought I'd try something different:
;WITH cte (RecordID, CheckDate) AS
( SELECT RecordID, Term_date FROM sourcetable UNION ALL
SELECT RecordID, Suspend_date FROM sourcetable UNION ALL
SELECT RecordID, GETDATE() FROM sourcetable )
SELECT src.RecordID, src.Field1, src.Field2, MinDate = MIN(cte.CheckDate)
FROM sourcetable src
LEFT JOIN cte ON cte.RecordID = src.RecordID
GROUP BY src.RecordID, src.Field1, src.Field2
Here is a method using cross apply to generate a work table from which you can get the minimum date:
-- mock table with sample testing data
declare #MyTable table
(
id int identity(1,1) primary key clustered,
term_date datetime null,
suspend_date datetime null
)
insert into #MyTable (term_date, suspend_date)
select null, null
union all select '1/1/2015', null
union all select null, '1/2/2015'
union all select '1/3/2015', '1/3/2015'
union all select '1/4/2015', '1/5/2015'
union all select '1/6/2015', '1/5/2015'
select * from #MyTable
select datevalues.id, min([date])
from #MyTable
cross apply
(
values (id, term_date), (id, suspend_date), (id, getdate())
) datevalues(id, [date])
group by datevalues.id

Query to merge continuous temporal records

I have a table like this:
id START_DATE end_date
1 01/01/2011 01/10/2011
2 01/11/2011 01/20/2011
3 01/25/2011 02/01/2011
4 02/10/2011 02/15/2011
5 02/16/2011 02/27/2011
I want to merge the records where the start_date is just next day of end_date of another record: So the end record should be something like this:
new_id START_DATE end_date
1 01/01/2011 01/20/2011
2 01/25/2011 02/01/2011
3 02/10/2011 02/27/2011
One way that I know to do this will be to create a row based temp table with various rows as dates (each record for one date, between the total range of days) and thus making the table flat.
But there has to be a cleaner way to do this in a single query... e.g. something using row_num?
Thanks guys.
declare #T table
(
id int,
start_date datetime,
end_date datetime
)
insert into #T values
(1, '01/01/2011', '01/10/2011'),
(2, '01/11/2011', '01/20/2011'),
(3, '01/25/2011', '02/01/2011'),
(4, '02/10/2011', '02/15/2011'),
(5, '02/16/2011', '02/27/2011')
select row_number() over(order by min(dt)) as new_id,
min(dt) as start_date,
max(dt) as end_date
from (
select dateadd(day, N.Number, start_date) as dt,
dateadd(day, N.Number - row_number() over(order by dateadd(day, N.Number, start_date)), start_date) as grp
from #T
inner join master..spt_values as N
on N.number between 0 and datediff(day, start_date, end_date) and
N.type = 'P'
) as T
group by grp
order by new_id
You can use a numbers table instead of using master..spt_values.
Try This
Declare #chgRecs Table
(updId int primary key not null,
delId int not null,
endt datetime not null)
While Exists (Select * from Table a
Where Exists
(Select * from table
Where start_date =
DateAdd(day, 1, a.End_Date)))
Begin
Insert #chgRecs (updId, delId , endt)
Select a.id, b.id, b.End_Date,
From table a
Where Exists
(Select * from table
Where start_date =
DateAdd(day, 1, a.End_Date)))
And Not Exists
(Select * from table
Where end_Date =
DateAdd(day, -1, a.Start_Date)))
Delete table Where id In (Select delId from #chgRecs )
Update table set
End_Date = u.endt
From table t join #chgRecs u
On u.updId = t.Id
Delete #delRecs
End
No, was not looking for a loop...
I guess this is a good solution:
taking all the data in a #temp table
SELECT * FROM #temp
SELECT t2.start_date , t1.end_date FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date)
UNION
SELECT START_DATE,end_date FROM #temp WHERE start_date NOT IN (SELECT t2.START_DATE FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date))
AND end_date NOT IN (SELECT t1.end_Date FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date))
DROP TABLE #temp
Please let me know if there is anything better than this.
Thanks guys.
A recursive solution:
CREATE TABLE TestData
(
Id INT PRIMARY KEY,
StartDate DATETIME NOT NULL,
EndDate DATETIME NOT NULL
);
SET DATEFORMAT MDY;
INSERT TestData
SELECT 1, '01/01/2011', '01/10/2011'
UNION ALL
SELECT 2, '01/11/2011', '01/20/2011'
UNION ALL
SELECT 3, '01/25/2011', '02/01/2011'
UNION ALL
SELECT 4, '02/10/2011', '02/15/2011'
UNION ALL
SELECT 5, '02/16/2011', '02/27/2011'
UNION ALL
SELECT 6, '02/28/2011', '03/06/2011'
UNION ALL
SELECT 7, '02/28/2011', '03/03/2011'
UNION ALL
SELECT 8, '03/10/2011', '03/18/2011'
UNION ALL
SELECT 9, '03/19/2011', '03/25/2011';
WITH RecursiveCTE
AS
(
SELECT t.Id, t.StartDate, t.EndDate
,1 AS GroupID
FROM TestData t
WHERE t.Id=1
UNION ALL
SELECT crt.Id, crt.StartDate, crt.EndDate
,CASE WHEN DATEDIFF(DAY,prev.EndDate,crt.StartDate)=1 THEN prev.GroupID ELSE prev.GroupID+1 END
FROM TestData crt
JOIN RecursiveCTE prev ON crt.Id-1=prev.Id
--WHERE crt.Id > 1
)
SELECT cte.GroupID, MIN(cte.StartDate) AS StartDate, MAX(cte.EndDate) AS EndDate
FROM RecursiveCTE cte
GROUP BY cte.GroupID
ORDER BY cte.GroupID;
DROP TABLE TestData;

How to delete when the parameter varies by group without looping? (T-SQL)

Imagine I have these columns in a table:
id int NOT NULL IDENTITY PRIMARY KEY,
instant datetime NOT NULL,
foreignId bigint NOT NULL
For each group (grouped by foreignId) I want to delete all the rows which are 1 hour older than the max(instant). Thus, for each group the parameter is different.
Is it possible without looping?
Yep, it's pretty straightforward. Try this:
DELETE mt
FROM MyTable AS mt
WHERE mt.instant <= DATEADD(hh, -1, (SELECT MAX(instant)
FROM MyTable
WHERE ForeignID = mt.ForeignID))
Or this:
;WITH MostRecentKeys
AS
(SELECT ForeignID, MAX(instant) AS LatestInstant
FROM MyTable)
DELETE mt
FROM MyTable AS mt
JOIN MostRecentKeys mrk ON mt.ForeignID = mrt.ForeignID
AND mt.Instant <= DATEADD(hh, -1, mrk.LatestInstant)
DELETE
FROM mytable
FROM mytable mto
WHERE instant <
(
SELECT DATEADD(hour, -1, MAX(instant))
FROM mytable mti
WHERE mti.foreignid = mto.foreignid
)
Note double FROM clause, it's on purpose, otherwise you won't be able to alias the table you're deleting from.
The sample data to check:
DECLARE #mytable TABLE
(
id INT NOT NULL PRIMARY KEY,
instant DATETIME NOT NULL,
foreignID INT NOT NULL
)
INSERT
INTO #mytable
SELECT 1, '2009-22-07 10:00:00', 1
UNION ALL
SELECT 2, '2009-22-07 09:30:00', 1
UNION ALL
SELECT 3, '2009-22-07 08:00:00', 1
UNION ALL
SELECT 4, '2009-22-07 10:00:00', 2
UNION ALL
SELECT 5, '2009-22-07 08:00:00', 2
UNION ALL
SELECT 6, '2009-22-07 07:30:00', 2
DELETE
FROM #mytable
FROM #mytable mto
WHERE instant <
(
SELECT DATEADD(hour, -1, MAX(instant))
FROM #mytable mti
WHERE mti.foreignid = mto.foreignid
)
SELECT *
FROM #mytable
1 2009-07-22 10:00:00.000 1
2 2009-07-22 09:30:00.000 1
4 2009-07-22 10:00:00.000 2
I'm going to assume when you say '1 hour older than the max(instant)' you mean '1 hour older than the max(instant) for that foreignId'.
Given that, there's almost certainly a more succinct way than this, but it will work:
DELETE
TableName
WHERE
DATEADD(hh, 1, instant) < (SELECT MAX(instant)
FROM TableName T2
WHERE T2.foreignId = TableName.foreignId)
The inner subquery is called a 'correlated subquery', if you want to look for more info. The way it works is that for each row under consideration by the outer query, it is the foreignId of that row that gets referenced by the subquery.