Self referencing table SQL Query to rows - sql

I have this oracle 11g query:
SELECT RPAD(' ', 2 * (T.ID_LEVEL - 1)) || T.IDE IDE, T.ID_LEVEL, T.CODE, T.FK_IDE
FROM TEST_DYNAMIC T
START WITH T.FK_IDE = 0
CONNECT BY NOCYCLE PRIOR T.IDE = T.FK_IDE
ORDER BY T.IDE,T.FK_IDE;
That returns this data:
IDE |IDE_LEVEL |CODE |FK_IDE |
-----|-----------|------|--------|
1 | 1|A01 | 0|
2 | 2|A01 | 1|
3 | 3|A01 | 2|
4| 4|A01 | 3|
5 | 2|A02 | 1|
6 | 2|A03 | 1|
7 | 3|A01 | 6|
8 | 1|A02 | 0|
As you can see, the data is obtained from a self refrencing table, where the IDE_LEVEL column is a foreign key of a master table that contains this values (1, 2, 3, 4) as PK and the IDE column is an autoincrement PK from TEST_DYNAMIC table.
Is there a way to convert that result to this one?:
IDE |CODE_LEVEL1 |CODE_LEVEL2 |CODE_LEVEL3 |CODE_LEVEL4 |
-----|-------------|-------------|-------------|-------------|
1 |A01 |A01 |A01 |A01 |
1 |A01 |A02 |NULL |NULL |
1 |A01 |A03 |A01 |NULL |
8 |A02 |NULL |NULL |NULL |
In the expected result above, the IDE column is shown three times that correspond to the three ocurrences in the FK_COLUMN for key 1, and one time for key 8 (this one have no children so must be shown in the resultset) of the first resultset.
Any help will be appreciated.

I didn't get the question at first. You don't need PIVOT, you need to show the path of each branch in separate columns. Anyway, depending on the number of levels you might need some dynamic sql
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select code, level lvl, sys_connect_by_path(code, '/') path, connect_by_isleaf lf, connect_by_root(ide) root_ide
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
)
select root_ide, path,
regexp_substr(path, '[^/]+', 1, 1),
regexp_substr(path, '[^/]+', 1, 2),
regexp_substr(path, '[^/]+', 1, 3),
regexp_substr(path, '[^/]+', 1, 4)
from tree where lf = 1;

if the number of levels is dynamic you need to use dynamic SQL anyway to create a query with dynamic columns.
There are at least 2 approaches how to create such a query:
1) Oracle with PIVOT (since 11g I suppose)
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select level lvl, t.ide, t.code
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
),
pivot_data as (
select * from tree pivot xml (max(code) lvl_code for lvl in (select distinct lvl from tree))
)
select ide,
extractvalue(lvl_xml,'/PivotSet/item[1]/column[2]') code_level1,
extractvalue(lvl_xml,'/PivotSet/item[2]/column[2]') code_level2,
extractvalue(lvl_xml,'/PivotSet/item[3]/column[2]') code_level3,
extractvalue(lvl_xml,'/PivotSet/item[4]/column[2]') code_level4
from pivot_data t order by 1;
(People say that EXTRACTVALUE might become deprecated and recommend using XMLTABLE, but I've never worked with this)
2) Oracle DECODE
with test_dynamic as (
select 1 ide, 1 id_level, 'A01' code, 0 fk_ide from dual
union all select 2, 2, 'A01', 1 from dual
union all select 3, 3, 'A01', 2 from dual
union all select 4, 4, 'A01', 3 from dual
union all select 5, 2, 'A02', 1 from dual
union all select 6, 2, 'A03', 1 from dual
union all select 7, 3, 'A01', 6 from dual
union all select 8, 1, 'A02', 0 from dual
),
tree as (
select level lvl, t.ide, t.code
from test_dynamic t
start with t.fk_ide = 0
connect by nocycle prior t.ide = t.fk_ide
)
select ide,
max(decode(lvl, 1, code, null)) code_level1,
max(decode(lvl, 2, code, null)) code_level2,
max(decode(lvl, 3, code, null)) code_level3,
max(decode(lvl, 4, code, null)) code_level4
from tree group by ide order by 1;

Related

Compute the child record value based on the parent value in Oracle PLSQL

I am trying to formulate a query in Oracle DB such that it computes the start_date value for the rows having it as null based on the numoddays , lvl (level), and the previous level's start_date column.
For an example:
Linenumber 3 and item 123:
Start_date = Start_date of previous level (2) + numofdays of current row
i.e Start_date = 03-FEB-23 01:54:00 PM + 1 = 04-FEB-23 01:54:00 PM
Notice that the non-null start date can be any arbitrary date and we have to compute the subsequent null rows for that item and the trailing non-null start_date wont follow the same pattern
ie Start_date of line number 2 is 03-FEB-23 01:54:00 PM which is not equal to 24-JAN-23 01:54:00 PM + 2 (from line number 2)
Sample table code:
select 1 LineNumber, 123 item, 1 lvl, 2 numofdays, sysdate start_date from dual
union all
select 2 , 123 , 2, 2, sysdate + 10 from dual
union all
select 3 , 123 , 3, 1, null from dual
union all
select 4 , 123 , 4, 3, null from dual
union all
select 5 , 123 , 5, 2, null from dual
union all
select 6 , 345 , 1, 1, sysdate+2 from dual
union all
select 7 , 345 , 2, 2, null from dual
union all
select 8 , 345 , 3, 1, null from dual
Desired Result:
select 1 LineNumber, 123 item, 1 lvl, 2 numofdays, sysdate start_date from dual
union all
select 2 , 123 , 2, 2, sysdate + 10 from dual
union all
select 3 , 123 , 3, 1, sysdate +10 +1 from dual
union all
select 4 , 123 , 4, 3, sysdate +10 +1+3 from dual
union all
select 5 , 123 , 5, 2, sysdate +10 +3+1+2 from dual
union all
select 6 , 345 , 1, 1, sysdate+2 from dual
union all
select 7 , 345 , 2, 2, sysdate +2 +2 from dual
union all
select 8 , 345 , 3, 1, sysdate +2 +2+1 from dual
Any help would be greatly appreciated
This is an ideal case for using MODEL clause. Your instruction to "... compute the start_date value for the rows having it as null based on the numoddays , lvl (level), and the previous level's start_date column." could be modeled just like that:
Select LINE_NUM, ITEM, LVL, NUM_OF_DAYS, START_DATE
From tbl
MODEL Partition By (ITEM)
Dimension By (LVL)
Measures (LINE_NUM, NUM_OF_DAYS, START_DATE)
Rules ( START_DATE[ANY] = CASE WHEN START_DATE[CV()] Is Not Null
THEN START_DATE[CV()]
ELSE START_DATE[CV() -1 ] + NUM_OF_DAYS[CV()] END )
In this case modeling is partitioned by ITEM column saying that for ANY (Dimension) LVL the START_DATE which Is Not Null stays as it is in that LVL (CV() - Current Value of LVL) and ELSE when START_DATE Is Null then take the date from previous LVL ( CV()-1 ) and add NUM_OF_DAYS from current LVL.
With Your sample data:
WITH
tbl (LINE_NUM, ITEM, LVL, NUM_OF_DAYS, START_DATE) AS
(
Select 1, 123 , 1, 2, SYSDATE From Dual Union All
Select 2, 123 , 2, 2, SYSDATE + 10 From Dual Union All
Select 3, 123 , 3, 1, null From Dual Union All
Select 4, 123 , 4, 3, null From Dual Union All
Select 5, 123 , 5, 2, null From Dual Union All
Select 6, 345 , 1, 1, SYSDATE+2 From Dual Union All
Select 7, 345 , 2, 2, null From Dual Union All
Select 8, 345 , 3, 1, null From Dual
)
... the result would be:
LINE_NUM ITEM LVL NUM_OF_DAYS START_DATE
---------- ---------- ---------- ----------- ----------
1 123 1 2 24-JAN-23
2 123 2 2 03-FEB-23
3 123 3 1 04-FEB-23
4 123 4 3 07-FEB-23
5 123 5 2 09-FEB-23
6 345 1 1 26-JAN-23
7 345 2 2 28-JAN-23
8 345 3 1 29-JAN-23
From Oracle 12, you can use MATCH_RECOGNIZE for row-by-row pattern matching:
SELECT LineNumber,
item,
lvl,
numofdays,
first_start_date + COALESCE(total_days, 0) AS start_date
FROM table_name
MATCH_RECOGNIZE(
PARTITION BY item
ORDER BY lvl
MEASURES
FIRST(start_date) AS first_start_date,
SUM(null_start_date.numofdays) AS total_days
ALL ROWS PER MATCH
PATTERN (not_null_start_date null_start_date*)
DEFINE
not_null_start_date AS start_date IS NOT NULL,
null_start_date AS start_date IS NULL
)
Which, for the sample data:
CREATE TABLE table_name (LineNumber, item, lvl, numofdays, start_date) AS
select 1, 123, 1, 2, sysdate from dual union all
select 2, 123, 2, 2, sysdate + 10 from dual union all
select 3, 123, 3, 1, null from dual union all
select 4, 123, 4, 3, null from dual union all
select 5, 123, 5, 2, null from dual union all
select 6, 345, 1, 1, sysdate+2 from dual union all
select 7, 345, 2, 2, null from dual union all
select 8, 345, 3, 1, null from dual;
Outputs:
LINENUMBER
ITEM
LVL
NUMOFDAYS
START_DATE
1
123
1
2
2023-01-24 18:23:54
2
123
2
2
2023-02-03 18:23:54
3
123
3
1
2023-02-04 18:23:54
4
123
4
3
2023-02-07 18:23:54
5
123
5
2
2023-02-09 18:23:54
6
345
1
1
2023-01-26 18:23:54
7
345
2
2
2023-01-28 18:23:54
8
345
3
1
2023-01-29 18:23:54
fiddle

Having a node at level N

I want to write the code by Having a node at level N (for example 3) of the tree, find all members up to level M (for example 6).How can I write this code? what is the best idea for this problem?
I am new in oracle , please explain me and help me for resolve this problem .
CREATE TABLE tree (
node_id INT,
parent_id INT
)
INSERT INTO
tree
SELECT 1, NULL FROM DUAL
UNION ALL SELECT 2, 1 FROM DUAL
UNION ALL SELECT 3, 1 FROM DUAL
UNION ALL SELECT 4, 2 FROM DUAL
UNION ALL SELECT 5, 2 FROM DUAL
UNION ALL SELECT 6, 3 FROM DUAL
UNION ALL SELECT 7, 3 FROM DUAL
UNION ALL SELECT 8, 4 FROM DUAL
UNION ALL SELECT 9, 4 FROM DUAL
UNION ALL SELECT 10, 5 FROM DUAL
UNION ALL SELECT 11, 5 FROM DUAL
UNION ALL SELECT 12, 6 FROM DUAL
UNION ALL SELECT 13, 6 FROM DUAL
UNION ALL SELECT 14, 7 FROM DUAL
UNION ALL SELECT 15, 7 FROM DUAL
15 rows affected
SELECT
node_id, parent_id, level
FROM
tree
WHERE
level < 3
CONNECT BY
PRIOR node_id = parent_id
START WITH
parent_id = 3
ORDER BY
level, node_id
NODE_ID
PARENT_ID
LEVEL
6
3
1
7
3
1
12
6
2
13
6
2
14
7
2
15
7
2
fiddle

hierarchical query without side rows

I have "id-parent_id related" data, like this:
1
/ \
/ \
2 4
/
/
3
I have the code, that returns data for all rows related to particular (related to condition in the start with clause) tree - in both sides ("up" and "down"), for example:
with
temp_cte(id, parent_id) as
(select 1, null from dual
union all
select 2, 1 from dual
union all
select 3, 2 from dual
union all
select 4, 1 from dual
union all
select 5, null from dual)
select *
from temp_cte t
connect by nocycle (prior id = parent_id) or (prior parent_id = id)
start with t.id = 2
order by id
How do i get data without "side" ("right"/"left") rows?
e.g. for the drawn above -
I need data without 4 when I start with 2 or 3,
and I need data without 2 and 3 when I start with 4
(if start with 1, I still want the full tree)
This is because of OR predicate in CONNECT BY: your query traverses in both directions, so on the second step of CONNECT BY you'll have all the childs of the parent and finally all the tree.
You need to split the query into union of two directions.
with
temp_cte(id, parent_id) as
(select 1, null from dual
union all
select 2, 1 from dual
union all
select 3, 2 from dual
union all
select 4, 1 from dual
union all
select 5, null from dual
)
select id, parent_id
from temp_cte t
where level > 1
connect by nocycle (prior id = parent_id)
start with t.id = 2
union all
select id, parent_id
from temp_cte t
connect by (prior parent_id = id)
start with t.id = 2
order by id
ID | PARENT_ID
-: | --------:
1 | null
2 | 1
3 | 2
db<>fiddle here

recursive query to find the root parent in oracle

I am trying to figure out the root parent in a table with hierarchical data. The following example works as expected but I need to do something extra. I want to avoid the query to ignore null id1 and show the (root parent - 1) if the root parent is null.
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
)
select connect_by_root id1 as id, id1 as root_parent_id
from table_a
where connect_by_isleaf = 1
connect by child_id = prior id1
order by id 1
This brings up the following data
4 4
6 5
7 5
8 5
5 5
3 null
null null
2 null
1 null
what I want is
3 1
1 1
2 1
4 4
7 5
8 5
5 5
6 5
is it possible?
Thanks for the help
Using a recursive CTE you can do:
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
),
n (s, e) as (
select id1 as s, child_id as e from table_a where id1 not in
(select child_id from table_a
where id1 is not null and child_id is not null)
union all
select n.s, a.child_id
from n
join table_a a on a.id1 = n.e
)
select
coalesce(e, s) as c, s
from n
order by s
Result:
C S
- -
3 1
1 1
2 1
4 4
5 5
7 5
8 5
6 5
As a side note, "Recursive CTEs" are more flexible than the old-school CONNECT BY.
This looks like it works but it may be incorrect, as I do not quite understand the logic behind choosing 1 for this, looks arbitrary to me, not much like real data will be.
As Hogan has asked already, it would be helpful if you could perhaps provide an explanation or an expanded data set to test this hierarchy.
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
)
select connect_by_root id1 as id, id1 as root_parent_id
from table_a
where connect_by_isleaf = 1 and connect_by_root id1 is not null
connect by nocycle child_id = prior nvl(id1, 1)
order by 2, 1;
Sample execution:
FSITJA#dbd01 2019-07-19 13:51:13> with table_a ( id1, child_id ) as (
2 select null, 1 from dual union all
3 select 1, 2 from dual union all
4 select 2, 3 from dual union all
5 select 3, NULL from dual union all
6 select 4, NULL from dual union all
7 select 5, 6 from dual union all
8 select 6, 7 from dual union all
9 select 7, 8 from dual union all
10 select 8, NULL from dual
11 )
12 select connect_by_root id1 as id, id1 as root_parent_id
13 from table_a
14 where connect_by_isleaf = 1 and connect_by_root id1 is not null
15 connect by nocycle child_id = prior nvl(id1, 1)
16 order by 2, 1;
ID ROOT_PARENT_ID
---------- --------------
1 1
2 1
3 1
4 4
5 5
6 5
7 5
8 5
8 rows selected.

Bigquery aggregating into array based on id and id_type

I have a table that looks similar to this:
WITH
table AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT
object_id,
type_id,
type_level
FROM
table
Now I am trying to create three new columns type_level_0_array,type_level_1_array,type_level_2_array for each object and aggregate the type_id of corresponding level of types into those array (I am not looking for string separated by commas).
So my resultant table should look like the following:
+----+--------------------+--------------------+--------------------+
| id | type_level_0_array | type_level_1_array | type_level_2_array |
+----+--------------------+--------------------+--------------------+
| 1 | 2 | 24,23 | 234 |
+----+--------------------+--------------------+--------------------+
| 2 | 3,4 | 34,46 | 465,349 |
+----+--------------------+--------------------+--------------------+
Is there any way to accomplish that?
Update:
Although it seems that my type_id has certain pattern e.g. level 0 types are of 1 length, level 1 types are of 2 length and so on, in my real dataset there is no such pattern. The identification of level is solely possible by looking at type_level of any row.
Below is for BigQuery Standard SQL
#standardSQL
SELECT object_id,
ARRAY_AGG(DISTINCT IF(type_level = 0, type_id, NULL) IGNORE NULLS) AS type_level_0_array,
ARRAY_AGG(DISTINCT IF(type_level = 1, type_id, NULL) IGNORE NULLS) AS type_level_1_array,
ARRAY_AGG(DISTINCT IF(type_level = 2, type_id, NULL) IGNORE NULLS) AS type_level_2_array
FROM `project.dataset.table`
GROUP BY object_id
You can test, play with above using sample data from your question as in below
#standardSQL
WITH `project.dataset.table` AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT object_id,
ARRAY_AGG(DISTINCT IF(type_level = 0, type_id, NULL) IGNORE NULLS) AS type_level_0_array,
ARRAY_AGG(DISTINCT IF(type_level = 1, type_id, NULL) IGNORE NULLS) AS type_level_1_array,
ARRAY_AGG(DISTINCT IF(type_level = 2, type_id, NULL) IGNORE NULLS) AS type_level_2_array
FROM `project.dataset.table`
GROUP BY object_id
with result
Row object_id type_level_0_array type_level_1_array type_level_2_array
1 1 2 24 234
23
2 2 4 34 349
3 46 465
Try this. Works for me.
Bigquery won't let you create an array with Nulls in them, which is why the IGNORE NULLS is required.
EDIT: I've updated the code to be based off the type_level column
WITH table
AS (
SELECT 1 object_id, 234 type_id, 2 type_level UNION ALL
SELECT 1, 23, 1 UNION ALL
SELECT 1, 24, 1 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 1, 2, 0 UNION ALL
SELECT 2, 34, 1 UNION ALL
SELECT 2, 46, 1 UNION ALL
SELECT 2, 465, 2 UNION ALL
SELECT 2, 349, 2 UNION ALL
SELECT 2, 4, 0 UNION ALL
SELECT 2, 3, 0 )
SELECT
ARRAY_AGG(CASE WHEN type_level = 0 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_0_array
, ARRAY_AGG(CASE WHEN type_level = 1 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_1_array
, ARRAY_AGG(CASE WHEN type_level = 2 THEN type_id ELSE NULL END IGNORE NULLS) AS type_level_2_array
FROM
table