Oracle - natural sort rows on multiple levels - sql

Using Oracle 10.2.0.
I have a table that consists of a line number, an indent level, and text. I need to write a routine to 'natural' sort the text within an indent level [that is a child of a lower indent level]. I have limited experience with analytic routines and connect by/prior, but from what I've read here and elsewhere, it seems like they could be put to use to help my cause, but I can't figure out how.
CREATE TABLE t (ord NUMBER(5), indent NUMBER(3), text VARCHAR2(254));
INSERT INTO t (ord, indent, text) VALUES (10, 0, 'A');
INSERT INTO t (ord, indent, text) VALUES (20, 1, 'B');
INSERT INTO t (ord, indent, text) VALUES (30, 1, 'C');
INSERT INTO t (ord, indent, text) VALUES (40, 2, 'D');
INSERT INTO t (ord, indent, text) VALUES (50, 2, 'Z');
INSERT INTO t (ord, indent, text) VALUES (60, 2, 'E');
INSERT INTO t (ord, indent, text) VALUES (70, 1, 'F');
INSERT INTO t (ord, indent, text) VALUES (80, 2, 'H');
INSERT INTO t (ord, indent, text) VALUES (90, 2, 'G');
INSERT INTO t (ord, indent, text) VALUES (100, 3, 'J');
INSERT INTO t (ord, indent, text) VALUES (110, 3, 'H');
This:
SELECT ord, indent, LPAD(' ', indent, ' ') || text txt FROM t;
...returns:
ORD INDENT TXT
---------- ---------- ----------------------------------------------
10 0 A
20 1 B
30 1 C
40 2 D
50 2 Z
60 2 E
70 1 F
80 2 H
90 2 G
100 3 J
110 3 H
11 rows selected.
In the case I've defined for you, I need my routine to set ORD 60 = 50 and ORD 50 = 60 [flip them] because E is after D and before Z.
Same with ORD 80 and 90 [with 90 bringing 100 and 110 with it because they belong to it], 100 and 110. The final output should be:
ORD INDENT TXT
10 0 A
20 1 B
30 1 C
40 2 D
50 2 E
60 2 Z
70 1 F
80 2 G
90 3 H
100 3 J
110 2 H
The result is that each indent level is sorted alphabetically, within its indent level, within the parent indent level.

Here's what I got to work. No idea how efficient it might be on larger sets. The hard part for me was identifying the "parent" for a given row based solely on indent and original order.
WITH
a AS (
SELECT
t.*,
( SELECT MAX( ord )
FROM t t2
WHERE t2.ord < t.ord AND t2.indent = t.indent-1
) AS parent_ord
FROM
t
)
SELECT
ROWNUM*10 AS ord,
indent,
rpad( ' ', LEVEL-1, ' ' ) || text
FROM
a
CONNECT BY
PRIOR ord = parent_ord
START WITH
parent_ord IS NULL
ORDER SIBLINGS BY
text

Okay, here you go. The hard part in your data structure is that the parent is not (explicitly) known, so that the first part of the query does nothing but identify the parent according to the rules (for each node, it gets all subnodes one level deep, stopping as soon as the identation is smaller or equal to the start node).
The rest is easy, basically just some recursion with connect by to get the items in the order you want them (renumbering them dynamically).
WITH OrdWithParentInfo AS
(SELECT ID,
INDENT,
TEXT,
MIN(ParentID) ParentID
FROM (SELECT O.*,
CASE
WHEN (CONNECT_BY_ROOT ID = ID) THEN
NULL
ELSE
CONNECT_BY_ROOT ID
END ParentID
FROM (SELECT ROWNUM ID,
INDENT,
TEXT
FROM T
ORDER BY ORD) O
WHERE (INDENT = CONNECT_BY_ROOT INDENT + 1)
OR (CONNECT_BY_ROOT ID = ID)
CONNECT BY ((ID = PRIOR ID + 1) AND (INDENT > CONNECT_BY_ROOT INDENT)))
GROUP BY ID,
INDENT,
TEXT)
SELECT ROWNUM * 10 ORD, O.INDENT, O.TEXT
FROM OrdWithParentInfo O
START WITH O.ParentID IS NULL
CONNECT BY O.ParentID = PRIOR ID
ORDER SIBLINGS BY O.Text;

Related

Select rows matching the pattern: greater_than, less_than, greater_than

Got a database with entries indicating units earned by staff. Am trying to find a query that can select for me entries where the units_earned by the staff follow this pattern: >30 then <30 and then >30
In this SQL Fiddle, I would expect the query to return:
For John, Rows:
2, 4, 6
9, 10, 11
For Jane, Rows:
3, 5, 8
12, 13, 14
Here is the relevant SQL:
CREATE TABLE staff_units(
id integer,
staff_number integer,
first_name varchar(50),
month_name varchar(3),
units_earned integer,
PRIMARY KEY(id)
);
INSERT INTO staff_units VALUES (1, 101, 'john', 'jan', 32);
INSERT INTO staff_units VALUES (2, 101, 'john', 'jan', 33);
INSERT INTO staff_units VALUES (3, 102, 'jane', 'jan', 39);
INSERT INTO staff_units VALUES (4, 101, 'john', 'feb', 28);
INSERT INTO staff_units VALUES (5, 102, 'jane', 'feb', 28);
INSERT INTO staff_units VALUES (6, 101, 'john', 'mar', 39);
INSERT INTO staff_units VALUES (7, 101, 'john', 'mar', 34);
INSERT INTO staff_units VALUES (8, 102, 'jane', 'mar', 40);
INSERT INTO staff_units VALUES (9, 101, 'john', 'mar', 36);
INSERT INTO staff_units VALUES (10, 101, 'john', 'apr', 18);
INSERT INTO staff_units VALUES (11, 101, 'john', 'may', 32);
INSERT INTO staff_units VALUES (12, 102, 'jane', 'jun', 31);
INSERT INTO staff_units VALUES (13, 102, 'jane', 'jun', 28);
INSERT INTO staff_units VALUES (14, 102, 'jane', 'jun', 32);
Using window function lead you can refer to the next two leading records of the current record and then compare the three against your desired pattern.
with staff_units_with_leading as (
select id, staff_number, first_name, units_earned,
lead(units_earned) over w units_earned_off1, -- units_earned from record with offset 1
lead(units_earned, 2) over w units_earned_off2, -- units_earned from record with offset 2
lead(id) over w id_off1, -- id from record with offset 1
lead(id, 2) over w id_off2 -- id from record with offset 2
from staff_units
window w as (partition by first_name order by id)
)
, ids_wanted as (
select unnest(array[id, id_off1, id_off2]) id --
from staff_units_with_leading
where
id_off1 is not null -- Discard records with no two leading records
and id_off2 is not null -- Discard records with no two leading records
and units_earned > 30 -- Match desired pattern
and units_earned_off1 < 30 -- Match desired pattern
and units_earned_off2 > 30 -- Match desired pattern
)
select * from staff_units
where id in (select id from ids_wanted)
order by staff_number, id;
To generate trigrams just get rid of the unnest
with staff_units_with_leading as (
select id, staff_number, first_name, units_earned,
lead(units_earned) over w units_earned_off1, -- units_earned from record with offset 1
lead(units_earned, 2) over w units_earned_off2, -- units_earned from record with offset 2
lead(id) over w id_off1, -- id from record with offset 1
lead(id, 2) over w id_off2 -- id from record with offset 2
from staff_units
window w as (partition by first_name order by id)
)
select staff_number, array[id, id_off1, id_off2] id, array[units_earned , units_earned_off1 , units_earned_off2 ] units_earned --
from staff_units_with_leading
where
id_off1 is not null -- Discard records with no two leading records
and id_off2 is not null -- Discard records with no two leading records
and units_earned > 30 -- Match desired pattern
and units_earned_off1 < 30 -- Match desired pattern
and units_earned_off2 > 30 -- Match desired pattern
I took cachique's answer (with excellent idea to use lead() ) and reformatted and extended it to generate 3-grams as you originally wanted:
with staff_units_with_leading as (
select
id, staff_number, first_name, units_earned,
lead(units_earned) over w units_earned_off1, -- units_earned from record with offset 1
lead(units_earned, 2) over w units_earned_off2, -- units_earned from record with offset 2
lead(id) over w id_off1, -- id from record with offset 1
lead(id, 2) over w id_off2 -- id from record with offset 2
from staff_units
window w as (partition by staff_number order by id)
), ids_wanted as (
select
id_off1, -- keep this to group 3-grams later
unnest(array[id, id_off1, id_off2]) id
from staff_units_with_leading
where
id_off1 is not null -- Discard records with no two leading records
and id_off2 is not null -- Discard records with no two leading records
and units_earned > 30 -- Match desired pattern
and units_earned_off1 < 30 -- Match desired pattern
and units_earned_off2 > 30 -- Match desired pattern
), res as (
select su.*, iw.id_off1
from staff_units su
join ids_wanted iw on su.id = iw.id
order by su.staff_number, su.id
)
select
staff_number,
array_agg(units_earned order by id) as values,
array_agg(id order by id) as ids
from res
group by staff_number, id_off1
order by 1
;
The result will be:
staff_number | values | ids
--------------+------------+------------
101 | {33,28,39} | {2,4,6}
101 | {36,18,32} | {9,10,11}
102 | {39,28,40} | {3,5,8}
102 | {31,28,32} | {12,13,14}
(4 rows)
The problem you're trying to solve is a bit complicated. It is probably easier to solve it if you'll use pl/pgsql and play with integer arrays inside pl/pgsql function, or probably with JSON/JSONB.
But it also can be solved in plain SQL, however such SQL is pretty advanced.
with rows_numbered as (
select
*, row_number() over (partition by staff_number order by id) as row_num
from staff_units
order by staff_number
), sequences (staff_number, seq) as (
select
staff_number,
json_agg(json_build_object('row_num', row_num, 'id', id, 'units_earned', units_earned) order by id)
from rows_numbered
group by 1
)
select
s1.staff_number,
(s1.chunk->>'id')::int as id1,
(s2.chunk->>'id')::int as id2,
(s3.chunk->>'id')::int as id3
from (select staff_number, json_array_elements(seq) as chunk from sequences) as s1
, lateral (
select *
from (select staff_number, json_array_elements(seq) as chunk from sequences) _
where
(s1.chunk->>'row_num')::int + 1 = (_.chunk->>'row_num')::int
and (_.chunk->>'units_earned')::int < 30
and s1.staff_number = _.staff_number
) as s2
, lateral (
select *
from (select staff_number, json_array_elements(seq) as chunk from sequences) _
where
(s2.chunk->>'row_num')::int + 1 = (_.chunk->>'row_num')::int
and (_.chunk->>'units_earned')::int > 30
and s2.staff_number = _.staff_number
) as s3
where (s1.chunk->>'units_earned')::int > 30
order by 1, 2;
I used several advanced SQL features:
CTE
JSON
LATERAL
window functions.

Replace hard coded values with data from table

Currently, I have 3 affiliations hard-coded in a query. They serve as a heirarchy: 1 = Faculty, 2 = Staff, 3 = Student. If a user from the affiliations_tbl table has more than one affiliation (example: a Staff member who is also a Student), it will use their Staff affiliation since it is higher on the heirarchy that is defined with the partition by and decode().
SELECT x2.emplid,
scc_afl_code
FROM (SELECT x.emplid,
scc_afl_code,
row_number() over(partition BY x.emplid ORDER BY x.affil_order) r
FROM (SELECT t.emplid,
scc_afl_code,
DECODE(scc_afl_code,
'FACULTY',
1,
'STAFF',
2,
'STUDENT',
3,
999) affil_order
FROM affiliations_tbl t
WHERE t.scc_afl_code IN
(SELECT a.scc_afl_code
FROM affiliation_groups_tbl a
WHERE a.group = 'COLLEGE')) x) x2
WHERE x2.r = 1;
I have created a table that will store affiliation groups affiliation_groups_tbl so I can scale this by adding data to the table, rather than changing the hard-coded values in this query. Example: Instead of adding 'CONSULTANT', 4 to the decode() list, I would add it to the table, so I wouldn't have to modify the SQL.
scc_afl_code | group | group_name | sort_order
-------------+---------+------------+-----------
FACULTY | COLLEGE | Faculty | 1
STAFF | COLLEGE | Staff | 2
STUDENT | COLLEGE | Student | 3
I've already updated the latter half of the query to only select scc_afl_code that are in the COLLEGE_GROUP group. How can I properly update the first part of the query to use the table as a hierarchy?
Try a piece of code below instead decode in the select clause of your statement:
coalesce((
select g.sort_order
from affiliation_groups_tbl g
where g.scc_afl_code = t.scc_afl_code ), 999)
You can try like that
create table dictionary
(id number,
code varchar2(32),
name varchar2(32),
sort number);
insert into dictionary (id, code, name, sort) values (16, 'B', 'B name', 1);
insert into dictionary (id, code, name, sort) values (23, 'A', 'A name', 2);
insert into dictionary (id, code, name, sort) values (15, 'C', 'C name', 4);
insert into dictionary (id, code, name, sort) values (22, 'D', 'D name', 3);
select partition,
string,
decode(string, 'B', 1, 'A', 2, 'D', 3, 'C', 4, 999) decode,
row_number() over(partition by partition order by decode(string, 'B', 1, 'A', 2, 'D', 3, 'C', 4, 999)) ordering
from (select mod(level, 3) partition, chr(65 + mod(level, 5)) string
from dual
connect by level <= 8)
minus
-- Alternate --
select partition,
string,
nvl(t.sort, 999) nvl,
row_number() over(partition by partition order by nvl(t.sort, 999)) ordering
from (select mod(level, 3) partition, chr(65 + mod(level, 5)) string
from dual
connect by level <= 8) r
left join dictionary t
on t.code = r.string;

Oracle hierarchical sql with rollup count

How would I write SQL in Oracle to return tree view with rolloup count:
SQL would return this:
KLAS - COUNT
----------------------------------------------
ROOT - 10
KLAS1 - 5
KLAS2 - 2
KLAS3 - 3
KLAS4 - 5
KLAS5 - 5
KLAS6 - 0
KLAS7 - 0
I have two tables, one is where is structure hold, second is where I have data. Both tables are joined by klas coumn
Code for reproducing tables:
create table table1 (id number, parent_id number, klas varchar2(10));
insert into table1 (id, parent_id, klas) values (1, null, 'ROOT');
insert into table1 (id, parent_id, klas) values (2, 1, 'KLAS1');
insert into table1 (id, parent_id, klas) values (3, 2, 'KLAS2');
insert into table1 (id, parent_id, klas) values (4, 2, 'KLAS3');
insert into table1 (id, parent_id, klas) values (5, 1, 'KLAS4');
insert into table1 (id, parent_id, klas) values (6, 5, 'KLAS5');
insert into table1 (id, parent_id, klas) values (7, 1, 'KLAS6');
insert into table1 (id, parent_id, klas) values (8, 7, 'KLAS7');
create table table2 (klas varchar2(10), cnt number);
insert into table2(klas, cnt) values ('KLAS2', 2);
insert into table2(klas, cnt) values ('KLAS3', 3);
insert into table2(klas, cnt) values ('KLAS5', 5);
commit;
Regards, Igor
with c1 (parent_id, id, klas, p, o) as
(
select
parent_id, id, klas, '' as p, lpad(id, 10, '0') as o
from table1
where parent_id is null
union all
select
table1.parent_id, table1.id, table1.klas,
c1.p || '.....',
c1.o || '.' || lpad(table1.id, 10, '0') as o
from table1
inner join c1 on table1.parent_id = c1.id
),
c2 (id, klas, p, o, cnt) as
(
select c1.id, c1.klas, c1.p, c1.o, nvl(table2.cnt, 0)
from c1
left outer join table2 on c1.klas = table2.klas
)
select c3.p || c3.klas, (select sum(cnt) from c2 where c2.o like c3.o || '%') from c2 c3
order by c3.o
SQLFiddle - http://sqlfiddle.com/#!4/be779/97
Explanation
The first CTE is for positioning (i.e. the .....) and ordering (this is done by constructing column o that ensures that children come under the parent (so if the parent o is xxxx, the child will be xxxx || )
The 2nd CTE just gets the cnts - I think you can do it in the first CTE itself, but that would have been difficult to understand.
The subquery in the final query just gets the sum of a node and it's children.
Limits
Note that you can go up to 10 digit IDs on this one, if you want to go over that you have to change the 10 on the lpad.
The limit on depth depends on the varchar(max) - you add 11 characters (10 + one .) for each level. You can increase your depth limit if you are willing to limit your id length (a 5 digit long id will only add 6 characters for each level).
For o, you actually don't need the ., but it help understand what is going on.

How to compare values with lookup table

I am having one table "Mark" which contains marks of different subjects. If marks fit into one particular range then I should pick up respective rank and insert into marks table itself in column 'rank_sub_1'. could you please help me how can I look up in the table and insert in the column. Below is my table structure.
**Marks**
Subject1_Marks Subject2_Marks
------------------------------
71 22
10 40
**LookupTable**
Rank range1 range2
----------------------
9 10 20
8 21 30
7 31 40
6 41 50
5 51 60
4 61 70
3 71 80
2 81 90
1 91 100
Now I want to check marks of each subject with lookup table which contains the ranges and ranks for different marks obtained.
**Marks**
Subject1_Marks Subject2_Marks Rank_Sub_1 Rank_Sub_2
------------------------------------------------------
71 22
10 40
If marks fit into one particular range then I should pick up respective rank and insert into marks table itself in column 'rank_sub_1'. could you please help me how can I look up in the table and insert in the column.
(Considering there is no overlapping in range values)
Take two instances of lookuptable and join first with subject1_marks and second with subject2_marks. Here i haven't used LEFT JOINS as i am assuming your subject marks will fall under 1 range for sure. If you are not sure about that, please use left joins and handle null values as per your requirement for columns RANK_SUB_1 and RANK_SUB_2
WITH LOOKUPTABLE_TMP AS (SELECT * FROM LOOKUPTABLE)
SELECT M.*, L1.RANK AS RANK_SUB_1, L2.RANK AS RANK_SUB_2
FROM MARKS M , LOOKUPTABLE_TMP L1, LOOKUPTABLE_TMP L2
WHERE M.SUBJECT1_MARKS BETWEEN L1.RANGE1 AND L1.RANGE2
AND M.SUBJECT2_MARKS BETWEEN L2.RANGE1 AND L2.RANGE2
Then MERGE the data into table MARKS.
Solution:
MERGE INTO MARKS MS
USING
(
SELECT M.SUBJECT1_MARKS, M.SUBJECT2_MARKS, L1.RNK AS RANK_SUB_1, L2.RNK AS RANK_SUB_2
FROM MARKS M , LOOKUPTABLE L1, LOOKUPTABLE L2
WHERE M.SUBJECT1_MARKS BETWEEN L1.RANGE1 AND L1.RANGE2
AND M.SUBJECT2_MARKS BETWEEN L2.RANGE1 AND L2.RANGE2
GROUP BY M.SUBJECT1_MARKS, M.SUBJECT2_MARKS, L1.RNK, L2.RNK
) SUB
ON (MS.SUBJECT1_MARKS=SUB.SUBJECT1_MARKS AND MS.SUBJECT2_MARKS =SUB.SUBJECT2_MARKS)
WHEN MATCHED THEN UPDATE
SET MS.RANK_SUB_1=SUB.RANK_SUB_1, MS.RANK_SUB_2=SUB.RANK_SUB_2;
Tested on below schema and data as per your question's details.
CREATE TABLE MARKS (SUBJECT1_MARKS NUMBER, SUBJECT2_MARKS NUMBER , RANK_SUB_1 NUMBER, RANK_SUB_2 NUMBER)
INSERT INTO MARKS (SUBJECT1_MARKS , SUBJECT2_MARKS ) VALUES (71, 22);
INSERT INTO MARKS (SUBJECT1_MARKS , SUBJECT2_MARKS ) VALUES (10, 40);
CREATE TABLE LOOKUPTABLE (RNK NUMBER, RANGE1 NUMBER , RANGE2 NUMBER)
INSERT INTO LOOKUPTABLE VALUES (9, 10, 20);
INSERT INTO LOOKUPTABLE VALUES (8, 21, 30);
INSERT INTO LOOKUPTABLE VALUES (7, 31, 40);
INSERT INTO LOOKUPTABLE VALUES (6, 41, 50);
INSERT INTO LOOKUPTABLE VALUES (5, 51, 60);
INSERT INTO LOOKUPTABLE VALUES (4, 61, 70);
INSERT INTO LOOKUPTABLE VALUES (3, 71, 80);
INSERT INTO LOOKUPTABLE VALUES (2, 81, 90);
INSERT INTO LOOKUPTABLE VALUES (1, 91, 100);
Thanks!!
I think thisupdatestatement should do what you want:
UPDATE Marks m
SET Rank_Sub_1 = (SELECT l.Rank
FROM LookupTable l
WHERE m.Subject1_Marks BETWEEN l.range1 AND l.range2)
WHERE EXISTS (
SELECT 1
FROM LookupTable l
WHERE m.Subject1_Marks BETWEEN l.range1 AND l.range2
);
Sample SQL Fiddle
If you want to update the value forRank_Sub_2at the same time you can do this:
UPDATE Marks m
SET Rank_Sub_1 = (SELECT l.Rank
FROM LookupTable l
WHERE m.Subject1_Marks BETWEEN l.range1 AND l.range2)
,Rank_Sub_2 = (SELECT l.Rank
FROM LookupTable l
WHERE m.Subject2_Marks BETWEEN l.range1 AND l.range2)
Sample SQL Fiddle
Consider the design below which eliminates the possibility of overlaps or gaps. Although I usually use this technique with dates, any data that defines an unbroken sequence will work the same way. The idea is that you only define where the range starts. It is understood that the range stops at the last value possible less than the next higher range. However, notice I added a tenth rank, in case values less than 10 are possible. Any values greater than 100 will, of course, show as rank 1.
with
Lookup( Rank, Cutoff )as(
select 1, 91 union all
select 2, 81 union all
select 3, 71 union all
select 4, 61 union all
select 5, 51 union all
select 6, 41 union all
select 7, 31 union all
select 8, 21 union all
select 9, 10 union all
select 10, 0
),
Marks( Mark1, Mark2 )as(
select 71, 22 union all
select 10, 40 union all
select 21, 101
)
select Mark1, l1.Rank as Rank1, Mark2, l2.Rank as Rank2
from Marks m
join Lookup l1
on l1.Cutoff =(
select Max( Cutoff )
from Lookup
where Cutoff <= m.Mark1 )
join Lookup l2
on l2.Cutoff =(
select Max( Cutoff )
from Lookup
where Cutoff <= m.Mark2 );
The output:
Mark1 Rank1 Mark2 Rank2
----------- ----------- ----------- -----------
71 3 22 8
10 9 40 7
21 8 101 1

Selecting leaf id + root name from a table in oracle

I have a table that is self referencing, with id, parentid (referencing id), name, ordering as columns.
What I want to do is to select the first leaf node of each root and have a pairing of the id of the leaf node with the name of the root node.
The data can have unbounded levels, and siblings have an order (assigned by the "ordering" column). "First leaf node" means the first child's first child's first child's (etc..) child.
The data looks something like this, siblings ordered by ordering:
A
--a
--b
----b.1
----b.2
----b.3
B
--c
----c.1
----c.2
--d
C
--e
----e.1
------e.1.1
I want to be able to produce a mapping as follows:
name of A, id of a
name of B, id of c.1
name of C, id of e.1.1
This is the sql I'm using to achieve this, but I'm not too sure if it will recurse correctly for unbounded levels:
select id,
connect_by_root name name
from table
where connect_by_isleaf = 1
and ((level = 2 and ordering = 1)
or (level > 2 and ordering = 1 and prior ordering = 1))
start with parentid is null
connect by prior id = parentid;
Is there any way I can make rewrite the sql to make it unbounded?
I would use a subquery:
SQL> SELECT root_name, MIN(leaf_name) first_leaf
2 FROM (SELECT id, connect_by_root(r.NAME) root_name, r.NAME leaf_name
3 FROM recurse r
4 WHERE connect_by_isleaf = 1
5 START WITH parentid IS NULL
6 CONNECT BY PRIOR id = parentid)
7 GROUP BY root_name;
ROOT_NAME FIRST_LEAF
---------- ----------
A a
B c.1
C e.1.1
This will give you the first leaf (ordered by the leaf name) for each root.
Update
This is the script I used to generate your data:
CREATE TABLE recurse (
ID NUMBER PRIMARY KEY,
name VARCHAR2(10),
parentid NUMBER REFERENCES recurse (ID));
INSERT INTO recurse VALUES (1, 'A', '');
INSERT INTO recurse VALUES (3, 'b', 1);
INSERT INTO recurse VALUES (4, 'b.1', 3);
INSERT INTO recurse VALUES (5, 'b.2', 3);
INSERT INTO recurse VALUES (6, 'b.3', 3);
INSERT INTO recurse VALUES (7, 'B', '');
INSERT INTO recurse VALUES (8, 'c', 7);
INSERT INTO recurse VALUES (9, 'c.1', 8);
INSERT INTO recurse VALUES (10, 'c.2', 8);
INSERT INTO recurse VALUES (11, 'd', 7);
INSERT INTO recurse VALUES (12, 'C', '');
INSERT INTO recurse VALUES (13, 'e', 12);
INSERT INTO recurse VALUES (14, 'e.2', 13);
INSERT INTO recurse VALUES (15, 'e.1', 13);
INSERT INTO recurse VALUES (16, 'a', 1);
INSERT INTO recurse VALUES (20, 'e.1.1', 15);
As you can see I anticipated that your ordering would not be by name (this is really unclear from your question though).
Now suppose you want to order by ID (or really any other column it doesn't matter), you want to use analytics, for example:
SQL> SELECT DISTINCT root_name,
2 first_value(leaf_name)
3 over(PARTITION BY root_name ORDER BY ID) AS first_leaf_name
4 FROM (SELECT id, connect_by_root(r.NAME) root_name, r.NAME leaf_name
5 FROM recurse r
6 WHERE connect_by_isleaf = 1
7 START WITH parentid IS NULL
8 CONNECT BY PRIOR id = parentid)
9 ORDER BY root_name;
ROOT_NAME FIRST_LEAF_NAME
---------- ---------------
A b.1
B c.1
C e.2