counting most recent consecutive rows with like data using tabibitosan - sql

My project is using an Oracle SQL database. I have a historical table that appends task status on a weekly basis, and am attempting to query the number of weeks a task that is currently off track has been off track. Here's an example excerpt from my source historical table:
ID WEEK ON_TRACK
1 1 N
1 2 Y
1 3 N
1 4 N
1 5 N
2 1 N
2 2 N
2 3 Y
2 4 Y
2 5 N
3 1 N
3 2 N
3 3 Y
3 4 Y
3 5 Y
I'm looking to return the count of consecutive "N" values in ON_TRACK starting backwards from the latest append. For the above example data, I'd like the query to return:
ID WKS_OFF_TRACK
1 3
2 1
3 0
I've done some research, and it looks like the Tabibitosan method is the most logical approach, and I've found ample examples to give the max consecutive values that match 1 criteria, but I'm having trouble tweaking to return the most recent consecutive values that match 2 criteria (ID and ON_TRACK).
Here's what I have so far
--this step creates a temp table with unique IDs for each weekly append to the historical table, and a 1 (if ON_TRACK = N) or 0 (if ON_TRACK = Y). This results in the expected info.
WITH HIST_TBL AS (
SELECT DISTINCT(ID),
CASE ON_TRACK
WHEN 'N' THEN 1
ELSE 0
END AS OFF_TRACK,
WEEK
FROM SOURCE_HISTORICAL_TBL
ORDER BY ID,WEEK DESC)
-- end of temp table
--this is where Im struggling I want one line per project number, and the sum of the latest string of 1s (weeks the task has been off track), until a 0 is reached.
SELECT ID,
SUM(OFF_TRACK) AS WKS_OFF_TRACK
FROM (SELECT WEEK,
ID,
OFF_TRACK,
ROW_NUMBER() OVER (ORDER BY WEEK DESC) - ROW_NUMBER() OVER
(PARTITION BY ID,OFF_TRACK ORDER BY WEEK DESC) GRP
FROM HIST_TBL)
GROUP BY ID, GRP
ORDER BY ID;
This code results in the a cumulative sum of all weeks each project has been off track, which for my example data would be:
ID WKS_OFF_TRACK
1 4
2 3
3 2
Any ideas where I'm going wrong?

Here is one method that assumes people were "on track" at some point in time:
select sht.id, count(*)
from SOURCE_HISTORICAL_TBL sht
where sht.week > (select max(sht2.week)
from SOURCE_HISTORICAL_TBL sht2
where sht2.id = sht.id and sht2.on_track = 'Y'
)
group by sht.id;
Otherwise, you need one more condition:
select sht.id, count(*)
from SOURCE_HISTORICAL_TBL sht
where sht.week > (select max(sht2.week)
from SOURCE_HISTORICAL_TBL sht2
where sht2.id = sht.id and sht2.on_track = 'Y'
) or
not exists (select 1
from SOURCE_HISTORICAL_TBL sht2
where sht2.id = sht.id and sht2.on_track = 'Y'
)
group by sht.id;
You can also phrase these as analytic functions:
select id,
sum(case when week > max_week_y or max_week_y is null then 1 else 0 end) as max_off_track
from (select sht.*,
max(case when on_track = 'Y' then week end) over (partition by id) as max_week_y
from SOURCE_HISTORICAL_TBL sht
) sht
group by id;
Note that this version will return 0s for people currently on track.

You can do it in a single table scan:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE SOURCE_HISTORICAL_TBL ( ID, WEEK, ON_TRACK ) AS
SELECT 1, 1, 'N' FROM DUAL UNION ALL
SELECT 1, 2, 'Y' FROM DUAL UNION ALL
SELECT 1, 3, 'N' FROM DUAL UNION ALL
SELECT 1, 4, 'N' FROM DUAL UNION ALL
SELECT 1, 5, 'N' FROM DUAL UNION ALL
SELECT 2, 1, 'N' FROM DUAL UNION ALL
SELECT 2, 2, 'N' FROM DUAL UNION ALL
SELECT 2, 3, 'Y' FROM DUAL UNION ALL
SELECT 2, 4, 'Y' FROM DUAL UNION ALL
SELECT 2, 5, 'N' FROM DUAL UNION ALL
SELECT 3, 1, 'N' FROM DUAL UNION ALL
SELECT 3, 2, 'N' FROM DUAL UNION ALL
SELECT 3, 3, 'Y' FROM DUAL UNION ALL
SELECT 3, 4, 'Y' FROM DUAL UNION ALL
SELECT 3, 5, 'Y' FROM DUAL UNION ALL
SELECT 4, 1, 'N' FROM DUAL UNION ALL
SELECT 5, 1, 'Y' FROM DUAL;
Query 1:
SELECT ID,
GREATEST(
COALESCE( MAX( CASE ON_TRACK WHEN 'N' THEN WEEK END ), 0 )
- COALESCE( MAX( CASE ON_TRACK WHEN 'Y' THEN WEEK END ), 0 ),
0
) AS weeks
FROM SOURCE_HISTORICAL_TBL
GROUP BY id
ORDER BY id
Results:
| ID | WEEKS |
|----|-------|
| 1 | 3 |
| 2 | 1 |
| 3 | 0 |
| 4 | 1 |
| 5 | 0 |

Related

Oracle how to return rows based on condition?

I am trying to return id and name based on flag column. If id has a rows with flag = 1 my query should only return these rows. If it hasn't flag=1 value it should return rows with flag = 0. What is the best way for it ? Here is sample data :
id name flag
5 aa 1
5 bb 0
6 cc 1
10 dd 0
11 ee 1
11 ee 0
Expected output is :
id name flag
5 aa 1
6 cc 1
10 dd 0
11 ee 1
Assuming flag column contains only 0 or 1, select rows whose flag is equal to maximal value of flags of given id:
select id, name, flag
from (
select id, name, flag, max(flag) over (partition by id) as m
from your_table
) x
where x.flag = x.m
You can use the keep dense_rank aggregating function to acheive that like below.
with t (id, name, flag) as (
select 5 , 'aa', 1 from dual union all
select 5 , 'bb', 0 from dual union all
select 6 , 'cc', 1 from dual union all
select 10, 'dd', 0 from dual union all
select 11, 'ee', 1 from dual union all
select 11, 'ee', 0 from dual
)
select id
, max(name)keep(dense_rank last order by id, flag) name
, max(flag)keep(dense_rank last order by id, flag) flag
from t
where flag in (0, 1)
group by id
order by id
;

Update with lag

I would like to set the ACTIVE value of a table as follow:
If FLAG=E => ACTIVE=1 and for any subsequent FLAG values, until FLAG=H
If FLAG=H => ACTIVE=0 and for any subsequent FLAG values, until FLAG=E
and so on and so forth.
Example
ID | FLAG | ACTIVE
---+------+-------
1 | E | 1
2 | V | 1
3 | H | 0
4 | V | 0
5 | E | 1
6 | S | 1
7 | V | 1
8 | D | 1
9 | H | 0
The value are ordered by date.
For simplicity, I added an ID column to get the column order.
Question
What can be the SQL update statement ?
Note:
The business rule can also be expressed as follow:
If for a given row, the count of preceding E - the count of preceding H is 1, then ACTIVE is 1 for this row, 0 otherwise.
You can get the active value with the last_value() analytic function:
select id, flag,
last_value(case when flag = 'E' then 1 when flag = 'H' then 0 end) ignore nulls
over (order by id) as active
from your_table;
As a demo:
create table your_table (id, flag) as
select 1, 'E' from dual
union all select 2, 'V' from dual
union all select 3, 'H' from dual
union all select 4, 'V' from dual
union all select 5, 'E' from dual
union all select 6, 'S' from dual
union all select 7, 'V' from dual
union all select 8, 'D' from dual
union all select 9, 'H' from dual;
select id, flag,
last_value(case when flag = 'E' then 1 when flag = 'H' then 0 end) ignore nulls
over (order by id) as active
from your_table;
ID F ACTIVE
---------- - ----------
1 E 1
2 V 1
3 H 0
4 V 0
5 E 1
6 S 1
7 V 1
8 D 1
9 H 0
You can use the same thing for an update, though a merge is probably going to be simpler:
alter table your_table add active number;
merge into your_table
using (
select id,
last_value(case when flag = 'E' then 1 when flag = 'H' then 0 end) ignore nulls
over (order by id) as active
from your_table
) tmp
on (your_table.id = tmp.id)
when matched then update set active = tmp.active;
9 rows merged.
select * from your_table;
ID F ACTIVE
---------- - ----------
1 E 1
2 V 1
3 H 0
4 V 0
5 E 1
6 S 1
7 V 1
8 D 1
9 H 0
db<>fiddle demo.
You said your real data is actually ordered by a date, and I guess there are multiple flags for each of multiple IDs, so something like this is probably more realistic:
create table your_table (id, flag_time, flag) as
select 1, timestamp '2018-07-04 00:00:00', 'E' from dual
union all select 1, timestamp '2018-07-04 00:00:01', 'V' from dual
union all select 1, timestamp '2018-07-04 00:00:02', 'H' from dual
union all select 1, timestamp '2018-07-04 00:00:03', 'V' from dual
union all select 1, timestamp '2018-07-04 00:00:04', 'E' from dual
union all select 1, timestamp '2018-07-04 00:00:05', 'S' from dual
union all select 1, timestamp '2018-07-04 00:00:06', 'V' from dual
union all select 1, timestamp '2018-07-04 00:00:07', 'D' from dual
union all select 1, timestamp '2018-07-04 00:00:08', 'H' from dual;
alter table your_table add active number;
merge into your_table
using (
select id, flag_time,
last_value(case when flag = 'E' then 1 when flag = 'H' then 0 end) ignore nulls
over (partition by id order by flag_time) as active
from your_table
) tmp
on (your_table.id = tmp.id and your_table.flag_time = tmp.flag_time)
when matched then update set active = tmp.active;
select * from your_table;
ID FLAG_TIME F ACTIVE
---------- ----------------------- - ----------
1 2018-07-04 00:00:00.000 E 1
1 2018-07-04 00:00:01.000 V 1
1 2018-07-04 00:00:02.000 H 0
1 2018-07-04 00:00:03.000 V 0
1 2018-07-04 00:00:04.000 E 1
1 2018-07-04 00:00:05.000 S 1
1 2018-07-04 00:00:06.000 V 1
1 2018-07-04 00:00:07.000 D 1
1 2018-07-04 00:00:08.000 H 0
The main difference is the partition by id and changing the ordering to use flag_time - or whatever your real columns are called.
db<>fiddle demo.
There is potentially an issue if two flags can share a time; with a timestamp column that's hopefully very unlikely, but with a date the precision of the column may allow it. There isn't much you can do about that though, except maybe get into some logic to break ties by assuming flags should arrive in a certain order, and give them a weighting based on that. Rather off-topic though.

Get last value from a certain group (Oracle)

I have something like this
Date Group ID
11/01 'A' 1
12/01 'A' 2
13/01 'B' 3
14/01 'B' 4
What i basically want is to get for example the latest from group 'A'
Date Group ID LatestID_from_GROUP_A_ordered_by_recent_date
11/01 'A' 1 2
12/01 'A' 2 2
13/01 'B' 3 2
14/01 'B' 4 2
or at least something like this
Date Group ID LatestID_from_GROUP_A_ordered_by_recent_date
11/01 'A' 1 null
12/01 'A' 2 null
13/01 'B' 3 2
14/01 'B' 4 2
How about this:
with demo (somedate, somegroup, id) as
( select date '2018-01-11', 'A', 1 from dual union all
select date '2018-01-12', 'A', 2 from dual union all
select date '2018-01-13', 'B', 3 from dual union all
select date '2018-01-14', 'B', 4 from dual union all
select date '2018-01-15', 'A', 5 from dual -- example from comments
)
select somedate, somegroup, id
, ( select max(id) keep (dense_rank last order by somedate)
from demo
where somegroup = 'A' ) as last_a
from demo;
SOMEDATE SOMEGROUP ID LAST_A
----------- --------- ---------- ----------
11/01/2018 A 1 5
12/01/2018 A 2 5
13/01/2018 B 3 5
14/01/2018 B 4 5
15/01/2018 A 5 5
Note the max(id) is only a tiebreaker in the event of multiple rows with the last date.
Gordon was almost there.
You want to create a window over your whole query, but only pick the biggest value of 'A':
select
t.*,
max(case when group = 'A' then id end) over (partition by 1) as latest_from_a
from t
'partition by 1' will create a window of your complete result set because it only groups by a single static value: 1.
The logic seems to be:
select t.*,
max(case when group = 'A' then id end) over (order by date) as latest_from_a
from t;
The above gets the cumulative maximum up to each date. If you want the overall maximum:
select t.*,
max(case when group = 'A' then id end) over () as latest_from_a
from t;

How to calculate percentage in oracle sql

I have a table in which I have multiple IDs which can have a value or 0. The IDs come from different sources so I would like to know what is the percentage of IDs with the value 0 as a percentage of total IDs, for each source file.
Sample Data:
ID Source
1 aaa
0 aaa
2 bbb
0 ccc
3 ccc
0 ccc
5 aaa
0 bbb
6 bbb
7 bbb
I need to display Output like:
CountOfIDs0 TotalIDs Source PercentageIDs0
2 3 ccc 66.6%%
1 3 aaa 33.3%%
1 4 bbb 25%
Thanks!
If you want a result like 66.6% rather than 66.7%, you would use trunc() rather than round() (although the latter is probably better). And you need to round a/b to three decimal places, so there is one left after you multiply by 100.
Then, you can have both counts in one query, and you can add the percentage calculation also in the same query.
select count(case when propkey = 0 then 1 end) countid0,
count(propkey) totalidcount,
source,
to_char(round(count(case when properkey = 0 then 1 end)/count(properkey), 3)*100)
|| '%' percentageids0
from......
Apply round function.
select count(id) as TotalIDs ,Source, sum(case when id=0 then 1 end) countid0,
to_char((sum(case when id=0 then 1 end)/count(id))*100)||'%' as PercentageIDs0
from Table1 group by Source
For Unique record you have to use DISTINCT Query
I would do it that way:
With MyRows AS (
SELECT 1 ID, 'aaa' SOURCE FROM DUAL UNION ALL
SELECT 0, 'aaa' FROM DUAL UNION ALL
SELECT 2, 'bbb' FROM DUAL UNION ALL
SELECT 0, 'ccc' FROM DUAL UNION ALL
SELECT 3, 'ccc' FROM DUAL UNION ALL
SELECT 0, 'ccc' FROM DUAL UNION ALL
SELECT 5, 'aaa' FROM DUAL UNION ALL
SELECT 0, 'bbb' FROM DUAL UNION ALL
SELECT 6, 'bbb' FROM DUAL UNION ALL
SELECT 7, 'bbb' FROM DUAL
)
SELECT
DISTINCT SOURCE,
SUM(CASE WHEN ID = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY SOURCE) ZERO_IDS,
COUNT(ID) OVER (PARTITION BY SOURCE) TOTAL_IDS,
(100 * SUM(CASE WHEN ID = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY SOURCE))/(COUNT(ID) OVER (PARTITION BY SOURCE)) PERCENTAGE
FROM MyRows
;
I calculated percentage of values in a column by using below query
Select A.,B., to_char((A.count_service/B.count_total)*100)||'%' from
(Select type_cd, count(type_cd) as count_type
from table1
group by type_cd) A
cross join
(Select count(type_cd) as count_total
from table1) B ;
select Source,
ROUND(100*number/sum(number) OVER (PARTITION BY p),2) as percentage,
sum(number) OVER (PARTITION BY p) as total
from(
select 1 p,
Source ,
count(Source) number
from declaration_assessment_result
GROUP by Source
)x

Get distinct rows based on priority?

I have a table as below.i am using oracle 10g.
TableA
------
id status
---------------
1 R
1 S
1 W
2 R
i need to get distinct ids along with their status. if i query for distinct ids and their status i get all 4 rows.
but i should get only 2. one per id.
here id 1 has 3 distinct statuses. here i should get only one row based on priority.
first priority is to 'S' , second priority to 'W' and third priority to 'R'.
in my case i should get two records as below.
id status
--------------
1 S
2 R
How can i do that? Please help me.
Thanks!
select
id,
max(status) keep (dense_rank first order by instr('SWR', status)) as status
from TableA
group by id
order by 1
fiddle
select id , status from (
select TableA.*, ROW_NUMBER()
OVER (PARTITION BY TableA.id ORDER BY DECODE(
TableA.status,
'S',1,
'W',2,
'R',3,
4)) AS row_no
FROM TableA)
where row_no = 1
This is first thing i would do, but there may be a better way.
Select id, case when status=1 then 'S'
when status=2 then 'W'
when status=3 then 'R' end as status
from(
select id, max(case when status='S' then 3
when status='W' then 2
when status='R' then 1
end) status
from tableA
group by id
);
To get it done you can write a similar query:
-- sample of data from your question
SQL> with t1(id , status) as (
2 select 1, 'R' from dual union all
3 select 1, 'S' from dual union all
4 select 1, 'W' from dual union all
5 select 2, 'R' from dual
6 )
7 select id -- actual query
8 , status
9 from ( select id
10 , status
11 , row_number() over(partition by id
12 order by case
13 when upper(status) = 'S'
14 then 1
15 when upper(status) = 'W'
16 then 2
17 when upper(status) = 'R'
18 then 3
19 end
20 ) as rn
21 from t1
22 ) q
23 where q.rn = 1
24 ;
ID STATUS
---------- ------
1 S
2 R
select id,status from
(select id,status,decode(status,'S',1,'W',2,'R',3) st from table) where (id,st) in
(select id,min(st) from (select id,status,decode(status,'S',1,'W',2,'R',3) st from table))
Something like this???
SQL> with xx as(
2 select 1 id, 'R' status from dual UNION ALL
3 select 1, 'S' from dual UNION ALL
4 select 1, 'W' from dual UNION ALL
5 select 2, 'R' from dual
6 )
7 select
8 id,
9 DECODE(
10 MIN(
11 DECODE(status,'S',1,'W',2,'R',3)
12 ),
13 1,'S',2,'W',3,'R') "status"
14 from xx
15 group by id;
ID s
---------- -
1 S
2 R
Here, logic is quite simple.
Do a DECODE for setting the 'Priority', then find the MIN (i.e. one with Higher Priority) value and again DECODE it back to get its 'Status'
Using MOD() example with added values:
SELECT id, val, distinct_val
FROM
(
SELECT id, val
, ROW_NUMBER() OVER (ORDER BY id) row_seq
, MOD(ROW_NUMBER() OVER (ORDER BY id), 2) even_row
, (CASE WHEN id = MOD(ROW_NUMBER() OVER (ORDER BY id), 2) THEN NULL ELSE val END) distinct_val
FROM
(
SELECT 1 id, 'R' val FROM dual
UNION
SELECT 1 id, 'S' val FROM dual
UNION
SELECT 1 id, 'W' val FROM dual
UNION
SELECT 2 id, 'R' val FROM dual
UNION -- comment below for orig data
SELECT 3 id, 'K' val FROM dual
UNION
SELECT 4 id, 'G' val FROM dual
UNION
SELECT 1 id, 'W' val FROM dual
))
WHERE distinct_val IS NOT NULL
/
ID VAL DISTINCT_VAL
--------------------------
1 S S
2 R R
3 K K
4 G G