views:

582

answers:

3

Hello

Lets assume I have this sample data:

| Name     | ID | PARENT_ID |
-----------------------------
| a1       | 1  | null      |
| b2       | 2  | null      |
| c3       | 3  | null      |
| a1.d4    | 4  | 1         |
| a1.e5    | 5  | 1         |
| a1.d4.f6 | 6  | 4         |
| a1.d4.g7 | 7  | 4         |
| a1.e5.h8 | 8  | 5         |
| a2.i9    | 9  | 2         |
| a2.i9.j10| 10 | 9         |

I would like to select all records start from accountId = 1, so the expected result would be:

| Name     | ID | PARENT_NAME | PARENT_ID | 
-------------------------------------------
| a1       | 1  | null        | null      |
| a1.d4    | 4  | a1          | 1         |
| a1.e5    | 5  | a1          | 1         |
| a1.d4.f6 | 6  | a1.d4       | 4         |
| a1.d4.g7 | 7  | a1.d4       | 4         |
| a1.e5.h8 | 8  | a1.e5       | 5         |

I am currently able to make the recursive select, but then I can't access the data from the parent reference, hence I can't return parent_name. The code I'm using is (adapted to the simplistic example):

SELECT id, parent_id, name
FROM tbl 
  START WITH id = 1 
  CONNECT BY PRIOR id = parent_id

What SQL should I be using to the mentioned above retrival?

Thank you, Maxim.

Additional key words for future seekers: SQL to select hierarchical data represented by parent keys in same table

+1  A: 

Do you want to do this?

SELECT id, parent_id, name, 
 (select Name from tbl where id = t.parent_id) parent_name
FROM tbl t start with id = 1 CONNECT BY PRIOR id = parent_id

Edit Another option based on OMG's one (but I think that will perform equally):

select 
           t1.id, 
           t1.parent_id, 
           t1.name,
           t2.name AS parent_name,
           t2.id AS parent_id
from 
    (select id, parent_id, name
    from tbl
    start with id = 1 
    connect by prior id = parent_id) t1
    left join
    tbl t2 on t2.id = t1.parent_id
Samuel
YES, but as far as I understand this is inefficient because that causes the RDBMS to make additional inner query for each returned result?
Maxim Veksler
@Maxim: You are correct - that is a correlated subquery, that will execute once for every row returned. While it works, it is the least efficient means available.
OMG Ponies
@OMG I don't think so. The optimizer is clever enough to infer a join by itself. Look at the explain plan.
Samuel
@Samuel: Check the `tkprof` trace - you'll find that it is.
OMG Ponies
@Samuel: Can you please explain your rational for using the inner select in this situation?
Maxim Veksler
@OMG Sorry but I can't agree. Scalar subquerys are a valid alternative to outer join's, and usually perform faster. Look at this http://www.oratechinfo.co.uk/scalar_subqueries.html
Samuel
@Maxim The inner select is for restricting the join only those rows that are going to be affected for the left join. It's faster to do the more restrictive "connect by" first and then the left join.
Samuel
+3  A: 

Use:

    SELECT t1.id, 
           t1.parent_id, 
           t1.name,
           t2.name AS parent_name,
           t2.id AS parent_id
      FROM tbl t1
 LEFT JOIN tbl t2 ON t2.id = t1.parent_id
START WITH t1.id = 1 
CONNECT BY PRIOR t1.id = t1.parent_id
OMG Ponies
I think that query is doing unnecessary work joining with itself too early. Don't you think?
Samuel
The OP lists columns that should return NULL, which the `LEFT JOIN` does - which is not unnecessary work either.
OMG Ponies
VERY GOOD, I'm happy. Thank you :). Please note for current version you would get "SQL Error: ORA-00918: column ambiguously defined", to fix this the SQL needs to be tweaked to read:START WITH t1.id = 1 CONNECT BY PRIOR t1.id = t1.parent_id
Maxim Veksler
@Maxim: Yeah, I realized after the 5 min mark that I missed the table aliases. Corrected.
OMG Ponies
A: 

It's a little on the cumbersome side, but I believe this should work (without the extra join). This assumes that you can choose a character that will never appear in the field in question, to act as a separator.

You can do it without nesting the select, but I find this a little cleaner that having four references to SYS_CONNECT_BY_PATH.

select id, 
       parent_id, 
       case 
         when lvl <> 1 
         then substr(name_path,
                     instr(name_path,'|',1,lvl-1)+1,
                     instr(name_path,'|',1,lvl)
                      -instr(name_path,'|',1,lvl-1)-1) 
         end as name 
from (
  SELECT id, parent_id, sys_connect_by_path(name,'|') as name_path, level as lvl
  FROM tbl 
  START WITH id = 1 
  CONNECT BY PRIOR id = parent_id)
Allan