Oracle SELECT query: collapsing null values when pairing up dates - oracle

I have the following Oracle query:
SELECT id,
DECODE(state, 'Open', state_in, NULL) AS open_in,
DECODE(state, 'Not Open', state_in, NULL) AS open_out,
FROM (
SELECT id,
CASE WHEN state = 'Open'
THEN 'Open'
ELSE 'Not Open'
END AS state,
TRUNC(state_time) AS state_in
FROM ...
)
This gives me data like the following:
id open_in open_out
1 2009-03-02 00:00:00
1 2009-03-05 00:00:00
1 2009-03-11 00:00:00
1 2009-03-26 00:00:00
1 2009-03-24 00:00:00
1 2009-04-13 00:00:00
What I would like is data like this:
id open_in open_out
1 2009-03-02 00:00:00 2009-03-05 00:00:00
1 2009-03-11 00:00:00 2009-03-24 00:00:00
That is, keep all the unique pairs of id/open_in and pair with them the earliest open_out that follows open_in. There can be any number of unique open_in values for a given id, and any number of unique open_out values. It is possible that a unique id/open_in will not have a matching open_out value, in which case open_out should be null for that row.
I feel like some analytic function, maybe LAG or LEAD, would be useful here. Perhaps I need MIN used with a PARTITION.

It can be done a little bit simpler. First let's create a sample table:
SQL> create table mytable (id,state,state_time)
2 as
3 select 1, 'Open', date '2009-03-02' from dual union all
4 select 1, 'Closed', date '2009-03-05' from dual union all
5 select 1, 'Open', date '2009-03-11' from dual union all
6 select 1, 'Shut down', date '2009-03-26' from dual union all
7 select 1, 'Wiped out', date '2009-03-24' from dual union all
8 select 1, 'Demolished', date '2009-04-13' from dual
9 /
Table created.
The data equals the output of your select statement:
SQL> SELECT id,
2 DECODE(state, 'Open', state_in, NULL) AS open_in,
3 DECODE(state, 'Not Open', state_in, NULL) AS open_out
4 FROM (
5 SELECT id,
6 CASE WHEN state = 'Open'
7 THEN 'Open'
8 ELSE 'Not Open'
9 END AS state,
10 TRUNC(state_time) AS state_in
11 FROM mytable
12 )
13 /
ID OPEN_IN OPEN_OUT
---------- ------------------- -------------------
1 02-03-2009 00:00:00
1 05-03-2009 00:00:00
1 11-03-2009 00:00:00
1 26-03-2009 00:00:00
1 24-03-2009 00:00:00
1 13-04-2009 00:00:00
6 rows selected.
And here is the slightly easier query:
SQL> select id
2 , min(case when state = 'Open' then state_time end) open_in
3 , min(case when state != 'Open' then state_time end) open_out
4 from ( select id
5 , state
6 , state_time
7 , max(x) over (partition by id order by state_time) grp
8 from ( select id
9 , state
10 , state_time
11 , case state
12 when 'Open' then
13 row_number() over (partition by id order by state_time)
14 end x
15 from mytable
16 )
17 )
18 group by id
19 , grp
20 order by id
21 , open_in
22 /
ID OPEN_IN OPEN_OUT
---------- ------------------- -------------------
1 02-03-2009 00:00:00 05-03-2009 00:00:00
1 11-03-2009 00:00:00 24-03-2009 00:00:00
2 rows selected.
Regards,
Rob.

I think Stack Overflow must be inspirational, or at least it helps me think clearer. After struggling with this thing all day, I finally got it:
SELECT id,
open_in,
open_out
FROM (
SELECT id,
open_in,
LAG(open_out, times_opened) OVER (PARTITION BY id
ORDER BY open_out DESC
NULLS LAST) AS open_out
FROM (
SELECT id,
open_in,
open_out,
COUNT(DISTINCT open_in) OVER (PARTITION BY id)
AS times_opened
FROM (
SELECT id,
DECODE(state, 'Open', state_in, NULL) AS open_in,
DECODE(state, 'Not Open', state_in, NULL)
AS open_out
FROM (
SELECT id,
CASE WHEN state = 'Open'
THEN 'Open'
ELSE 'Not Open'
END AS state,
TRUNC(au_time) AS state_in
FROM ...
)
)
)
)
WHERE open_in IS NOT NULL
Update: looks like this doesn't completely work. It works fine with the example in my question, but when there are multiple unique id's, the LAG stuff gets shifted and dates don't always align. :(

Related

Oracle query to keep looking until value is not 0 anymore

I am using Oracle 11.
I have 2 tables
TblA with columns id, entity_id and effective_date.
TblADetail with columns id and value.
If Value = 0 for the effective date, I want to keep looking for the next effective date until I found value <> 0 anymore.
The below query only look for value on 3/10/21.
If value = 0, I want to look for value on 3/11/21. If that's not 0, I want to stop.
But, if that's 0, I want to look for value on 3/12/21. If that's not 0, I want to stop.
But, if that's 0, I want to keep looking until value is not 0.
How can I do that ?
SELECT SUM(pd.VALUE)
FROM TblA p,TblADetail pd
WHERE p.id = pd.id
AND p.effective_date = to_date('03/10/2021','MM/DD/YYYY')
AND TRIM (p.entity_id) = 123
Sample data:
TblA
id entity_id effective_date
1 123 3/10/21
2 123 3/11/21
3 123 3/12/21
TblADetail
id value
1 -136
1 136
2 2000
3 3000
In the above data, for entity_id 123, starting from effective_date 3/10/21, I would like to to return value 2000 (from TblADetail) effective_date 3/11/21.
So, starting from a certain date, I want the results from the minimum date that has non-zero values.
Thank you.
You can do what you need to do by grouping the sum on the effective date, and using the MIN analytic function to find the earliest date. Once you've done that, you simply need to select the date that matches the earliest date.
E.g.:
with tbla as (select 1 id, ' 123' entity_id, to_date('10/03/2021', 'dd/mm/yyyy') effective_date from dual union all
select 2 id, ' 123' entity_id, to_date('11/03/2021', 'dd/mm/yyyy') effective_date from dual union all
select 3 id, ' 123' entity_id, to_date('12/03/2021', 'dd/mm/yyyy') effective_date from dual),
tbla_detail as (select 1 id, -136 value from dual union all
select 1 id, 136 value from dual union all
select 2 id, 2000 value from dual union all
select 3 id, 3000 value from dual),
results as (select a.effective_date,
sum(ad.value) sum_value,
min(case when sum(ad.value) != 0 then a.effective_date end) over () min_effective_date
from tbla a
inner join tbla_detail ad on a.id = ad.id
where a.effective_date >= to_date('10/03/2021', 'dd/mm/yyyy')
and trim(a.entity_id) = '123'
group by a.effective_date)
select sum_value
from results
where effective_date = min_effective_date;
SUM_VALUE
----------
2000
Straightforward; read comments within code. Sample data in lines #1 - 13, query begins at line #14.
SQL> with
2 -- sample data
3 tbla (id, entity_id, effective_date) as
4 (select 1, 123, date '2021-03-10' from dual union all
5 select 2, 123, date '2021-03-11' from dual union all
6 select 3, 123, date '2021-03-12' from dual
7 ),
8 tblb (id, value) as
9 (select 1, -136 from dual union all
10 select 1, 136 from dual union all
11 select 2, 2000 from dual union all
12 select 3, 3000 from dual
13 ),
14 tblb_temp as
15 -- simple grouping per ID
16 (select id, sum(value) value
17 from tblb
18 group by id
19 )
20 -- return TBLA values whose ID equals TBLB_TEMP's minimum ID
21 -- whose value isn't zero
22 select a.id, a.entity_id, a.effective_date
23 from tbla a
24 where a.id = (select min(b.id)
25 from tblb_temp b
26 where b.value > 0
27 );
ID ENTITY_ID EFFECTIVE_
---------- ---------- ----------
2 123 03/11/2021
SQL>

Oracle Query to get max of date_column and if null present in that column, then that row should be returned

I had a situation like to read max(end_time_) when delete_reason_ != 'deleted', but when end_time_ has null, the query should return 2nd row only.
SELECT MAX(END_TIME_) FROM TASK_HISTORY WHERE DELETE_REASON_ != 'deleted'
is returning me 1st Row. But, my desired result should return 2nd row. As per Oracle documentation, Aggregate functions like max, sum, min shouldn't consider null values unlike Count.
Is there a way to get the Null value as max if null present, otherwise, max(end_time_) should be my desired output.
Any help will be appreciated.
thank you.
If I understood you correctly, here's one option:
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> with test (end_time_, delete_reason_) as
2 (select date '2017-12-13', 'completed' from dual union
3 select null, null from dual union
4 select date '2017-12-12', 'deleted' from dual union
5 select date '2018-01-05', 'unknown' from dual
6 ),
7 inter as
8 (select row_number() over (order by end_time_ desc) rn,
9 end_time_, delete_reason_
10 from test
11 where nvl(delete_reason_, 'x') <> 'deleted'
12 )
13 select end_time_ From inter
14 where rn = 1;
END_TIME_
----------
NULL
SQL>
SQL> with test (end_time_, delete_reason_) as
2 (select date '2017-12-13', 'completed' from dual union
3 -- select null, null from dual union
4 select date '2017-12-12', 'deleted' from dual union
5 select date '2018-01-05', 'unknown' from dual
6 ),
7 inter as
8 (select row_number() over (order by end_time_ desc) rn,
9 end_time_, delete_reason_
10 from test
11 where nvl(delete_reason_, 'x') <> 'deleted'
12 )
13 select end_time_ From inter
14 where rn = 1;
END_TIME_
----------
05.01.2018
SQL>
Finally I got a query something like this to achieve null value as result instead of date when I use max() function.
SELECT task_def_key_
, CASE WHEN MAX(CASE WHEN end_time_ IS NULL THEN 1 ELSE 0 END) = 0 THEN MAX(end_time_) END
FROM TASK_HISTORY WHERE DELETE_REASON_ != 'deleted'
GROUP BY task_def_key_
select * from (select * from TASK_HISTORY where DELETE_REASON_ != 'deleted' order by end_time desc) where rownum=1
This will work for your problem

get particular day between a data range from giving table

I read many articles which introduce the using of connect by, but all of them get the particular days from a giving parameter(almost the begin date and end date)
What I want to know is how could I get split the rows from a certain table?
Example
Table T1
StartDate EndDate T_ID
2017-06-01 2017-06-15 01
2017-06-05 2017-06-06 02
The result I want is
TargetDate T_ID
2017-06-01 01
2017-06-02 01
2017-06-03 01
2017-06-04 01
2017-06-05 01
.
.
.
.
2017-06-15 01
2017-06-06 01
2017-06-06 02
I tried
SELECT T_ID, T1.StartDate+ LEVEL - 1 DD, LEVEL
FROM T1
WHERE T1.T_ID in = '01'
CONNECT BY LEVEL <= (TO_DATE(TRUNC(T1.EndDate)) - T1.StartDate + 1 ) ;
Waiting for your solution. Thanks.
Test Data:
CREATE TABLE t1 ( t_id, startdate, enddate ) AS
SELECT 1, DATE '2017-06-01', DATE '2017-06-15' FROM DUAL UNION ALL
SELECT 2, DATE '2017-06-05', DATE '2017-06-06' FROM DUAL;
Query:
SELECT T_ID,
COLUMN_VALUE AS dt,
ROW_NUMBER() OVER ( PARTITION BY t1.ROWID
ORDER BY Column_value ) AS lvl
FROM T1
CROSS JOIN
TABLE(
CAST(
MUTLTSET(
SELECT t1.Startdate + LEVEL - 1
FROM DUAL
CONNECT BY t1.Startdate + LEVEL - 1 <= t1.EndDate
) AS SYS.ODCIDATELIST
)
);
Output:
T_ID DT LVL
---- ---------- ---
1 2017-06-01 1
1 2017-06-02 2
1 2017-06-03 3
1 2017-06-04 4
1 2017-06-05 5
1 2017-06-06 6
1 2017-06-07 7
1 2017-06-08 8
1 2017-06-09 9
1 2017-06-10 10
1 2017-06-11 11
1 2017-06-12 12
1 2017-06-13 13
1 2017-06-14 14
1 2017-06-15 15
2 2017-06-05 1
2 2017-06-06 2
Here is the query in standard SQL (with a recursive cte) which also works in Oracle:
with all_dates(targetdate, t_id, enddate) as
(
select startdate as targetdate, t_id, enddate from t1
union all
select targetdate + 1, t_id, enddate from all_dates where targetdate < enddate
)
select targetdate, t_id
from all_dates
order by t_id, targetdate;
SELECT DISTINCT T_ID
, T1.StartDate+ LEVEL - 1 DD
, LEVEL
FROM T1
WHERE T1.T_ID IN( 1,2)
CONNECT BY LEVEL <= T1.EndDate - T1.StartDate + 1
But I'm not sure about performances (At moment I didn't find a way to limit without DISTINCT but using CONNECT BY clauses).
As an alternative you can use a CTE like this (you can remove RN column, I left it as a check):
with all_dates(targetdate, t_id, enddate, RN) as
(
select startdate as targetdate, t_id, enddate, 1 AS RN from t1
union all
select T1.startdate + all_dates.RN, T1.t_id, T1.enddate, all_dates.RN+1 AS RN
from t1
inner JOIN all_dates ON T1.startdate+all_dates.RN<=all_dates.enddate
AND T1.T_ID = all_dates.T_ID
)
select targetdate, t_id , RN
from all_dates
order by t_id, targetdate;
Sample data:
CREATE TABLE T1 (StartDate DATE, EndDate DATE, T_ID NUMBER(10,0));
INSERT INTO T1 VALUES ('20170601','20170615', 1);
INSERT INTO T1 VALUES ('20170605','20170606', 2);
INSERT INTO T1 VALUES ('20170701','20170703', 3);
Output:
20170601 1 1
20170602 1 2
20170603 1 3
20170604 1 4
20170605 1 5
20170606 1 6
20170607 1 7
20170608 1 8
20170609 1 9
20170610 1 10
20170611 1 11
20170612 1 12
20170613 1 13
20170614 1 14
20170615 1 15
20170605 2 1
20170606 2 2
20170701 3 1
20170702 3 2
20170703 3 3
If you're wanting to use connect by to achieve this, you will need to add a couple of additional clauses in order to get it to work with multiple rows:
WITH t1 AS (SELECT to_date('01/06/2017', 'dd/mm/yyyy') startdate, to_date('15/06/2017', 'dd/mm/yyyy') enddate, 1 t_id FROM dual UNION ALL
SELECT to_date('05/06/2017', 'dd/mm/yyyy') startdate, to_date('06/06/2017', 'dd/mm/yyyy') enddate, 2 t_id FROM dual)
SELECT t_id,
startdate + LEVEL -1 dd
FROM t1
CONNECT BY LEVEL <= enddate - startdate + 1
AND PRIOR t_id = t_id
AND PRIOR sys_guid() IS NOT NULL
ORDER BY t_id, dd;
T_ID DD
---------- -----------
1 01/06/2017
1 02/06/2017
1 03/06/2017
1 04/06/2017
1 05/06/2017
1 06/06/2017
1 07/06/2017
1 08/06/2017
1 09/06/2017
1 10/06/2017
1 11/06/2017
1 12/06/2017
1 13/06/2017
1 14/06/2017
1 15/06/2017
2 05/06/2017
2 06/06/2017

Calculating time spans in oracle

I need to calclulate time span of employees in oracle database.
I need to implement a logic as follows:
Case 1
Jan 1 - Jan 5
Jan 6 - Jan 10
AS the time interval between Jan 5 and Jan 6 is only one day so, the overall output
should be
Jan 1 - Jan 10
Case 2
Jan 1 - Jan 5
Jan 7 - Jan 10
AS the time interval between Jan 5 and Jan 7 is more than one day so, the overall output
should be
Jan 1 - Jan 5
Jan 7 - Jan 10
There can be any number of rows for each employee. I know it can be done
by use of lead/lag functions, but could not get it solved. Can anyone help me??
The sample data I used is as follows:
empid FROMDATE TODATE
===== ======== ======
1 01.01.2013 03.01.2013
1 02.01.2013 05.01.2013
2 01.01.2013 04.01.2013
2 02.01.2013 03.01.2013
2 02.01.2013 06.01.2013
3 01.01.2013 02.01.2013
3 04.01.2013 06.01.2013
3 01.01.2013 04.01.2013
4 01.01.2013 03.01.2013
4 04.01.2013 06.01.2013
5 01.01.2013 06.01.2013
5 01.01.2013 02.01.2013
5 02.01.2013 05.01.2013
5 03.01.2013 04.01.2013
6 01.01.2013 02.01.2013
6 02.01.2013 03.01.2013
6 05.01.2013 06.01.2013
6 05.01.2013 07.01.2013
In case of empid 1-5 the min from date and max to date is gives me the solution, i am stuck up on the cases of empid 6 where the gap is more than 1 day.
The date overlaps, particularly that some date ranges are entirely within others, makes this complicated as Jeffrey Kemp noted. The simplest way to deal with that might be to explode all the ranges into all their individual days, and then combine them back into distinct ranges. One way to explode them, if you have 11gR2, is with recursive subquery factoring (CTE):
with r (empid, onedate, todate) as (
select empid, fromdate, todate
from t42
union all
select empid, onedate + 1, todate
from r
where onedate < todate
)
...
This generates all the dates for all the employees; but it has duplicates because of the overlaps, so you can eliminate those:
...,
s as (
select distinct empid, onedate
from r
)
...
Then you're back to using lead and lag to spot the contiguous ranges. This can be compressed a bit but I've left it like this so it's easier (I hope) to follow the logic). First find the previous and next date for the employee:
...,
t as (
select empid, onedate,
lag(onedate) over (partition by empid order by onedate) as lagdate,
lead(onedate) over (partition by empid order by onedate) as leaddate
from s
)
...
And effectively blank out that are mid-range:
...,
u as (
select empid, onedate, lagdate, leaddate,
case when lagdate is null or lagdate < onedate - 1 then onedate end
as fromdate,
case when leaddate is null or leaddate > onedate + 1 then onedate end
as todate
from t
)
...
And finally collapse the calculated rows you have left, using lead and lag again - which you can do because the 'from' and 'to' records are adjacent, if we eliminate all the mid-range values:
select distinct empid,
case when fromdate is null then lag(fromdate)
over (partition by empid order by onedate) else fromdate end as fromdate,
case when todate is null then lead(todate)
over (partition by empid order by onedate) else todate end as todate
from u
where fromdate is not null
or todate is not null
order by empid, fromdate;
So putting that all together:
with r (empid, onedate, todate) as (
select empid, fromdate, todate
from t42
union all
select empid, onedate + 1, todate
from r
where onedate < todate
),
s as (
select distinct empid, onedate
from r
),
t as (
select empid, onedate,
lag(onedate) over (partition by empid order by onedate) as lagdate,
lead(onedate) over (partition by empid order by onedate) as leaddate
from s
),
u as (
select empid, onedate, lagdate, leaddate,
case when lagdate is null or lagdate < onedate - 1 then onedate end
as fromdate,
case when leaddate is null or leaddate > onedate + 1 then onedate end
as todate
from t
)
select distinct empid,
case when fromdate is null then lag(fromdate)
over (partition by empid order by onedate) else fromdate end as fromdate,
case when todate is null then lead(todate)
over (partition by empid order by onedate) else todate end as todate
from u
where fromdate is not null
or todate is not null
order by empid, fromdate;
... gives:
EMPID FROMDATE TODATE
---------- ---------- ----------
1 2013-01-01 2013-01-05
2 2013-01-01 2013-01-06
3 2013-01-01 2013-01-06
4 2013-01-01 2013-01-06
5 2013-01-01 2013-01-06
6 2013-01-01 2013-01-03
6 2013-01-05 2013-01-07
7 rows selected
This works in 11.2.0.3, but the recursive CTE seems to give the wrong answer on SQL Fiddle, which is 11.2.0.2 - so not sure if that's seeing a bug. And you can't use it in previous versions anyway. Expanding ranges from multiple rows using connect by is tricky, and I'm trying to avoid a function, but you can do this instead:
with r as (
select mindate + level - 1 as onedate
from (
select min(fromdate) as mindate, max(todate) as maxdate
from t42
)
connect by level <= maxdate - mindate + 1
),
s as (
select distinct t.empid, r.onedate
from r
join t42 t on r.onedate between t.fromdate and t.todate
)
...
With the rest of the CTEs and the query above, that does work on SQL Fiddle, and produces the same output. And it'll work at least back to 10g. This Fiddle shows it broken down into the stages so you can see how the data is being manipulated at each point.

Oracle SELECT query: collapsing NULL values when pairing up dates for different fields

This question is very much like my previous question, but a bit more complicated. Rob van Wijk's answer worked perfectly for my other question, and I've been using that as a starting point. My problem now is that I am pivoting dates for different fields. Whereas before I cared about getting all open_in and open_out values for a given id, now I want new_in, new_out, open_in, open_out, fixed_in, and fixed_out for each id. I have the following:
SELECT id,
state,
state_time,
MAX(new_row_num) OVER (PARTITION BY id ORDER BY state_time) AS new_row_group,
MAX(open_row_num) OVER (PARTITION BY id ORDER BY state_time) AS open_row_group,
MAX(fixed_row_num) OVER (PARTITION BY id ORDER BY state_time) AS fixed_row_group
FROM (
SELECT id,
state,
state_time,
CASE state
WHEN 'New'
THEN ROW_NUMBER() OVER (PARTITION BY id ORDER BY state_time)
END AS new_row_num,
CASE state
WHEN 'Open'
THEN ROW_NUMBER() OVER (PARTITION BY id ORDER BY state_time)
END AS open_row_num,
CASE state
WHEN 'Fixed'
THEN ROW_NUMBER() OVER (PARTITION BY id ORDER BY state_time)
END AS fixed_row_num
FROM ...
)
This gives me data like the following:
id state state_time new_row_group open_row_group fixed_row_group
1 New 2009-03-03 00:03:31 1
1 Closed 2009-03-04 04:15:27 1
2 New 2010-05-22 14:38:49 1
2 Open 2010-05-22 14:39:14 1 2
2 Fixed 2010-05-22 17:15:27 1 2 3
I would like data like the following:
id new_in new_out open_in open_out fixed_in fixed_out
1 2009-03-03 00:03:31 2009-03-04 04:15:27
2 2010-05-22 14:38:49 2010-05-22 14:39:14 2010-05-22 14:39:14 2010-05-22 17:15:27 2010-05-22 17:15:27
How can I pivot the data to get this date-pairing for each id?
Edit: to clarify, an id can enter and leave a state multiple times. For example, an id might go from New to Open to Fixed to Open to Fixed to Closed. In that case, there would need to be as many rows as is necessary to hold all the state times, e.g.:
id new_in new_out open_in open_out fixed_in fixed_out
4 2009-01-01 00:00:00 2009-01-02 00:00:00 2009-01-02 00:00:00 2009-01-03 00:00:00 2009-01-03 00:00:00 2009-01-04 00:00:00
4 2009-01-04 00:00:00 2009-01-05 00:00:00 2009-01-05 00:00:00 2009-01-06 00:00:00
Sarah,
Here is an example with your sample data:
SQL> create table yourtable (id,state,state_time)
2 as
3 select 1, 'New', to_date('2009-03-03 00:03:31','yyyy-mm-dd hh24:mi:ss') from dual union all
4 select 1, 'Closed', to_date('2009-03-04 04:15:27','yyyy-mm-dd hh24:mi:ss') from dual union all
5 select 2, 'New', to_date('2010-05-22 14:38:49','yyyy-mm-dd hh24:mi:ss') from dual union all
6 select 2, 'Open', to_date('2010-05-22 14:39:14','yyyy-mm-dd hh24:mi:ss') from dual union all
7 select 2, 'Fixed', to_date('2010-05-22 17:15:27','yyyy-mm-dd hh24:mi:ss') from dual union all
8 select 3, 'New', date '2009-01-01' from dual union all
9 select 3, 'Open', date '2009-01-02' from dual union all
10 select 3, 'Fixed', date '2009-01-03' from dual union all
11 select 3, 'Open', date '2009-01-04' from dual union all
12 select 3, 'Fixed', date '2009-01-05' from dual union all
13 select 3, 'Closed', date '2009-01-06' from dual
14 /
Table created.
The query:
SQL> select id
2 , max(decode(state,'New',state_time)) new_in
3 , max(decode(state,'New',out_time)) new_out
4 , max(decode(state,'Open',state_time)) open_in
5 , max(decode(state,'Open',out_time)) open_out
6 , max(decode(state,'Fixed',state_time)) fixed_in
7 , max(decode(state,'Fixed',out_time)) fixed_out
8 from ( select id
9 , state
10 , state_time
11 , max(cnt) over (partition by id order by state_time) the_row
12 , lead(state_time) over (partition by id order by state_time) out_time
13 from ( select id
14 , state
15 , state_time
16 , count(*) over (partition by id,state order by state_time) cnt
17 from yourtable
18 )
19 )
20 group by id
21 , the_row
22 order by id
23 , the_row
24 /
ID NEW_IN NEW_OUT OPEN_IN OPEN_OUT FIXED_IN FIXED_OUT
---------- ------------------- ------------------- ------------------- ------------------- ------------------- -------------------
1 03-03-2009 00:03:31 04-03-2009 04:15:27
2 22-05-2010 14:38:49 22-05-2010 14:39:14 22-05-2010 14:39:14 22-05-2010 17:15:27 22-05-2010 17:15:27
3 01-01-2009 00:00:00 02-01-2009 00:00:00 02-01-2009 00:00:00 03-01-2009 00:00:00 03-01-2009 00:00:00 04-01-2009 00:00:00
3 04-01-2009 00:00:00 05-01-2009 00:00:00 05-01-2009 00:00:00 06-01-2009 00:00:00
4 rows selected.
To understand how it works, execute the query from the inside out and check the intermediate result sets. Please let me know if you need some additional explanation.
Regards,
Rob.
I'm not sure how you'd prefer to handle the situation where the same state is repeated more than once for an ID. The following answer takes the easy route, assuming that you would want the first time the state was set and the last time the state was replaced.
select id,
min(case state when 'New' then state_time else null end) as new_in,
max(case state when 'New' then out_state_time else null end) as new_out,
min(case state when 'Open' then state_time else null end) as open_in,
max(case state when 'Open' then out_state_time else null end) as open_out,
min(case state when 'Fixed' then state_time else null end) as fixed_in,
max(case state when 'Fixed' then out_state_time else null end) as fixed_out
from
(select id,
state,
state_time,
lead(state_time) over (partition by id
order by state_time) as out_state_time
from ...
)
group by id
The lead analytic function gets the next row described by the partition/order statement, so that's the easiest way to find out when the state changed. The middle query is a basic pivot query (transforming columns to rows).
select news.id, news.state_time as new_in, min(not_news.state_time) as new_out
, min(opens.state_time) as open_in
, min(not_opens.state_time) as open_out
, min(closes.state_time) as close_in
, min(not_closed.state_time) as close_out
from
(SELECT id,
state,
state_time
from mytable
where state = 'New' ) news
left join
(SELECT id,
state,
state_time
from mytable
where state <> 'New' ) not_news on news.id = not_news.id and news.state_time <= not_news.state_time
left join
(SELECT id,
state,
state_time
from mytable
where state = 'Open' ) opens on news.id = opens.id and news.state_time <= opens.state_time
left join
(SELECT id,
state,
state_time
from mytable
where state not in ('New', 'Open' )) not_opens on news.id = opens.id and news.state_time <= opens.state_time and opens.state_time <= not_opens.state_time
left join
(SELECT id,
state,
state_time
from mytable
where state = 'Closed' ) closes on news.id = closes.id and news.state_time <= closes.state_time
left join
(SELECT id,
state,
state_time
from mytable
where state not in ('Closed' )) not_closed on news.id = not_closed.id and news.state_time <= closes.state_time and closes.state_time <= not_closed.state_time
group by news.id, news.state_time
order by id, news.state_time
My test data (borrowed from Rob):
create table mytable (id,state,state_time)
as
select 1, 'New', to_date('2009-03-03 00:03:31','yyyy-mm-dd hh24:mi:ss') from dual union all
select 1, 'Closed', to_date('2009-03-04 04:15:27','yyyy-mm-dd hh24:mi:ss') from dual union all
select 2, 'New', to_date('2010-05-22 14:38:49','yyyy-mm-dd hh24:mi:ss') from dual union all
select 2, 'Open', to_date('2010-05-22 14:39:14','yyyy-mm-dd hh24:mi:ss') from dual union all
select 2, 'Fixed', to_date('2010-05-22 17:15:27','yyyy-mm-dd hh24:mi:ss') from dual union all
select 3, 'New', date '2009-01-01' from dual union all
select 3, 'Open', date '2009-01-02' from dual union all
select 3, 'Fixed', date '2009-01-03' from dual union all
select 3, 'Open', date '2009-01-04' from dual union all
select 3, 'Fixed', date '2009-01-05' from dual union all
select 3, 'Closed', date '2009-01-06' from dual
query results:
ID NEW_IN NEW_OUT OPEN_IN OPEN_OUT CLOSE_IN CLOSE_OUT
1 3/3/2009 12:03:31 3/4/2009 4:15:27 3/4/2009 4:15:27
2 5/22/2010 2:38:49 5/22/2010 2:39:14 5/22/2010 2:39:14 5/22/2010 5:15:27
3 1/1/2009 1/2/2009 1/2/2009 1/3/2009 1/6/2009
I hope you can read the above, I'm having trouble formatting it.

Resources