Related
I am trying to solve a "gaps and islands" by date issue I'm facing (kudos to Gordon Linoff helping me identify this issue). I want to group the below table by person, office and job while respecting order by person,from_date. consider the table below:
declare #temp table(person varchar(25),office varchar(25),job varchar(25),from_date date,to_date date)
insert into #temp values ('jon','ny','programmer','1/1/2020','1/3/2020');
insert into #temp values ('jon','ny','programmer','1/4/2020','1/5/2020');
insert into #temp values ('jon','dc','programmer','1/6/2020','1/7/2020');
insert into #temp values ('jon','ny','programmer','1/8/2020','1/9/2020');
insert into #temp values ('lou','ny','programmer','1/1/2020','1/3/2020');
insert into #temp values ('lou','ny','programmer','1/4/2020','1/5/2020');
insert into #temp values ('lou','dc','programmer','1/6/2020','1/7/2020');
insert into #temp values ('lou','ny','programmer','1/8/2020','1/9/2020');
the intended output is
This is a type of gaps-and-islands problem. If there are no gaps in the dates, the simplest solution is the difference of row numbers:
select person, office, job, min(from_date), max(to_date)
from (select t.*,
row_number() over (partition by person, office, job order by from_date) as seqnum,
row_number() over (partition by person, office order by from_date) as seqnum_2
from t
) t
group by person, office, job, (seqnum - seqnum_2)
This is a general solution:
WITH preceders_and_followers AS (
SELECT
b.person,
b.office,
b.job,
b.from_date,
b.to_date,
CASE
WHEN EXISTS (
SELECT
c.*
FROM
ora$ptt_tmp c
WHERE
b.person = c.person
AND b.office = c.office
AND b.job = c.job
AND ( b.from_date - 1 BETWEEN c.from_date AND c.to_date )
) THEN
1
END AS has_preceder,
CASE
WHEN EXISTS (
SELECT
c.*
FROM
ora$ptt_tmp c
WHERE
b.person = c.person
AND b.office = c.office
AND b.job = c.job
AND ( b.to_date + 1 BETWEEN c.from_date AND c.to_date )
) THEN
1
END AS has_follower
FROM
ora$ptt_tmp b
ORDER BY
1,
2,
3
)
SELECT DISTINCT
pf1.person,
pf1.office,
pf1.job,
pf1.from_date,
(
SELECT
MIN(pf2.to_date)
FROM
preceders_and_followers pf2
WHERE
pf1.person = pf2.person
AND pf1.office = pf2.office
AND pf1.job = pf2.job
AND pf2.to_date >= pf1.from_date
AND has_follower IS NULL
) to_date
FROM
preceders_and_followers pf1
WHERE
pf1.has_preceder IS NULL
ORDER BY
1,
4,
2,
3;
I want to update 2 columns Enr and Drop based on StartDate and EndDate for a set of ID values.
In attached screenshot, example for ID = 82, we have 2 rows so have to compare 2 rows. Enddate (12/13/2013) in first row with startDate (2/17/2014) in next row and if datediff(dy, Startdate, Enddate) > 30 I have to update the columns Drop in row1 to N and Enr in row2 to Y accordingly.
Perform the same comparison for all matching id's in the group. I want to iterate over each set of ID values and compare start and end dates and based on that update columns err and Drop.
Please provide inputs on best way to write query for this? Any help is greatly appreciated.
Thanks
Swetha
Try this by using CTE and OUTER APPLY combination as below:
DECLARE #table TABLE(id INT, StartDate DATE, EndDate DATE, Enr CHAR(1), [Drop] CHAR(1))
INSERT INTO #table VALUES
(82,'2010-12-14','2013-12-13','Y','N'),
(82,'2014-02-17','2016-12-21','N','Y'),
(125,'2010-12-22','2015-06-23','Y','N'),
(125,'2015-06-23','2015-06-30','N','N'),
(125,'2015-08-16',NULL,'N','N'),
(555,'2010-12-28','2017-03-31','Y','N'),
(555,'2017-03-31',NULL,'N','N')
;WITH cte AS(
SELECT t.*,
RANK() OVER(PARTITION BY id ORDER BY id, StartDate) rnk
FROM #table t
)
UPDATE ta SET ta.[Drop] = CASE WHEN t1.Diff>30 THEN 'Y' ELSE ta.[Drop] END,
ta.[Enr] = CASE WHEN t2.Diff>30 THEN 'Y' ELSE ta.[Enr] END
FROM cte t
INNER JOIN #table ta ON ta.id = t.id AND ta.StartDate = t.StartDate
OUTER APPLY(SELECT id, t1.StartDate, DATEDIFF(DAY,t.EndDate, t1.StartDate) Diff
FROM cte t1
WHERE t1.id = t.id
AND t1.rnk = t.rnk+1) t1
OUTER APPLY(SELECT id, t.StartDate, DATEDIFF(DAY,t2.EndDate, t.StartDate) Diff
FROM cte t2
WHERE t2.id = t.id
AND t2.rnk+1 = t.rnk) t2
OUTPUT:
id StartDate EndDate Enr Drop
82 2010-12-14 2013-12-13 Y Y
82 2014-02-17 2016-12-21 Y Y
125 2010-12-22 2015-06-23 Y N
125 2015-06-23 2015-06-30 N Y
125 2015-08-16 NULL Y N
555 2010-12-28 2017-03-31 Y N
555 2017-03-31 NULL N N
Note: Please confirm StartDate for id 125, that is different in Before and After update.
I hope it will resolve your problem
DECLARE #table TABLE(id INT, StartDate DATE, EndDate DATE, Enr CHAR(1), [Drop] CHAR(1))
INSERT INTO #table VALUES
(82,'2010-12-14','2013-12-13','Y','N'),
(82,'2014-02-17','2016-12-21','N','Y'),
(125,'2010-12-22','2015-06-23','Y','N'),
(125,'2015-06-23','2015-06-30','N','N'),
(125,'2015-08-16',NULL,'N','N'),
(555,'2010-12-28','2017-03-31','Y','N'),
(555,'2017-03-31',NULL,'N','N')
select * from #table
;WITH cte AS(
SELECT t.*,
RANK() OVER(PARTITION BY id ORDER BY id, StartDate) rnk,
lead(StartDate,1)OVER (PARTITION BY id ORDER BY id, StartDate) as NextStartDate,
Lag(enddate,1)OVER (PARTITION BY id ORDER BY id, StartDate) as PrevEndDate
FROM #table t
)
--select * from(
--select *,DATEDIFF(DD,PrevEndDate,StartDate) q,DATEDIFF(DD,EndDate,NextStartDate) r,
--case when (DATEDIFF(DD,PrevEndDate,StartDate) >30 and [Enr]='N') or [Enr]='Y' then 'Y' else 'N' end newEnr,
--case when (DATEDIFF(DD,EndDate,NextStartDate) >30 and [Drop]='N') or[Drop]='Y' then 'Y' else 'N' end newdrop
--from cte) as a
update t
set Enr=case when (DATEDIFF(DD,c.PrevEndDate,c.StartDate) >30 and c.[Enr]='N') or c.[Enr]='Y' then 'Y' else 'N' end,
t.[Drop]= case when (DATEDIFF(DD,c.EndDate,c.NextStartDate) >30 and c.[Drop]='N') or c.[Drop]='Y' then 'Y' else 'N' end
from #table t
inner join cte as c on t.id=c.id and t.StartDate=c.StartDate
select * from #table
I am sure this is a very stupid question and I am having a dumb moment.
Consider the following basic scenario (this is a very small scenario compared with reality which has many many dimensions and measures):
What I need to get to is the expected output.
So ALL costs between the input_Date and output_date defined in the params are included. However only the latest PID is included- defined as either:
1- where PIDs run sequentially, or overlap the latest one based on date_to as long as both aren't active at the # output date
2- where there are two PID active at the # output date show both
I can't for the life of me work out how to do this in SQL, note that is has to be non dynamic and not use any CTE unfortunately, just your basic SQL with subqueries
Obviously returning the necessary list of ID and PID is easy:
declare #input_date date ='2006-01-01'
declare #output_date date ='2006-12-31'
select a.PID, a.ID
from #tmp a
where date_from <=#output_date and date_to >=#input_date
But I can't figure out how to join this back to return the correct cost values
drop table tmp
CREATE TABLE [dbo].[tmp](
[date_from] [datetime] NOT NULL,
[date_to] [datetime] NOT NULL,
[ID] [nvarchar](25) NOT NULL,
[PID] [nvarchar](25) NOT NULL,
[cost] [float] NULL
) ON [PRIMARY]
INSERT tmp VALUES('2005-1-1','2005-1-31','10001','X123',1254.32)
INSERT tmp VALUES('2000-10-10','2006-8-21','10005','TEST01',21350.9636378758)
INSERT tmp VALUES('2006-8-22','2099-12-31','10005','TEST02',22593.4926163943)
INSERT tmp VALUES('2006-1-1','2099-12-31','10006','X01',22458.3342354444)
INSERT tmp VALUES('2006-2-8','2099-12-31','10006','X02',22480.3772331959)
INSERT tmp VALUES('2006-1-1','2006-2-7','10007','AB01',565.416874152212)
INSERT tmp VALUES('2006-2-8','2006-7-31','10007','AA05',19108.3206482165)
I've made some progress using a CTE so you can see how I would do it this way if I could:
drop table #tmp
CREATE TABLE #tmp (
[date_from] [datetime] NOT NULL,
[date_to] [datetime] NOT NULL,
[ID] [nvarchar](25) NOT NULL,
[PID] [nvarchar](25) NOT NULL,
[cost] [float] NULL
) ON [PRIMARY]
INSERT #tmp VALUES('2005-1-1','2005-1-31','10001','X123',1254.32)
INSERT #tmp VALUES('2000-10-10','2006-8-21','10005','TEST01',21350.9636378758)
INSERT #tmp VALUES('2006-8-22','2099-12-31','10005','TEST02',22593.4926163943)
INSERT #tmp VALUES('2006-1-1','2099-12-31','10006','X01',22458.3342354444)
INSERT #tmp VALUES('2006-2-8','2099-12-31','10006','X02',22480.3772331959)
INSERT #tmp VALUES('2006-1-1','2006-2-7','10007','AB01',565.416874152212)
INSERT #tmp VALUES('2006-2-8','2006-7-31','10007','AA05',19108.3206482165)
declare #input_date date ='2006-01-01'
declare #output_date date ='2006-12-31'
;with cte as (
select t.id,t.PID,t.cost,t.date_from,t.date_to ,
iif(date_To >= #output_date OR max_date_To is not null,PID,NULL) as PID2,
b.total_id_cost
from #tmp t
left join (select ID,max(date_to) as max_date_to
from #tmp
where date_from <=#output_date and date_to >=#input_date
group by ID) a
on t.ID = a.ID and t.date_to = a.max_date_to
left join (Select ID, sum(cost) as total_id_cost
from #tmp
where date_from <=#output_date and date_to >=#input_date
group by ID) b
on t.ID = b.ID
where date_from <=#output_date and date_to >=#input_date )
select distinct ID,PID2,
iif(ID in (
select ID
from cte
where PID2 IS NULL)
and ID not in (select ID
from cte
where PID IS NOT NULL
group by ID
having count (distinct PID2) >1 ), cte.total_id_cost, cost) as cost
from cte
where PID2 is not null;
so it looks like there's several problems to solve within 1 query.
We want the PID that matches the latest date. This wasn't too difficult and can be solved by joining the data with an aggregate of itself that finds the latest date
Where both PID is active i.e. overlapping from and to dates, then both must show. I found this to be more tricky. in the end I did a query to find the ones that do overlap and meet the dates, and did a count on that. then used this count as a criteria for the join on 1. so that it can conditionally pick the PID that matches the latest date
Then finally using the results from above, you can do the sum to get the cost. The resulting query is a bit of a monster, but here it is.
if it doesn't cover other scenarios not detailed, do let me know.
DECLARE #Data TABLE (date_from DATETIME, date_to DATETIME, ID INT, PID NVARCHAR(50), COST MONEY)
INSERT #Data VALUES('2005-1-1','2005-1-31','10001','X123',1254.32)
INSERT #Data VALUES('2000-10-10','2006-8-21','10005','TEST01',21350.9636378758)
INSERT #Data VALUES('2006-8-22','2099-12-31','10005','TEST02',22593.4926163943)
INSERT #Data VALUES('2006-1-1','2099-12-31','10006','X01',22458.3342354444)
INSERT #Data VALUES('2006-2-8','2099-12-31','10006','X02',22480.3772331959)
INSERT #Data VALUES('2006-1-1','2006-2-7','10007','AB01',565.416874152212)
INSERT #Data VALUES('2006-2-8','2006-7-31','10007','AA05',19108.3206482165)
declare #input_date date ='2006-01-01'
declare #output_date date ='2006-12-31'
select
a.ID,
PIDForMaxDateThatMatches.PID,
SUM(a.cost) as cost
from
#Data a
inner join (
-- number of PIDs for dates that overlap grouped by ID
select
a.ID,
-- where there's no overlap then we want the count to be 1 so that later we can use it as condition
COUNT(DISTINCT ISNULL(b.PID,'')) as NumberOfPID
from
#Data a
-- may or may not find overlaps
LEFT JOIN #data b ON
b.date_from <=#output_date and
b.date_to >=#input_date and
a.date_from <= b.date_to and
a.date_to >= b.date_from and
a.ID = b.ID and
a.PID <> b.PID
where
a.date_from <=#output_date and
a.date_to >=#input_date
group by
a.ID) as PIDCountForOverlappingMatches ON
a.ID = PIDCountForOverlappingMatches.ID
left join (
-- get the PID that matches the max date_to
select
DataForMaxDate.ID,
DataForMaxDate.date_from,
DataForMaxDate.date_to,
DataForMaxDate.PID
from
#Data as DataForMaxDate
inner join (
-- get the max date_to that matches the criteria
select
ID,
MAX(date_to) as maxDateTo
from
#Data a
where
date_from <=#output_date and
date_to >=#input_date
group by
ID) as MaxToDatePerID on
DataForMaxDate.ID = MaxToDatePerID.ID and
DataForMaxDate.date_to = MaxToDatePerID.maxDateTo) as PIDForMaxDateThatMatches on
a.ID = PIDForMaxDateThatMatches.ID AND
-- if there's no overlapping dates the PID count would be 1, which we'll take the PID that matches the max(date_to)
-- but if there is overlap, then we want both dates to show, thus the from date must also match before we take the PID
(PIDCountForOverlappingMatches.NumberOfPID = 1 OR a.date_from = PIDForMaxDateThatMatches.date_from)
where
a.date_from <= #output_date and
a.date_to >= #input_date
GROUP BY
a.ID,
PIDForMaxDateThatMatches.PID
ORDER BY
a.ID
EDIT: DB Fiddle http://dbfiddle.uk/?rdbms=sqlserver_2014&fiddle=d43cb4b9765da1bca035531e78a2c77d
Results:
ID PID cost
10005 TEST02 43944.4562
10006 X01 22458.3342
10006 X02 22480.3772
10007 AA05 19673.7375
Hello you can try the following query :
select a.resource_id ID, max(a.post_id) PID, SUM(a.cost) Cost
from #tmp a
where date_from <=#output_date and date_to >=#input_date
group by a.resource_id
order by a.resource_id;
I think this might work:
SELECT
t1.ID,
q1.PID,
SUM(t1.cost)
FROM
Table AS t1
JOIN
(
SELECT
q2.ID,
t2.PID
FROM
(
SELECT
ID,
MAX(date_to) AS maxdate
FROM
Table
GROUP BY
ID
) AS q2
JOIN
table AS t2
ON
q2.ID = t2.ID
AND
q2.maxdate = t2.date_to
) AS q1
ON
t1.ID = q1.ID
AND
t1.PID = q1.PID
GROUP BY
t1.ID,
q1.PID
Here is a query without CTE. Idea of query:
1) Find consecutive dates and make different groups within each id
2) Find min and max date, sum of costs for each group
3) Limit by input parametres
declare #date_from date = '20060101'
declare #date_to date = '20061231'
declare #myTable table(
date_from date
, date_to date
, id int
, pid varchar(30)
, cost decimal(10,2)
)
insert into #myTable values
('20050101', '20050201', 10001, 'x123', 1254.32)
, ('20001010', '20060821', 10005, 'test01', 21350.96)
, ('20060822', '20991231', 10005, 'test02', 22593.49)
, ('20060101', '20991231', 10006, 'x01', 22548.33)
, ('20060208', '20991231', 10006, 'x02', 22480.38)
, ('20060101', '20060207', 10007, 'abo1', 565.42)
, ('20060208', '20060731', 10007, 'abo2', 19108.32)
select
date_from = min(date_from), date_to = max(date_to)
, id, pid = max(case when date_to = max_date_to then pid end)
, cost = sum(cost)
from (
select
a.date_from, a.date_to, a.id, a.pid, a.cost, a.rn, grp = sum(b.ss)
, max_date_to = max(a.date_to) over (partition by a.id, sum(b.ss))
from
(
select
a.*, ss = case when datediff(dd, b.date_to, a.date_from) = 1 then 0 else 1 end
from
(
select
*, rn = row_number() over (partition by id order by date_from)
from
#myTable
) a
left join (
select
*, rn = row_number() over (partition by id order by date_from)
from
#myTable
) b on a.id = b.id and a.rn - 1 = b.rn
) a
left join (
select
a.*, ss = case when datediff(dd, b.date_to, a.date_from) = 1 then 0 else 1 end
from
(
select
*, rn = row_number() over (partition by id order by date_from)
from
#myTable
) a
left join (
select
*, rn = row_number() over (partition by id order by date_from)
from
#myTable
) b on a.id = b.id and a.rn - 1 = b.rn
) b on a.id = b.id and a.rn >= b.rn
group by a.date_from, a.date_to, a.id, a.pid, a.cost, a.rn
) t
group by id, grp, max_date_to
having min(date_from) <= #date_from and max(date_to) >= #date_to
order by id
Output
date_from date_to id pid cost
------------------------------------------------
2000-10-10 2099-12-31 10005 test02 43944.45
2006-01-01 2099-12-31 10006 x01 22548.33
Result is a bit different than your provided output. But:
1) For id = 10006 and pid = X02 date_from = 08/02/2006 while input is 01/01/2006
2) For id = 10007 date_to = 31/07/2006 while input is 31/12/2006
So, I think query works correctly
Rextester demo in more readable format with cte
I have a table like below.
I need to get the data like below.
I have created two temp tables and achieved the result like this. Please help me to do the same with PIVOT.
At least I wouldn't use pivot for that, to my mind this is simpler to do with group by and row_number:
select UserId, max(starttime) as starttime, max(endtime) as endtime
from (
select UserId,
case when StartOrEnd = 'S' then time end as starttime,
case when StartOrEnd = 'E' then time end as endtime,
row_number() over (partition by UserID order by time asc)
+ case when StartOrEnd = 'S' then 1 else 0 end as GRP
from table1
) X
group by UserId, GRP
order by starttime
The derived table splits the time into start / end time columns (to handle cases where only one exists) and uses a trick with row number to group the S / E items together. The outer select just groups the rows into the same row.
Example in SQL Fiddle
Not a efficient solution as JamesZ but should work
create table #tst (userid int,start_end char(1),times datetime)
insert #tst values
(1,'S','07-27-2015 16:45'),
(1,'E','07-27-2015 16:46'),
(2,'S','07-27-2015 16:47'),
(2,'E','07-27-2015 16:48'),
(1,'S','07-27-2015 16:49'),
(1,'E','07-27-2015 16:50')
WITH cte
AS (SELECT Row_number()OVER(ORDER BY times) rn,*
FROM #tst),
cte1
AS (SELECT a.userid,
a.start_end,
a.times,
CASE WHEN a.userid = b.userid THEN 0 ELSE 1 END AS com,
a.rn
FROM cte a
LEFT OUTER JOIN cte b
ON a.rn = b.rn + 1),
cte2
AS (SELECT userid,
start_end,
times,
(SELECT Sum(com)
FROM cte1 b
WHERE b.rn <= a.rn) AS row_num
FROM cte1 a)
SELECT USERID,
starttime=Min(CASE WHEN start_end = 's' THEN times END),
endtime=Max(CASE WHEN start_end = 'e' THEN times END)
FROM cte2
GROUP BY USERID,
row_num
Here is another method
declare #t table(userid int, StartOrEnd char(1), time datetime)
insert into #t
select 1,'S','2015-07-27 16:45' union all
select 1,'E','2015-07-27 16:46' union all
select 2,'S','2015-07-27 16:47' union all
select 2,'E','2015-07-27 16:48' union all
select 1,'S','2015-07-27 16:49' union all
select 1,'E','2015-07-27 16:50'
select userid,min(time) as minimum_time, max(time) as maximum_time from
(
select *, row_number() over (partition by cast(UserID as varchar(10))
+StartOrEnd order by time asc) as sno
from #t
) as t
group by userid,sno
Result
userid minimum_time maximum_time
----------- ----------------------- -----------------------
1 2015-07-27 16:45:00.000 2015-07-27 16:46:00.000
2 2015-07-27 16:47:00.000 2015-07-27 16:48:00.000
1 2015-07-27 16:49:00.000 2015-07-27 16:50:00.000
I have a table example like this:
date id status
01/01/2013 55555 high
01/01/2014 55555 low
01/01/2010 44444 high
01/01/2011 33333 low
I need in order: group by id and select most recent date.
this is the result I want.
date id status
01/01/2014 55555 low
01/01/2010 44444 high
01/01/2011 33333 low
I do not care the order of the rows.
you need to join your table with a subquery that "links" the record date with the greatest date for each id:
select a.*
from your_table as a
inner join (
select id, max(date) as max_date
from your_table
group by id
) as b on a.id = b.id and a.date = b.max_date;
I think you will need a subquery to get the MAX(Date) and then inner join. Try this:
SELECT A.[Date], A.[Id], A.[Status]
FROM Table A
INNER JOIN(SELECT Id, MAX([Date]) AS MaxDate
FROM Table
GROUP BY [Id]) B ON
A.[Id] = B.[Id] AND
A.[Date] = B.[MaxDate]
--return the group id and the latest date in that group
select id
, MAX([date]) [latestDateInGroup]
from tbl
group by id
--return the group id, and the related status and date for the record with the latest date in that group
select id
, [status] [latestDateInGroup'sStatus]
, [date] [latestDateInGroup]
from
(
select id
, [status]
, [date]
, row_number() over (partition by id order by [date] desc) r
from tbl
) x
where x.r = 1
--return all ids and statuses, along with the latest date in that group's group (requires SQL 2012+)
select id
, [status]
, max([date]) over (partition by id order by [date] desc) [latestDateInGroup]
from tbl
SQL Fiddle's offline at the moment; once back up the following code should allow you to build a table to test the above queries with
http://sqlfiddle.com
create table tbl ([date] date, id bigint, [status] nvarchar(4))
go
insert tbl select '2013-01-01', 55555, 'high'
insert tbl select '2014-01-01', 55555, 'low'
insert tbl select '2010-01-01', 44444, 'high'
insert tbl select '2011-01-01', 33333, 'low'