How to get rows with last update on column - sql

Goal:
For each "IDCONT", i need to get the "DAY_ID" where i have the last change/update on "STATE_ID".
Example:
with reftable as (
select 1 as PROCESSID, 'A' as IDCONT, 'X' as STATEID, '10' AS DAY_ID union all
select 2 as PROCESSID, 'A' as IDCONT, 'X' as STATEID, '11' AS DAY_ID union all
select 3 as PROCESSID, 'A' as IDCONT, 'Y' as STATEID, '12' AS DAY_ID union all
select 4 as PROCESSID, 'A' as IDCONT, 'Y' as STATEID, '13' AS DAY_ID union all
select 1 as PROCESSID, 'B' as IDCONT, 'N' as STATEID, '14' AS DAY_ID union all
select 2 as PROCESSID, 'B' as IDCONT, 'N' as STATEID, '15' AS DAY_ID union all
select 3 as PROCESSID, 'B' as IDCONT, 'M' as STATEID, '16' AS DAY_ID union all
select 1 as PROCESSID, 'C' as IDCONT, 'X' as STATEID, '11' AS DAY_ID union all
select 2 as PROCESSID, 'C' as IDCONT, 'X' as STATEID, '18' AS DAY_ID union all
) ...
Expected result:
PROCESSID IDCONT STATID DAYID
3 A Y 12
2 B N 15
1 C X 11
I solved the problem with this:
...
SELECT IDCONT, STATEID, MIN(DAY_ID)
FROM REFTABLE
WHERE (IDCONT, STATEID) IN (
SELECT IDCONT, FIRST_VALUE(STATEID) OVER PARTITION BY IDCONT ORDER BY PROCESSID DESC) AS STATEID
FROM REFTABLE
)
But i want to do the same without the need to call the table a 2nd time.
Thx!

Here is one method:
select r.*
from (select r.*,
lag(stateid) over (partition by idcont order by day_id) as prev_stateid,
first_value(stateid) over (partition by idcont order by day_id desc) as last_stateid
from reftable r
) r
where stateid = last_stateid and (prev_stateid is null or prev_stateid <> stateid);
However, this does not handle the case where the state changes back to a previous state. That logic can be added in, if necessary.

It would be simpler if you didn't need to return IDCONT whose STATEID didn't change (that would be a C). One REFTABLE trip might look like this; see if it does any good in reality.
SQL> with reftable as (
2 select 1 as PROCESSID, 'A' as IDCONT, 'X' as STATEID, '10' AS DAY_ID from dual union all
3 select 2 as PROCESSID, 'A' as IDCONT, 'X' as STATEID, '11' AS DAY_ID from dual union all
4 select 3 as PROCESSID, 'A' as IDCONT, 'Y' as STATEID, '12' AS DAY_ID from dual union all
5 select 4 as PROCESSID, 'A' as IDCONT, 'Y' as STATEID, '13' AS DAY_ID from dual union all
6 --
7 select 1 as PROCESSID, 'B' as IDCONT, 'N' as STATEID, '14' AS DAY_ID from dual union all
8 select 2 as PROCESSID, 'B' as IDCONT, 'N' as STATEID, '15' AS DAY_ID from dual union all
9 select 3 as PROCESSID, 'B' as IDCONT, 'M' as STATEID, '16' AS DAY_ID from dual union all
10 --
11 select 1 as PROCESSID, 'C' as IDCONT, 'X' as STATEID, '11' AS DAY_ID from dual union all
12 select 2 as PROCESSID, 'C' as IDCONT, 'X' as STATEID, '18' AS DAY_ID from dual
13 ),
14 inter as
15 (select processid, idcont, stateid, day_id,
16 case when nvl(lag(stateid) over
17 (partition by idcont order by processid ), '?') <> stateid then
18 row_number() over (partition by idcont order by processid )
19 end grp
20 from reftable
21 )
22 select processid, idcont, stateid, day_id
23 from inter i
24 where grp = (select max(i1.grp)
25 from inter i1
26 where i1.idcont = i.idcont)
27 order by idcont, processid;
PROCESSID IDCONT STATEID DAY_ID
---------- ---------- ---------- ----------
3 A Y 12
3 B M 16
1 C X 11
SQL>

Using "FIRST_VALUE" does give you the change, but can you rely on there only being a single change in the table? Won't other changes invalidate this and result in bad data?
I used the LAG function instead but it doesn't return IDCONT C because it has not had a change.
Using a CTE to grab the data and then a query to filter might be faster (since you can't put LAG or FIRST_VALUE in the where clause). It would prevent another trip to the database.
CREATE TABLE REFTABLE
([PROCESSID] int, [IDCONT] varchar(1), [STATEID] varchar(1), [DAY_ID] int)
;
INSERT INTO REFTABLE
([PROCESSID], [IDCONT], [STATEID], [DAY_ID])
VALUES
(1, 'A', 'X', 10),
(2, 'A', 'X', 11),
(3, 'A', 'Y', 12),
(4, 'A', 'Y', 13),
(1, 'B', 'N', 14),
(2, 'B', 'N', 15),
(3, 'B', 'M', 16),
(1, 'C', 'X', 11),
(2, 'C', 'X', 18)
;
with chgfound as (SELECT TOP 100 PERCENT PROCESSID, IDCONT, STATEID, DAY_ID, LAG(STATEID) OVER(PARTITION BY IDCONT ORDER BY IDCONT, PROCESSID) as LastState
from REFTABLE
order by IDCONT, PROCESSID
)
select * from chgfound where STATEID !=LastState
http://www.sqlfiddle.com/#!18/086134
Also just noticed you have the Oracle tag. I did this in SQL Server but it has to be really close to the same.

Related

ORACLE SQL | If a column contains a value, then it will exclude a different value from the same column

I have this query that returns the data below it
select LISTAGG(d.DOCUMENT_TYPE_CD, ',') WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d;
VALUE
---------
CI,ECI,POA
now I'm trying to add a condition whenever 'ECI' value is present, it should exclude 'CI' in the result like this one below
VALUE
---------
ECI,POA
I tried using case statement in where condition it prompted an error
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d
where CASE d.DOCUMENT_TYPE_CD
WHEN 'ECI' THEN d.DOCUMENT_TYPE_CD <> 'CI'
END;
ORA-00905: missing keyword
00905. 00000 - "missing keyword"
*Cause:
*Action:
Error at Line: 7 Column: 36
is there any other way I could resolve this?
See if this helps; read comments within code.
SQL> with
2 test (id, document_type_cd) as
3 -- sample data
4 (select 1, 'ECI' from dual union all
5 select 1, 'CI' from dual union all
6 select 1, 'POA' from dual union all
7 --
8 select 2, 'CI' from dual union all
9 select 2, 'POA' from dual union all
10 --
11 select 3, 'XYZ' from dual union all
12 select 3, 'ABC' from dual
13 ),
14 temp as
15 -- see whether CI and ECI exist per each ID
16 (select id,
17 sum(case when document_type_cd = 'CI' then 1 else 0 end) sum_ci,
18 sum(case when document_type_cd = 'ECI' then 1 else 0 end) sum_eci
19 from test
20 group by id
21 ),
22 excl as
23 -- exclude CI rows if ECI exist for that ID
24 (select a.id,
25 a.document_type_cd
26 from test a join temp b on a.id = b.id
27 where a.document_type_cd <> case when b.sum_ci > 0 and b.sum_eci > 0 then 'CI'
28 else '-1'
29 end
30 )
31 -- finally:
32 select e.id,
33 listagg(e.document_type_cd, ',') within group (order by e.document_type_cd) result
34 from excl e
35 group by e.id;
ID RESULT
---------- --------------------
1 ECI,POA
2 CI,POA
3 ABC,XYZ
SQL>
Something like this:
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d,
(select sum (case when DOCUMENT_TYPE_CD = 'CI' then 1 else 0 end) C
from test_table) A
where d.DOCUMENT_TYPE_CD <> case when A.c > 0 then 'CI' when A.c = 0 then ' ' end;
DEMO
You may identify the presence of both the values with two conditional aggregations in the same group by and then replace CI inside the result of listagg in one pass.
with a(id, cd) as (
select 1, 'ABC' from dual union all
select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
select 2, 'XYZ' from dual union all
select 2, 'ECI' from dual union all
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
select 3, 'CI' from dual union all
select 3, 'POA' from dual union all
select 4, 'ABC' from dual union all
select 4, 'DEF' from dual
)
select
id,
ltrim(
/*Added comma in case CI will be at the beginning*/
replace(
',' || listagg(cd, ',') within group (order by cd asc),
decode(
/*If both are present, then replace CI. If not, then do not replace anything*/
max(decode(cd, 'CI', 1))*max(decode(cd, 'ECI', 1)),
1,
',CI,'
),
','
),
','
) as res
from a
group by id
ID | RES
-: | :----------
1 | ABC,ECI,POA
2 | ECI,POA,XYZ
3 | CI,POA
4 | ABC,DEF
db<>fiddle here
Instead of using GROUP BY, you can also use windowing (aka analytic) functions to check the presence of ECI per group (test data shamelessly stolen from #littlefoot):
with
test (id, document_type_cd) as
-- sample data
(select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
--
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
--
select 3, 'XYZ' from dual union all
select 3, 'ABC' from dual
),
temp as
(select id,
document_type_cd,
sum(case when document_type_cd = 'ECI' then 1 else 0 end) over (partition by id) as sum_eci
from test
)
select a.id,
listagg(a.document_type_cd, ',') within group (order by a.document_type_cd) result
from temp a
where a.document_type_cd != 'CI' or sum_eci = 0
group by a.id;

I need the count of values

I have data in my table in oracle like below
A_CODE B_M P_id
------ ---- ------
123 A 1
123 A 2
123 B 5
678 B 3
678 C 3
678 B 4
123 BC 2
The value "BC" is B and C. The data is not normalized so we need to count it as B and C. I need the counts to be displayed as below per A_CODE
A_CODE B_M COUNT
------- ---- -------
123 A 2
123 B 2
123 C 1
678 B 2
678 C 1
How can i do this in Oracle?
You should use CONNECT BY and CONNECT_BY_ROOT.
I hope this helps:
SELECT A_CODE, B_M, COUNT (*)
FROM ( SELECT A_CODE, SUBSTR (CONNECT_BY_ROOT (B_M), LEVEL, 1) B_M
FROM your_table
CONNECT BY LEVEL <= LENGTH (B_M))
WHERE B_M IS NOT NULL
GROUP BY A_CODE, B_M
ORDER BY A_CODE;
Please try below:
SELECT A_CODE, B_M, COUNT(*) "COUNT" FROM
(SELECT A_CODE, B_M
FROM
(SELECT A_CODE,
SUBSTR(B_M,x.LVL,1) B_M
FROM my_table t,
(SELECT LEVEL LVL FROM dual
CONNECT BY LEVEL <=
(SELECT MAX(LENGTH(B_M)) FROM my_table)
) x
WHERE t.B_M is not null
)
WHERE B_M IS NOT NULL
UNION ALL
SELECT A_CODE, B_M FROM my_table WHERE B_M IS NULL
)
GROUP BY A_CODE,
B_M
ORDER BY A_CODE, B_M;
One option is to join this table to a data set that provides you with the normalised structure you need.
with cte_normaliser as (
select 'A' B_M, 'A' 'B_M_norm from dual union all
select 'B' B_M, 'B' 'B_M_norm from dual union all
select 'C' B_M, 'C' 'B_M_norm from dual union all
select 'BC' B_M, 'B' 'B_M_norm from dual union all
select 'BC' B_M, 'C' 'B_M_norm from dual)
select my_table.A_CODE,
n.B_M_norm,
count(*)
from my_table join
cte_normaliser n on n.B_M = my_table.B_M
group by my_table.A_CODE,
n.B_M_norm;
Using a fixed data set like that might not be feasible if you have a large number of variable code combination, though, and that data set might need to be built dynamically.
Besides having my_table, you create a table with all possible values. That is:
P_VAL
-----
A
B
C
Then, you should be able to obtain the count of occurrences with a join. Something like:
with my_table
as ( select '123' a_code, 'A' b_m, '1' p_id from dual
union select '123' a_code, 'A' b_m, '2' p_id from dual
union select '123' a_code, 'B' b_m, '5' p_id from dual
union select '678' a_code, 'B' b_m, '3' p_id from dual
union select '678' a_code, 'C' b_m, '3' p_id from dual
union select '678' a_code, 'B' b_m, '4' p_id from dual
union select '123' a_code, 'BC' b_m, '2' p_id from dual)
,possible_values
as ( select 'A' p_val from dual
union select 'B' p_val from dual
union select 'C' p_val from dual)
select a_code
,p_val b_m
,count('X') count
from my_table
join possible_values
on instr(b_m,p_val) > 0
group by a_code,p_val
order by a_code,p_val;

Oracle SQL query using case when, compacting null fields

I have a table like this:
Items
id group old_new object
1 A O pen
2 A N house
3 B O dog
4 B O cat
5 C N mars
6 C O sun
7 C N moon
8 C o earth
I would like the select return:
Items
group new_object old_object
A house pen
B null dog
B null cat
C mars sun
C moon earth
If I try:
select id,
case when old_new = 'N' then object end as new_object,
case when old_new = 'O' then object end as old_object
from the_table
order by id;
I have 8 row with many field as null
es: last rows:
group new_object old_object
C mars null
c null sun
C moon null
c null earth
But of group C I want only 2 rows...
is not like the other query 'Oracle sql join same table ecc...' because here don't want null result
I'm going to make the assumption that Old and New records are paired in the order they appear based on the ID value. With that assumption the following query:
WITH DTA(ID, GRP, OLD_NEW, OBJECT) AS (
select 1, 'A', 'O', 'pen' from dual union all
select 2, 'A', 'N', 'house' from dual union all
select 3, 'B', 'O', 'dog' from dual union all
select 4, 'B', 'O', 'cat' from dual union all
select 5, 'C', 'N', 'mars' from dual union all
select 6, 'C', 'O', 'sun' from dual union all
select 7, 'C', 'N', 'moon' from dual union all
select 8, 'C', 'O', 'earth' from dual
), dta2 as (
select dta.*
, row_number() over (partition by GRP, old_new order by id) rn
from dta
)
select coalesce(n.grp, o.grp) grp
, n.object new_object
, o.object old_object
from (select * from dta2 where old_new = 'N') n
full join (select * from dta2 where old_new = 'O') o
on n.grp = o.grp
and n.rn = o.rn;
Aside from the sample data section (with dta) this script first uses the analytic function ROW_NUMBER() to add a sequential number partitioned by the group and old_new columns. It then performs a full outer join on two inline views of the dta2 subfactored query, one for thr old objects and one for the new objects. The result, at least for this data set is:
GRP NEW_OBJECT OLD_OBJECT
--- ---------- ----------
A house pen
B dog
B cat
C mars sun
C moon earth
In the first step assign an index (IDX) of the chnage withing your group. I'm using order by ID, but this is upon you. The important thing is that the old and new valuea are unique connected with GRP and IDX.
In next step let PIVOT work for you (I'm using the data from #Sentinel, thx!)
WITH DTA(ID, GRP, OLD_NEW, OBJECT) AS (
select 1, 'A', 'O', 'pen' from dual union all
select 2, 'A', 'N', 'house' from dual union all
select 3, 'B', 'O', 'dog' from dual union all
select 4, 'B', 'O', 'cat' from dual union all
select 5, 'C', 'N', 'mars' from dual union all
select 6, 'C', 'O', 'sun' from dual union all
select 7, 'C', 'N', 'moon' from dual union all
select 8, 'C', 'O', 'earth' from dual
), DTA2 as (
SELECT
ROW_NUMBER() OVER (PARTITION BY GRP,OLD_NEW order by ID) as IDX,
GRP, OLD_NEW, OBJECT
from DTA
)
select * from DTA2
PIVOT (max(OBJECT) OBJECT for (OLD_NEW) in
('N' as "NEW",
'O' as "OLD"
))
order by GRP;
result
IDX, GRP, NEW_OBJECT, OLD_OBJECT
1 A house pen
1 B dog
2 B cat
2 C moon earth
1 C mars sun
Here's an alternative using PIVOT to get the results:
with items as (select 1 id, 'A' grp, 'O' old_new, 'pen' obj from dual union all
select 2 id, 'A' grp, 'N' old_new, 'house' obj from dual union all
select 3 id, 'B' grp, 'O' old_new, 'dog' obj from dual union all
select 4 id, 'B' grp, 'O' old_new, 'cat' obj from dual union all
select 5 id, 'C' grp, 'N' old_new, 'mars' obj from dual union all
select 6 id, 'C' grp, 'O' old_new, 'sun' obj from dual union all
select 7 id, 'C' grp, 'N' old_new, 'moon' obj from dual union all
select 8 id, 'C' grp, 'O' old_new, 'earth' obj from dual)
-- end of mimicking your items table with data in it. See SQL below:
select grp,
new_object,
old_object
from (select grp,
old_new,
obj,
row_number() over (partition by grp, old_new order by id) rn
from items)
pivot (max(obj)
for old_new in ('N' new_object,
'O' old_object))
order by grp,
rn;
GRP NEW_OBJECT OLD_OBJECT
--- ---------- ----------
A house pen
B dog
B cat
C mars sun
C moon earth
Provided that
there's at most one new object for each old,
there's no new object without old object, and
there's at most one old object for any group (this is not true for your sample data, but in comments you indicate you're interested in such solution as well)
a simpler query may be used than for the general case:
select
old.group as group, new.object as new_object, old.object as old_object
from
(select group, object from my_table where old_new = 'O') old
left join
(select group, object from my_table where old_new = 'N') new
on (old.group = new.group)

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

How can I rank sequential rows with a null value in a query?

I would like to rank sequential rows with the null value inside a group of the same natural key. The database is oracle.
Here's the example :
NAT_KEY ATTRIBUTE_A ORDERED_FIELD RANK
A A 1
A 2 1
A A 4
A I 6
A 8 1
A 10 2
A 11 3
B 2 1
B 3 2
B A 5
B A 6
B 9 1
C A 1
C A 5
C I 6
C 7 1
C 8 2
There must be a way with row_number() function, level and connect by or another one.
My guess is that you want something like
select key,
attr,
order_by,
(case when rnk1 is not null
then rank() over (partition by key order by rnk1)
else null
end) rnk
from (
select x.*,
(case when attr is null
then row_number() over (partition by key order by order_by)
else null
end) rnk1
from <<table name>> x
)
order by key, order_by
That produces output like this
SQL> ed
Wrote file afiedt.buf
1 with x as (
2 select 'A' key, 'A' attr, 1 order_by from dual union all
3 select 'A', null, 2 from dual union all
4 select 'A', 'A', 4 from dual
5 )
6 select key,
7 attr,
8 order_by,
9 (case when rnk1 is not null
10 then rank() over (partition by key order by rnk1)
11 else null
12 end) rnk
13 from (
14 select x.*,
15 (case when attr is null
16 then row_number() over (partition by key order by order_by)
17 else null
18 end) rnk1
19 from x
20 )
21* order by key, order_by
SQL> /
K A ORDER_BY RNK
- - ---------- ----------
A A 1
A 2 1
A A 4
I finally figured it out with Andriy M's link.
Here's the solution :
select grouped_table.nat_key,
grouped_table.attr,
grouped_table.order_by,
case
when attr is null
then row_number() over (partition by nat_key, attr, grp order by order_by)
else null
end rowrank
from (
select the_table.*,
row_number() over (partition by nat_key order by order_by) - row_number() over (partition by nat_key, nvl2(attr, 1, 0) order by order_by) grp
from (
select 'A' nat_key, 'A' attr, 1 order_by from dual
union all
select 'A' nat_key, null attr, 2 order_by from dual
union all
select 'A' nat_key, 'A' attr, 4 order_by from dual
union all
select 'A' nat_key, 'I' attr, 6 order_by from dual
union all
select 'A' nat_key, null attr, 8 order_by from dual
union all
select 'A' nat_key, null attr, 10 order_by from dual
union all
select 'A' nat_key, null attr, 11 order_by from dual
union all
select 'B' nat_key, null attr, 2 order_by from dual
union all
select 'B' nat_key, null attr, 3 order_by from dual
union all
select 'B' nat_key, 'A' attr, 5 order_by from dual
union all
select 'B' nat_key, 'A' attr, 6 order_by from dual
union all
select 'B' nat_key, null attr, 9 order_by from dual
union all
select 'C' nat_key, 'A' attr, 1 order_by from dual
union all
select 'C' nat_key, 'A' attr, 5 order_by from dual
union all
select 'C' nat_key, 'I' attr, 6 order_by from dual
union all
select 'C' nat_key, null attr, 7 order_by from dual
union all
select 'C' nat_key, null attr, 8 order_by from dual
) the_table
) grouped_table
order by nat_key, order_by
Thanks!