Search code examples
sqloracle-databaseoracle11ghierarchical-dataunpivot

UNPIVOT the results of a delimited string on several rows


I am struggling with some complex hierarchical data. I have successfully used a CONNECT BY query to limit the rows down to the subset that i want - and i have used SYS_CONNECT_BY_PATH to return the full tree up to the nodes of interest.

this gives me essentially some rows like this (delimited by '|'):

id  path
-------------------
1, '|10|11|12|13'
2, '|10|14|15'
3, '|16|11|12|13'
4, '|16|17'

now - my challenge is to unwrap or UNPIVOT these values back into a structure like this:

id  ord node
-------------
1, 1, 10
1, 2, 11
1, 3, 12
1, 4, 13
2, 1, 10
2, 2, 14
2, 3, 15
3, 1, 16
3, 2, 11
3, 3, 12
3, 4, 13
4, 1, 16
4, 2, 17

I think i am unable to use UNPIVOT directly as that is working on a fixed set of columns - which this is not.

I am playing with a PIPELINE function to unwrap this, but frankly - passing all these rows to the function is an issue since they come from another query. I am wondering if anyone has a way to UNPIVOT the values from a SYS_CONNECT_BY_PATH result set back into rows that is maybe a pure sql solution - probably with REGEX parsing...

help always appreciated - thanks


Solution

  • Yes, UNPIVOT operator wont do much here to help you produce the desired output.

    As one of the approaches you could user regexp_count()(11g R1 version and up) regular

    expression function to count all occurrences of numbers and then use regexp_substr() regular

    expression function to extract the numbers as follows:

    -- sample of data 
    SQL> with t1(id1, path1) as(
      2    select 1, '|10|11|12|13' from dual union all
      3    select 2, '|10|14|15' from dual union all
      4    select 3, '|16|11|12|13' from dual union all
      5    select 4, '|16|17' from dual
      6  ),
      7  occurrences(ocr) as( -- occurrences 
      8    select level
      9     from ( select max(regexp_count(path1, '[^|]+')) as mx_ocr
     10              from t1
     11          ) t
     12   connect by level <= t.mx_ocr
     13  )
     14  select id1
     15       , row_number() over(partition by id1 order by id1) as ord
     16       , node
     17    from ( select q.id1
     18                , regexp_substr(q.path1, '[^|]+', 1, o.ocr)        as node
     19             from t1 q
     20            cross join occurrences o
     21          )
     22  where  node is not null
     23  order by id1, 2, node
     24  ;
    

    Result:

           ID1        ORD NODE
    ---------- ---------- ------------------------------------------------
             1          1 10
             1          2 11
             1          3 12
             1          4 13
             2          1 10
             2          2 14
             2          3 15
             3          1 11
             3          2 12
             3          3 13
             3          4 16
             4          1 16
             4          2 17
    
    13 rows selected
    

    As another approach, starting from 10g version and up, you could use model clause:

     SQL> with t1(id1, path1) as(
      2    select 1, '|10|11|12|13' from dual union all
      3    select 2, '|10|14|15' from dual union all
      4    select 3, '|16|11|12|13' from dual union all
      5    select 4, '|16|17' from dual
      6  )
      7  select id1
      8       , ord
      9       , node
     10    from t1
     11   model
     12   partition by ( rownum as id1)
     13   dimension by ( 1 as ord)
     14   measures( path1
     15           , cast(null as varchar2(11)) as node
     16           , nvl(regexp_count(path1, '[^|]+'), 0) as ocr )
     17   rules(
     18      node[for ord from 1 to ocr[1] increment 1] = 
     19          regexp_substr(path1[1], '[^|]+', 1, cv(ord))
     20  )
     21  order by id1, ord, node
     22  ;
    

    Result:

           ID1        ORD NODE
    ---------- ---------- -----------
             1          1 10
             1          2 11
             1          3 12
             1          4 13
             2          1 10
             2          2 14
             2          3 15
             3          1 16
             3          2 11
             3          3 12
             3          4 13
             4          1 16
             4          2 17
    
    13 rows selected
    

    SQLFiddle Demo