Fetching Specific Group of Rows - sql

I have a table with 'Name', 'Flag' and some other columns. I want to select specific group of rows from table. Data is already sorted based on another time-stamp column.
Name Flag
------ ------
A D
B D
C D
D I
E I
D D
E D
B I
D I
F I
I want to fetch 1st set of 'D' Flag and last set of 'I' flag. Is it possible in SQL (only select statement, not PL/SQL) somehow?
Desired Output:
Name Flag
------ ------
A D
B D
C D
B I
D I
F I

SQL tables represent unordered sets. So, there is no "first" or "last", unless you have a column that specifies the ordering. Note that this applies to both SQL queries and to PL/SQL code. Of course, you specify that you have two columns, so no such column exists in your data.
But let me assume that you do have one. If so, you can do:
select t.*
from t
where (t.flag = 'D' and
t.orderingcol < (select min(t2.orderingcol) from t t2 where t2.flag <> 'D'
) or
(t.flag = 'I' and
t.orderingcol > (select max(t2.orderingcol) from t t2 where t2.flag <> 'I'
)
order by t.orderingcol;

Assuming you have some sort of column that determines the ordering of the result set (e.g. the id column in my query below), this is easy enough to do with a technique known as Tabibitosan:
WITH sample_data AS (SELECT 1 ID, 'A' NAME, 'D' flag FROM dual UNION ALL
SELECT 2 ID, 'B' NAME, 'D' flag FROM dual UNION ALL
SELECT 3 ID, 'C' NAME, 'D' flag FROM dual UNION ALL
SELECT 4 ID, 'D' NAME, 'I' flag FROM dual UNION ALL
SELECT 5 ID, 'E' NAME, 'I' flag FROM dual UNION ALL
SELECT 6 ID, 'D' NAME, 'D' flag FROM dual UNION ALL
SELECT 7 ID, 'E' NAME, 'D' flag FROM dual UNION ALL
SELECT 8 ID, 'B' NAME, 'I' flag FROM dual UNION ALL
SELECT 9 ID, 'D' NAME, 'I' flag FROM dual UNION ALL
SELECT 10 ID, 'F' NAME, 'I' flag FROM dual)
SELECT ID,
NAME,
flag
FROM (SELECT ID,
NAME,
flag,
grp,
MIN(CASE WHEN flag = 'D' THEN grp END) OVER (PARTITION BY flag) min_d_grp,
MAX(CASE WHEN flag = 'I' THEN grp END) OVER (PARTITION BY flag) max_i_grp
FROM (SELECT ID,
NAME,
flag,
row_number() OVER (ORDER BY ID) - row_number() OVER (PARTITION BY flag ORDER BY ID) grp
FROM sample_data
WHERE flag IN ('D', 'I')))
WHERE (flag = 'D' AND grp = min_d_grp)
OR (flag = 'I' AND grp = max_i_grp)
ORDER BY id;
ID NAME FLAG
---------- ---- ----
1 A D
3 C D
2 B D
9 D I
8 B I
10 F I
This query uses the tabibitosan method to generate an additional "grp" column, which you can then use to find the lowest number for the D flag rows and the highest for the I flag rows.
ETA: This may or may not perform better than Gordon's answer, but I would recommend you test both answers to see which works better for your tables/indexes/data etc.

Related

SQL how to change columns value in next 2 records

I have a table which contains two columns, one named "ID", and the other is "FLG". I need to add anther column "FLG1" that based on column "FLG".
The process logic is that if one record is "Y" in this table, then the next 2 records should be both "N" in "FLG1", if it is "N", then just keep it in "FLG1".
For example:
For record which ID=2 and FLG=Y,then FLG1 of this record should be "Y", but for the next 2 records(ID=3,4), FLG1 should be "N", though their FLG is "Y".
The expected result is as below:
I have tried many ways for days but failed and I don't want a stored procedure, I just want SQL query scripts for the implementation.
Here is the scripts for the data:
select 1 as ID, 'N' as FLG from dual union all
select 2 as ID, 'Y' as FLG from dual union all
select 3 as ID, 'Y' as FLG from dual union all
select 4 as ID, 'Y' as FLG from dual union all
select 5 as ID, 'Y' as FLG from dual union all
select 6 as ID, 'Y' as FLG from dual union all
select 7 as ID, 'Y' as FLG from dual union all
select 8 as ID, 'Y' as FLG from dual union all
select 9 as ID, 'N' as FLG from dual union all
select 10 as ID, 'Y' as FLG from dual union all
select 11 as ID, 'N' as FLG from dual union all
select 12 as ID, 'Y' as FLG from dual union all
select 13 as ID, 'N' as FLG from dual union all
select 14 as ID, 'N' as FLG from dual union all
select 15 as ID, 'N' as FLG from dual union all
select 16 as ID, 'Y' as FLG from dual union all
select 17 as ID, 'N' as FLG from dual union all
select 18 as ID, 'N' as FLG from dual union all
select 19 as ID, 'N' as FLG from dual union all
select 20 as ID, 'N' as FLG from dual union all
select 21 as ID, 'N' as FLG from dual union all
select 22 as ID, 'Y' as FLG from dual union all
select 23 as ID, 'Y' as FLG from dual
You need to iterate over the rows sequentially and calculate whether to show the row before you can calculate whether to show row following it; this means you need a recursive (or hierarchical) query.
You need to split the rows up into triplets of sequential rows such that each triplet starts with a FLG = 'Y' row and then all rows that are not in such a triplet or are in the second or third rows of the triplet will have a FLG1 value of N.
Like this:
WITH find_triplets ( id, flg, flg_count ) AS (
SELECT id,
flg,
DECODE( flg, 'Y', 1, 0 )
FROM table_name
WHERE id = 1
UNION ALL
SELECT t.id,
t.flg,
CASE f.flg_count
WHEN 0
THEN DECODE( t.flg, 'Y', 1, 0 )
ELSE MOD( f.flg_count + 1, 3 )
END
FROM find_triplets f
INNER JOIN table_name t
ON ( t.id = f.id + 1 )
)
SELECT id,
flg,
CASE
WHEN flg = 'Y' AND flg_count = 1
THEN 'Y'
ELSE 'N'
END as flg1
FROM find_triplets
ORDER BY id
Which, for your sample data, outputs:
ID | FLG | FLG1
-: | :-- | :---
1 | N | N
2 | Y | Y
3 | Y | N
4 | Y | N
5 | Y | Y
6 | Y | N
7 | Y | N
8 | Y | Y
9 | N | N
10 | Y | N
11 | N | N
12 | Y | Y
13 | N | N
14 | N | N
15 | N | N
16 | Y | Y
17 | N | N
18 | N | N
19 | N | N
20 | N | N
21 | N | N
22 | Y | Y
23 | Y | N
db<>fiddle here
Use `LAG` to see the previous two rows. Then use `CASE WHEN` to decide what to show.
select
id,
flg,
case
when lag(flg) over(order by id) = 'Y' or lag(flg, 2) over(order by id) = 'Y' then 'N'
else flg
end as flg1
from mytable
order by id;
Your description is wrong. You want to iterate through your rows for which you'd use a recursive query in SQL.
Number your rows first, because there can always be gaps in a table's IDs. Then use a recursive query to loop through the rows. I think this is the straight-forward to approach this.
with numbered as (select t.*, row_number() over (order by id) as rn from mytable t)
, cte (id, flg, flg1, prev_flg1, rn) as
(
select id, flg, flg, null, rn from numbered where rn = 1
union all
select
t.id,
t.flg,
case
when cte.flg1 = 'Y' or cte.prev_flg1 = 'Y' then 'N'
else t.flg
end,
cte.flg1,
t.rn
from cte
join numbered t on t.rn = cte.rn + 1
)
select id, flg, flg1
from cte
order by id;
In my misinterpretation of the question, this is a pretty simple gaps-and-islands problem. See the edit below for an improved answer. I would suggest using the difference of row numbers to define the islands. The definition of the flag is then just checking the row number on each group of 'Y' values:
select id, flg,
(case when flg = 'Y' and
mod(row_number() over (partition by flg, seqnum - seqnum_2 order by id), 3) = 1
then 'Y'
else 'N'
end) as flg1
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by flg order by id) as seqnum_2
from t
) t
order by id;
Here is a db<>fiddle.
If you want to update the flag, I would recommend using merge.
Note: I would also expect this to be faster (perhaps much faster) than a recursive CTE approach.
EDIT:
Alex makes a really good point. I think this requires a recursive CTE. If you have a large amount of data, it might be possible to optimize it by splitting the data into groups where you have multiple 'N's in a row. Your question doesn't mention data size.
I would approach this as:
with tt as (
select t.*, row_number() over (order by id) as seqnum
from t
),
cte (seqnum, id, flg, flg1, counter) as (
select seqnum, id, flg, flg,
(case when flg = 'Y' then 1 else 0 end)
from tt
where seqnum = 1
union all
select tt.seqnum, tt.id, tt.flg,
(case when cte.counter in (1, 2) then 'N'
when tt.flg = 'Y' then 'Y'
else 'N'
end),
(case when cte.counter in (1, 2) then cte.counter + 1
when tt.flg = 'Y' then 1
else 0
end)
from cte join
tt
on tt.seqnum = cte.seqnum + 1
)
select *
from cte;
Basically, this walks through the data and finds the first 'Y'. At that point, it sets a counter to 1. In the next two rows, the counter is incremented, regardless of the value of the flag. Then it goes back to looking for a 'Y' to repeat the process.
Amusingly, this seems like a pretty simple operation to implement using a Turing machine. Usually, it is not obvious how to implement such things.
Interestingly, if you put all the flags in a string, regular expressions solve the problem very simply:
select flgs,
substr(regexp_replace(flgs, 'Y(..|.$|$)', 'YNN'), 1, length(flgs)) as flg1s
from (select listagg(flg, '') within group (order by id) as flgs
from t
) t;

Identify only when value matches

I need to return only rows that have the match e.g Value = A, but I only need the rows that have A and with no other values.
T1:
ID Value
1 A
1 B
1 C
2 A
3 A
3 B
4 A
5 B
5 D
5 E
5 F
Desired Output:
2
4
how can I achieve this?
when I try the following, 1&3 are also returned:
select ID from T1 where Value ='A'
With NOT EXISTS:
select t.id
from tablename t
where t.value = 'A'
and not exists (
select 1 from tablename
where id = t.id and value <> 'A'
)
From the sample data you posted there is no need to use:
select distinct t.id
but if you get duplicates then use it.
Another way if there are no null values:
select id
from tablename
group by id
having sum(case when value <> 'A' then 1 else 0 end) = 0
Or if you want the rows where the id has only 1 value = 'A':
select id
from tablename
group by id
having count(*) = 1 and max(value) = 'A'
I think the simplest way is aggregation with having:
select id
from tablename
group by id
having min(value) = max(value) and
min(value) = 'A';
Note that this ignores NULL values so it could return ids with both NULL and A. If you want to avoid that:
select id
from tablename
group by id
having count(value) = count(*) and
min(value) = max(value) and
min(value) = 'A';
Oracle Setup:
CREATE TABLE test_data ( ID, Value ) AS
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 1, 'B' FROM DUAL UNION ALL
SELECT 1, 'C' FROM DUAL UNION ALL
SELECT 2, 'A' FROM DUAL UNION ALL
SELECT 3, 'A' FROM DUAL UNION ALL
SELECT 3, 'B' FROM DUAL UNION ALL
SELECT 4, 'A' FROM DUAL UNION ALL
SELECT 5, 'B' FROM DUAL UNION ALL
SELECT 5, 'D' FROM DUAL UNION ALL
SELECT 5, 'E' FROM DUAL UNION ALL
SELECT 5, 'F' FROM DUAL
Query:
SELECT ID
FROM test_data
GROUP BY ID
HAVING COUNT( CASE Value WHEN 'A' THEN 1 END ) = 1
AND COUNT( CASE Value WHEN 'A' THEN NULL ELSE 1 END ) = 0
Output:
| ID |
| -: |
| 2 |
| 4 |
db<>fiddle here

ORACLE get rows with condition value equals something but not equals to anything else

I have rows that look like .
OrderNo OrderStatus SomeOtherColumn
A 1
A 1
A 3
B 1 X
B 1 Y
C 2
C 3
D 2
I want to return all orders that have only one possible value of orderstatus. For e.g Here order B has only order status 1 SO result should be
B 1 X
B 1 Y
Notes:
Rows can be duplicated with same order status. For e.g. B here.
I am interested in the order having a very peculiar status for e.g. 1 here and not having any other status. So if B had a status of 3 at any point of time it is disqualified.
You can use not exists:
select t.*
from t
where not exists (select 1
from t t2
where t.orderno = t2.orderno and t.OrderStatus = t2.OrderStatus
);
If you just want the orders where this is true, you can use group by and having:
select orderno
from t
group by orderno
having min(OrderStatus) = max(OrderStatus);
If you only want a status of 1 then add max(OrderStatus) = 1 to the having clause.
Here is one way to do it. It does not handle the case where the status can be NULL; if that is possible, you will need to explain how you want it handled.
SQL> create table test_data ( orderno, status, othercol ) as (
2 select 'A', 1, null from dual union all
3 select 'A', 1, null from dual union all
4 select 'A', 3, null from dual union all
5 select 'B', 1, 'X' from dual union all
6 select 'B', 1, 'Y' from dual union all
7 select 'C', 2, null from dual union all
8 select 'C', 3, null from dual union all
9 select 'D', 2, null from dual
10 );
Table created.
SQL> variable input_status number
SQL> exec :input_status := 1
PL/SQL procedure successfully completed.
SQL> column orderno format a8
SQL> column othercol format a8
SQL> select orderno, status, othercol
2 from (
3 select t.*, count(distinct status) over (partition by orderno) as cnt
4 from test_data t
5 )
6 where status = :input_status
7 and cnt = 1
8 ;
ORDERNO STATUS OTHERCOL
-------- ---------- --------
B 1 X
B 1 Y
One way to handle NULL status (if that may happen), if in that case the orderno should be rejected (not included in the output), is to define the cnt differently:
count(case when status != :input_status or status is null then 1 end)
over (partition by orderno) as cnt
and in the outer query change the WHERE clause to a single condition,
where cnt = 0
Count distinct OrderStatus partitioned by OrderNo and show only rows where number equals one:
select OrderNo, OrderStatus, SomeOtherColumn
from ( select t.*, count(distinct orderstatus) over (partition by orderno) cnt
from t )
where cnt = 1
SQLFiddle demo
Just wanted to add something to Gordon's answer, using a stats function:
select orderno
from t
group by orderno
having variance(orderstatus) = 0;

ORACLE SQL LISTAGG with case when I get more then 100 results

Need get from DB some values as>
"value1, value2, ... value100"
but in case when I will recive more then 100 values need to do
"value1, value2, ... value100.." to know it is not all values but I want show maximal 100 values now I using
select LISTAGG(CASE WHEN ROWNUM <=100 THEN within.number ELSE NULL END,', ')
WITHIN GROUP (ORDER BY within.number )
from ...........
but that does not works as I need.
You can do this like so:
WITH sample_data AS (SELECT 1 ID, 'a' val FROM dual UNION ALL
SELECT 1 ID, 'b' val FROM dual UNION ALL
SELECT 1 ID, 'c' val FROM dual UNION ALL
SELECT 2 ID, 'd' val FROM dual UNION ALL
SELECT 2 ID, 'e' val FROM dual UNION ALL
SELECT 2 ID, 'f' val FROM dual UNION ALL
SELECT 2 ID, 'g' val FROM dual UNION ALL
SELECT 3 ID, 'h' val FROM dual UNION ALL
SELECT 3 ID, 'i' val FROM dual UNION ALL
SELECT 3 ID, 'h' val FROM dual UNION ALL
SELECT 3 ID, 'j' val FROM dual UNION ALL
SELECT 3 ID, 'k' val FROM dual UNION ALL
SELECT 4 ID, 'l' val FROM dual UNION ALL
SELECT 4 ID, 'm' val FROM dual UNION ALL
SELECT 5 ID, 'n' val FROM dual)
SELECT ID,
listagg(CASE WHEN rn <= 3 THEN val ELSE '...' END, ',') WITHIN GROUP (ORDER BY val) vals
FROM (SELECT ID,
val,
row_number() OVER (PARTITION BY ID ORDER BY val) rn
FROM sample_data)
WHERE rn <= 4 -- max amount of expected elements + 1
GROUP BY ID;
ID VALS
--- -----------
1 a,b,c
2 d,e,f,...
3 h,h,i,...
4 l,m
5 n
In my example, I want to display just three elements, along with "..." if there are other elements available. So, first off, we filter the results to just the first four rows for each id.
To do that, I used the ROW_NUMBER analytic function to label each row with a number in ascending val order for each id.
Once we know the row numbers, we can filter the rows to return the expected number of elements + 1 - we need the extra row to know if there are more rows available or not. In my case, that means we need to get the first 4 rows.
Next, we need a case statement to output the actual value for the first three elements, and "..." for the fourth element, if present.
Then we can incorporate that into the LISTAGG and voila!
Of course, the above assumes that your database isn't at version 12.2 - if it is, then you can take advantage of the new overflow enhancements - see here for more information

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