Overlap SQL with group by - sql

I have a question regarding overlap in SQL, I have the following structure and data from my database:
Table A (Id = uniqueidentifier)
| Name | StartDate | EndDate | DaysToReceive |
------------------------------------------------------
| A | 2019-08-26 | 2020-04-13 | 232 |
| A | 2019-12-15 | 2020-04-11 | 119 |
| A | 2020-03-06 | 2020-03-31 | 26 |
| B | 2020-01-07 | 2020-01-31 | 25 |
| B | 2020-02-11 | 2020-02-29 | 19 |
I need to get the days to receive, but if there is an overlap I need the difference between the min date and the max date otherwise I use the DaysToReceive Summed Column.
I'm trying to get the result to look like this:
| Name | DaysToReceive |
------------------------
A | 232
B | 44
I have managed to get this query but only works for overlap days.
select DATEDIFF(d, MIN(t1.dt),MAX(t1.enddt)) + 1 as DaysToReceive
from (
select distinct cp1.dt, min(cp2.dt) enddt
from ( select StartDate as dt, Id from TableA ) cp1,
( select EndDate as dt from TableA ) cp2
where cp2.dt > cp1.dt cp1.Id = cp2.Id
group by cp1.dt
) t1, TableA t2
where t2.StartDate between t1.dt and t1.enddt
group by t1.dt, t1.enddt
Thanks in advance.
Cheers

Check this
Select [name], Case when [InRange] = 1
then Max(DateDiff(dd, MinStartdate, MaxEnddate) + 1)
Else
Sum(DateDiff(dd, Startdate, Enddate) + 1)
End as [Days]
from
(
Select Distinct a.[name], StartDate, EndDate, MinStartdate, MaxEnddate,
Case when StartDate > MinStartdate and EndDate < MaxEnddate or
(StartDate = MinStartdate and EndDate = MaxEnddate) then
1 Else 0
End as [InRange]
from
(
SELECT [name],
Min(StartDate) AS MinStartdate, Max(EndDate) AS MaxEnddate
FROM A
Group By [Name]
) Q
inner join A a
on a.[name] = q.[name]
) QQ
and here is the fiddle

Related

Split a date range in SQL Server

I'm struggling with a solution for a problem but I couldn't find anything similar here.
I have a table "A" like:
+---------+------------+------------+-----------+
| user_id | from | to | attribute |
+---------+------------+------------+-----------+
| 1 | 2020-01-01 | 2020-12-31 | abc |
+---------+------------+------------+-----------+
and I get a table "B" like:
+---------+------------+------------+-----------+
| user_id | from | to | attribute |
+---------+------------+------------+-----------+
| 1 | 2020-03-01 | 2020-04-15 | def |
+---------+------------+------------+-----------+
And what I need is:
+---------+------------+------------+-----------+
| user_id | from | to | attribute |
+---------+------------+------------+-----------+
| 1 | 2020-01-01 | 2020-02-29 | abc |
| 1 | 2020-03-01 | 2020-04-15 | def |
| 1 | 2020-04-16 | 2020-12-31 | abc |
+---------+------------+------------+-----------+
I tried just using insert and update but I couldn't figure out how to simultaneously do both. Is there a much simpler way? I read about CTE, could this be an approach?
I'd be very thankful for your help!
Edit: more examples
TABLE A
| user_id | from | to | attribute |
+=========+============+============+===========+
| 1 | 2020-01-01 | 2020-12-31 | atr1 |
| 1 | 2021-01-01 | 2021-12-31 | atr2 |
| 2 | 2020-01-01 | 2021-06-15 | atr1 |
| 3 | 2020-01-01 | 2021-06-15 | atr3 |
TABLE B
| user_id | from | to | attribute |
+=========+============+============+===========+
| 1 | 2020-09-01 | 2021-02-15 | atr3 |
| 2 | 2020-04-15 | 2020-05-31 | atr2 |
| 3 | 2021-04-01 | 2022-01-01 | atr1 |
OUTPUT:
| user_id | from | to | attribute |
+=========+============+============+===========+
| 1 | 2020-01-01 | 2020-08-31 | atr1 |
| 1 | 2020-09-01 | 2021-02-15 | atr3 |
| 1 | 2021-02-16 | 2021-12-31 | atr2 |
| 2 | 2020-01-01 | 2020-04-14 | atr1 |
| 2 | 2020-04-15 | 2020-05-31 | atr2 |
| 2 | 2020-06-01 | 2021-06-15 | atr1 |
| 3 | 2020-01-01 | 2021-03-31 | atr3 |
| 3 | 2021-04-01 | 2022-01-01 | atr1 |
Initially I just asked to split the date range and make a new row because the new attribute of table B is between the one in table A. But it's only a part of the problem. Maybe it's more clear with the new dataset(?)
Sample data,
create table #TableA( userid int, fromdt date
,todt date, attribute varchar(10))
insert into #TableA (userid , fromdt , todt , attribute)
values
( 1 ,'2020-01-01','2020-12-31' , 'atr1' ),
( 1 ,'2021-01-01','2021-12-31' , 'atr2' ),
( 2 ,'2020-01-01','2021-06-15' , 'atr1' ),
( 3 ,'2020-01-01','2021-06-15' , 'atr3' )
create table #TableB( userid int,fromdt date
,todt date, attribute varchar(10))
insert into #TableB (userid,fromdt, todt, attribute)
values
( 1 ,'2020-09-01','2021-02-15' , 'atr3' ),
( 2 ,'2020-04-15','2020-05-31' , 'atr2' ),
( 3 ,'2021-04-01','2022-01-01' , 'atr1' )
;
The Script,
;WITH CTE
AS (
SELECT *
FROM #TableA
UNION ALL
SELECT *
FROM #TableB
)
,CTE2
AS (
SELECT userid
,min(fromdt) minfromdt
,max(todt) maxtodt
FROM CTE
GROUP BY userid
)
,CTE3
AS (
SELECT c.userid
,c.fromdt
,c.todt
,c.attribute
,LEAD(c.fromdt, 1) OVER (
PARTITION BY c.userid ORDER BY c.fromdt
) LeadFromdt
FROM CTE c
)
,CTE4
AS (
SELECT c3.userid
,c3.fromdt
,CASE
WHEN c3.todt > c3.LeadFromdt
THEN dateadd(day, - 1, c3.leadfromdt)
--when c3.todt<c3.LeadFromdt then dateadd(day,-1,c3.leadfromdt)
ELSE c3.todt
END AS Todt
,
--c3.todt as todt1,
c3.attribute
FROM CTE3 c3
)
,CTE5
AS (
SELECT userid
,fromdt
,todt
,attribute
FROM CTE4
UNION ALL
SELECT c2.userid
,dateadd(day, 1, c4.Todt) AS Fromdt
,maxtodt AS Todt
,c4.attribute
FROM CTE2 c2
CROSS APPLY (
SELECT TOP 1 c4.todt
,c4.attribute
FROM cte4 c4
WHERE c2.userid = c4.userid
ORDER BY c4.Todt DESC
) c4
WHERE c2.maxtodt > c4.Todt
)
SELECT *
FROM CTE5
ORDER BY userid
,fromdt
drop table #TableA, #TableB
Your output is wrong.
Also append other sample data in same example
where my script is not working.
The easiest way is to work with a calendar table. You can create one and reuse it later.
When you have one (here I called it "AllDates"), you can do something like this:
WITH cte
as
(
select ad.theDate,u.userid,isnull(b.attrib,a.attrib) as attrib,
ROW_NUMBER() over (PARTITION BY u.userid, isnull(b.attrib,a.attrib)ORDER BY ad.theDate)
- ROW_NUMBER() over (PARTITION BY u.userid ORDER BY ad.theDate) as grp
from AllDates ad
cross join (select userid from tableA union select userid from tableB) u
left join tableB b on ad.theDate between b.frm and b.toD and u.userid = b.userid
left join tableA a on ad.theDate between a.frm and a.toD and u.userid = a.userid
where b.frm is not null
or a.frm is not null
)
SELECT userid,attrib,min(theDate) as frmD, max(theDate) as toD
FROM cte
GROUP BY userid,attrib,grp
ORDER BY 1,3;
If I understand the request correctly the data from table A should be merged into table B to fill the gaps based on four scenarios, here is how I achieved it:
/*
Scenario 1 - Use dates from B as base to be filled in from A
- Start and end dates from B
*/
SELECT
B.UserId,
B.StartDate,
B.EndDate,
B.Attr
FROM #tmpB AS B
UNION
/*
Scenario 2 - Start date between start and end date of another record
- End date from B plus one day as start date
- End date from A as end date
*/
SELECT
B.UserId,
DATEADD(DD, 1, B.EndDate) AS StartDate,
A.EndDate,
A.Attr
FROM #tmpB AS B
JOIN #tmpA AS A ON
B.UserId = A.UserId
AND B.StartDate < A.StartDate
AND B.EndDate > A.StartDate
UNION
/*
Scenario 3 - End date between start and end date of another record or both dates between start and end date of another record
- Start date from A as start date
- Start date from B minus one as end date
*/
SELECT
B.UserId,
A.StartDate,
DATEADD(DD, -1, B.StartDate) AS EndDate,
A.Attr
FROM #tmpB AS B
JOIN #tmpA AS A ON
B.UserId = A.UserId
AND (B.StartDate < A.EndDate AND B.EndDate > A.EndDate
OR B.StartDate BETWEEN A.StartDate AND A.EndDate AND B.EndDate BETWEEN A.StartDate AND A.EndDate)
UNION
/*
Scenario 4 - Both dates between start and end date of another record
- End date from B minus one as start date
- End date from A as end date
*/
SELECT
B.UserId,
DATEADD(DD, -1, B.EndDate) AS StartDate,
A.EndDate,
A.Attr
FROM #tmpB AS B
JOIN #tmpA AS A ON
B.UserId = A.UserId
AND B.StartDate BETWEEN A.StartDate AND A.EndDate
AND B.EndDate BETWEEN A.StartDate AND A.EndDate

How to repeat dates for multiples values and fill missing dates in SQL

I have a table with date, ID and Values, which looks like this
I want to repeat the date range(start date, end date) for all IDs, and if the value is missing for any date enter 0.
How can I achieve this in SQL?
Thanks in Advance
Just use cross join to generate the rows and left join to bring in the existing values:
select i.id, t.date, coalesce(t.value, 0) as value
from (select distinct id from t) i cross join
(select distinct date from t) left join
t
on t.id = i.id and t.date = i.date;
You can use insert into or select into to put this data into another table.
I have tried below example in SQL Server.
First you need to have a date table with the list of dates.
Then, you need to have the combination of date, id values
Now, you need to fill the val if it exists for the date, otherwise, it will be 0.
DECLARE #FromDate DATETIME, #ToDate DATETIME;
SET #FromDate = '2020-02-01';
SET #ToDate = '2020-02-28';
DECLARE #DateTable TABLE(theDate DATE)
DECLARE #datasource TABLE(id int, dateVal DATE, Val INT)
-- all days in that period
INSERT INTO #DateTable
SELECT TOP (DATEDIFF(DAY, #FromDate, #ToDate)+1)
TheDate = CAST(DATEADD(DAY, number, #FromDate) AS DATE)
FROM [master].dbo.spt_values
WHERE [type] = N'P' ORDER BY number;
--
INSERT INTO #datasource
VALUES
(59895,'2020-02-01',5),(59895,'2020-02-03',7),
(59888,'2020-02-01',2),(59888,'2020-02-02',10)
;WITH CTE_idDates AS
(
SELECT theDate,id FROM #DateTable
CROSS JOIN
#datasource
)
SELECT theDate, id,
COALESCE((SELECT Val from #dataSource WHERE dateval=c.thedate and id=c.id),0)
FROM CTE_idDates AS c
Result set
+------------+-------+------------------+
| theDate | id | (No column name) |
+------------+-------+------------------+
| 2020-02-01 | 59895 | 5 |
| 2020-02-02 | 59895 | 0 |
| 2020-02-03 | 59895 | 7 |
| 2020-02-04 | 59895 | 0 |
| 2020-02-05 | 59895 | 0 |
| 2020-02-06 | 59895 | 0 |
.
.
.
| 2020-02-28 | 59895 | 0 |
| 2020-02-01 | 59888 | 2 |
| 2020-02-02 | 59888 | 10 |
.
.
.
| 2020-02-26 | 59888 | 0 |
| 2020-02-27 | 59888 | 0 |
| 2020-02-28 | 59888 | 0 |
+------------+-------+------------------+

SQL Server get ids in one table on Date criteria in table one and table two

I am having problems getting the ids in TABLE A that satisfies the following criteria (I have tried a lot of different things and looked at various SO answers but cannot make it work. I looked into using OVER(PARTITION BY TABLE_B.calendar)):
Open (TABLE_B) should be equal to 1 on the first calendarDay (TABLE_B) on or after 10 days after startDate(TABLE_A).
endDate (TABLE_A) should be equal to the day found in 1) (i.e. the calendarDay for the respective id that satisfies the criteria).
Sample data:
TABLE_A:
+----+------------+------------+
| id | startDate | endDate |
+----+------------+------------+
| 1 | 2011-02-14 | 2011-03-14 |
| 2 | 2012-12-19 | 2013-01-20 |
| 3 | 2014-12-19 | 2015-01-21 |
+----+------------+------------+
TABLE_B:
+-------------+------+
| calendarDay | open |
+-------------+------+
| 2011-03-14 | 1 |
| 2011-03-16 | 0 |
| 2013-01-20 | 1 |
| 2013-01-21 | 1 |
| 2015-01-21 | 0 |
| 2015-01-22 | 1 |
+-------------+------+
Desired result:
+----+------------+------------+
| id | startDate | endDate |
+----+------------+------------+
| 1 | 2011-02-14 | 2011-03-14 |
| 2 | 2012-12-19 | 2013-01-20 |
+----+------------+------------+
I think you want:
select a.*
from a cross apply
(select top (1) b.*
from b
where b.open = 1 and b.calendarDate >= dateadd(day, 10, a.startdate)
order by b.calendarDate asc
) b
where b.calendarDate = a.endDate
You could use a CTE to first get the first calendar day:
with cteId(n, id, [open])
as (
select ROW_NUMBER() over (partition by a.id order by b.calendarDay) n, a.id, b.[open]
from #TABLE_A a
inner join #TABLE_B b on b.calendarDay >= DATEADD(day, 10, a.startDate)
)
... then just join it with TABLE_A
select a.*
from #TABLE_A a
inner join cteId c on a.id = c.id
where c.n = 1 and c.[open] = 1
You can try this query.
Use Exists
select a.*
from TABLE_A as a
where exists(
SELECT 1
FROM TABLE_B b
where
a.startDate <= DateAdd(day, 10, b.calendarDay) and b.[open] = 1
)
and exists(
SELECT 1
FROM TABLE_B b
where
a.endDate = b.calendarDay and b.[open] = 1
)
sqlfiddle:http://sqlfiddle.com/#!18/320111/15
another way can try to use join
select a.*
from TABLE_A as a
INNER JOIN
(
SELECT b.*,DateAdd(day, 10, b.calendarDay) addDay
FROM TABLE_B b
where b.[open] = 1
) b on a.startDate <= addDay and a.endDate = b.calendarDay
sqlfiddle:http://sqlfiddle.com/#!18/320111/19
you could probably use Exists here to look for the matching value
Select *
From Table_A a
Where Exists (
Select 1
From Table_B b
Where b.[open] = 1
And b.calendarDay >= DateAdd(dd, 10, a.endDate)
And b.calendarDay = a.endDate)
)

Have a column with the lowest possible next value (self-joining a table)

I am looking for a way to get the lowest next value in a sequence. Basically, I have a dataset of Dates and I want it to return the next day unless it's the latest date in the database, then I want it to return this instead.
My current query looks like this and almost works - of course up to the point where I want the latest possible value instead of the next one:
SELECT
a.date,
a.key,
a.description,
b.date NextDate
FROM
my_table a
CROSS APPLY (SELECT TOP 1
b.date
FROM
my_table b
WHERE
a.key = b.key AND
a.date < b.date) b
Sample data:
+----------+-----+-------------+
| date | key | description |
+----------+-----+-------------+
| 20170101 | atx | xxx |
| 20161228 | hfn | xxx |
| 20161222 | ktn | xxx |
| 20161214 | yqe | xxx |
| 20161204 | olp | xxx |
| 20161122 | bux | xxx |
+----------+-----+-------------+
What the result should look like:
+----------+-----+-------------+----------+
| date | key | description | NextDate |
+----------+-----+-------------+----------+
| 20170101 | atx | xxx | 20170101 |
| 20161228 | hfn | xxx | 20170101 |
| 20161222 | ktn | xxx | 20161228 |
| 20161214 | yqe | xxx | 20161222 |
| 20161204 | olp | xxx | 20161214 |
| 20161122 | bux | xxx | 20161204 |
+----------+-----+-------------+----------+
You can use a case expression to do this.
SELECT
a.date,
a.key,
a.description,
case when date = max(a.date) over() then date
else (select min(date) from mytable b where a.date < b.date) end as NextDate
FROM
my_table a
You can use lag on date column
select t.*,
lag(date, 1, date) over (order by date desc) nextdate
from
(SELECT
a.date,
a.key,
a.description,
b.date NextDate
FROM
my_table a
CROSS APPLY (SELECT TOP 1
b.date
FROM
my_table b
WHERE
a.key = b.key AND
a.date < b.date) b) t
I believe you want:
select a.*,
coalesce(lead(date) over (order by date),
max(date) over ()
)
from my_table a;
If your table never has a missing date the following would work.
SELECT CONVERT(DATE,CONVERT(CHAR(10),a.date,120))
,a.key,
,a.description,
,CASE
WHEN (SELECT MAX(a.date) FROM my_table a) <> AsAtDateID
THEN DATEADD(DAY,1,CONVERT(DATE,CONVERT(CHAR(10),a.date,120)))--This could be a select statement
ELSE CONVERT(DATE,CONVERT(CHAR(10),a.date,120))
END
FROM my_table a
ORDER BY Date DESC
Alternatively if there are missing dates then you could use a SQL statement in the CASE to get the next highest date.
SELECT MIN(Date) FROM my_table WHERE Date > a.Date
Not the most performant code, but seeing as we are talking date tables it would work. I'm sure a CTE could be used to do this as well, if you need a bit more performance
Using SQL 2008 without LEAD & LAG etc...
Try this
;with cte as
(
SELECT [DATE] = Cast([date] AS DATE),
[key],
[description],
Lag([date])OVER(ORDER BY Cast([date] AS DATE) DESC) AS prev_date
FROM ( VALUES ('20170101','atx','xxx'),
('20161228','hfn','xxx'),
('20161222','ktn','xxx'),
('20161214','yqe','xxx'),
('20161204','olp','xxx'),
('20161122','bux','xxx')) tc ([date], [key], [description])
)
SELECT [date],
[Key],
[Description],
NextDate = Iif([date] < prev_date, prev_date, [date])
FROM cte
Result :
+------------+-----+-------------+------------+
| date | Key | Description | NextDate |
+------------+-----+-------------+------------+
| 2017-01-01 | atx | xxx | 2017-01-01 |
| 2016-12-28 | hfn | xxx | 2017-01-01 |
| 2016-12-22 | ktn | xxx | 2016-12-28 |
| 2016-12-14 | yqe | xxx | 2016-12-22 |
| 2016-12-04 | olp | xxx | 2016-12-14 |
| 2016-11-22 | bux | xxx | 2016-12-04 |
+------------+-----+-------------+------------+

SQL Query Startdate to Enddate

I have a table (eg. TableA) like this
| Name | Startdate | Enddate |
|----------------------------------------------|
| a | 2014-02-26 | 2014-02-28 |
| b | 2014-03-05 | 2014-03-06 |
If I want to below results. Please help how to do?
| Name | Date |
|---------------------------|
| a | 2014-02-26 |
| a | 2014-02-27 |
| a | 2014-02-28 |
| b | 2014-03-05 |
| b | 2014-03-06 |
Thank you.
In Oracle it's a little tricky:
SELECT distinct b.name, trim(regexp_substr(b.dates, '[^,]+', 1, LEVEL)) day
FROM
(
select a.name name,(select WM_CONCAT(a.startdate + rownum -1) from all_objects where rownum <= a.enddate - a.startdate + 1) dates
from TableA a
) b
CONNECT BY LEVEL <= length(b.dates) - length(REPLACE(b.dates, ',', ''))+1
order by 1,2;
The inner query generates days for each name as a comma-separated String. The outer simply splits the String in several rows.
In SQL Server it can be achieved by using CTE as below
;WITH cte(name, Startdate,Enddate)
AS
(
SELECT name, Startdate,Enddate FROM your_table
UNION ALL
SELECT name, DATEADD(dd, 1,Startdate),Enddate FROM CTE
WHERE Startdate < Enddate
)
SELECT * FROM cte ORDER BY name
Here is the code at SQL Fiddle
Here is a easy way to do this
select name, startdate from test_table
UNION ALL
select name, enddate from test_table
order by 2 desc