Recursive Insert using connect by clause - sql

I have hierarchical data (right) in table in following manner which creates Hierarchy as shown in left. Tables are kept in oracle 11g.
TREE Hierarchy Tree Table
-------------- Element Parent
------ ------
P0 P0
P1 P1 P0
P11 P2 P0
C111 P11 P1
C112 P12 P1
P12 P21 P2
C121 P22 P2
C122 C111 P11
P2 C112 P11
P21 C121 P12
C211 C122 P12
C212 C211 P21
P22 C212 P21
C221 C221 P22
C222 C222 P22
My data table has values as follows. It contains values for all leaf nodes.
Data Table
Element Value
C111 3
C112 3
C121 3
C122 3
C211 3
C212 3
C221 3
C222 3
P11 6
I need to generate insert statement, preferably single insert statement which will insert rows in data table based on sum of values of the children.
Please note we need to calculate sum for only those parents whose value is not present in data table.
Data Table (Expected After Insert)
Element Value
C111 3
C112 3
C121 3
C122 3
C211 3
C212 3
C221 3
C222 3
P11 6
-- Rows to insert
P12 6
P21 6
P22 6
P1 12
P2 12
P0 24

If all leaf nodes are at the same height (here lvl=4), you can write a simple CONNECT BY query with a ROLLUP:
SQL> SELECT lvl0,
2 regexp_substr(path, '[^/]+', 1, 2) lvl1,
3 regexp_substr(path, '[^/]+', 1, 3) lvl2,
4 SUM(VALUE) sum_value
5 FROM (SELECT sys_connect_by_path(t.element, '/') path,
6 connect_by_root(t.element) lvl0,
7 t.element, d.VALUE, LEVEL lvl
8 FROM tree t
9 LEFT JOIN DATA d ON d.element = t.element
10 START WITH t.PARENT IS NULL
11 CONNECT BY t.PARENT = PRIOR t.element)
12 WHERE VALUE IS NOT NULL
13 AND lvl = 4
14 GROUP BY lvl0, ROLLUP(regexp_substr(path, '[^/]+', 1, 2),
15 regexp_substr(path, '[^/]+', 1, 3));
LVL0 LVL1 LVL2 SUM_VALUE
---- ----- ----- ----------
P0 P1 P11 6
P0 P1 P12 6
P0 P1 12
P0 P2 P21 6
P0 P2 P22 6
P0 P2 12
P0 24
The insert would look like:
INSERT INTO data (element, value)
(SELECT coalesce(lvl2, lvl1, lvl0), sum_value
FROM <query> d_out
WHERE NOT EXISTS (SELECT NULL
FROM data d_in
WHERE d_in.element = coalesce(lvl2, lvl1, lvl0)));
If the height of the leaf nodes is unknown/unbounded this gets more hairy. The above approach wouldn't work since ROLLUP needs to know exactly how many columns are to be considered.
In that case, you could use the tree structure in a self-join :
SQL> WITH HIERARCHY AS (
2 SELECT t.element, path, VALUE
3 FROM (SELECT sys_connect_by_path(t.element, '/') path,
4 connect_by_isleaf is_leaf, ELEMENT
5 FROM tree t
6 START WITH t.PARENT IS NULL
7 CONNECT BY t.PARENT = PRIOR t.element) t
8 LEFT JOIN DATA d ON d.element = t.element
9 AND t.is_leaf = 1
10 )
11 SELECT h.element, SUM(elements.value)
12 FROM HIERARCHY h
13 JOIN HIERARCHY elements ON elements.path LIKE h.path||'/%'
14 WHERE h.VALUE IS NULL
15 GROUP BY h.element
16 ORDER BY 1;
ELEMENT SUM(ELEMENTS.VALUE)
------- -------------------
P0 24
P1 12
P11 6
P12 6
P2 12
P21 6
P22 6

Here is another option using the SQL MODEL clause. I've taken some hints from what Vincent has done in his answer (use of regexp_subsr) to simplify my code.
The first part, within the WITH clause just rejigs the data and extracts out the hierarchy at each level.
The model clause, at the end of the query, brings the data up from the lowest levels. This will need additional columns added if there are more than four levels but should work no matter at what level the values are held.
I'm not entirely sure that this will work in all circumstances since I'm not that experienced with the MODEL clause but it does at least seem to work in this case.
with my_hierarchy_data as (
select
element,
value,
path,
parent,
lvl0,
regexp_substr(path, '[^/]+', 1, 2) as lvl1,
regexp_substr(path, '[^/]+', 1, 3) as lvl2,
regexp_substr(path, '[^/]+', 1, 4) as lvl3
from (
select
element,
value,
parent,
sys_connect_by_path(element, '/') as path,
connect_by_root element as lvl0
from
tree
left outer join data using (element)
start with parent is null
connect by prior element = parent
order siblings by element
)
)
select
element,
value,
path,
parent,
new_value,
lvl0,
lvl1,
lvl2,
lvl3
from my_hierarchy_data
model
return all rows
partition by (lvl0)
dimension by (lvl1, lvl2, lvl3)
measures(element, parent, value, value as new_value, path)
rules sequential order (
new_value[lvl1, lvl2, null] = sum(value)[cv(lvl1), cv(lvl2), lvl3 is not null],
new_value[lvl1, null, null] = sum(new_value)[cv(lvl1), lvl2 is not null, null],
new_value[null, null, null] = sum(new_value)[lvl1 is not null, null, null]
)
The insert statement you can use is
INSERT INTO data (elelment, value)
select element, newvalue
from <the_query>
where value is null;

Related

Dynamically selecting the column to select from the row itself in SQL

I have a SQL Server table with some data as follows. The number of P columns are fixed but there will be too many columns. There will be multiple columns in the fashion like S1, S2 etc
Id
SelectedP
P1
P2
P3
P4
P5
1
P2
3
8
4
15
7
2
P1
0
2
6
0
3
3
P3
1
15
2
1
11
4
P4
3
4
6
2
4
I need to write a SQL statement which can get the below result. Basically which column that needs to be selected from each row depends upon the SelectedP value in that row itself. The SelectedP contains the column to select for each row.
Id
SelectedP
Selected-P-Value
1
P2
8
2
P1
0
3
P3
2
4
P4
2
Thanks in advance.
You just need a CASE expression...
SELECT
id,
SelectedP,
CASE SelectedP
WHEN 'P1' THEN P1
WHEN 'P2' THEN P2
WHEN 'P3' THEN P3
WHEN 'P4' THEN P4
WHEN 'P5' THEN P5
END
AS SelectedPValue
FROM
yourTable
This will return NULL for anything not mentioned in the CASE expression.
EDIT:
An option with just a little less typing...
SELECT
id, SelectedP, val
FROM
yourTable AS pvt
UNPIVOT
(
val FOR P IN
(
P1,
P2,
P3,
P4,
P5
)
)
AS unpvt
WHERE
SelectedP = P
NOTE: If the value of SelectedP doesn't exist in the UNPIVOT, then the row will not appear at all (unlike the CASE expression which will return a NULL)
Demo: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=b693738aac0b594cf37410ee5cb15cf5
EDIT 2:
I don't know if this will perform much worse than the 2nd option, but this preserves the NULL behaviour.
(The preferred option is still to fix your data-structure.)
SELECT
id, SelectedP, MAX(CASE WHEN SelectedP = P THEN val END) AS val
FROM
yourTable AS pvt
UNPIVOT
(
val FOR P IN
(
P1,
P2,
P3,
P4,
P5
)
)
AS unpvt
GROUP BY
id, SelectedP
Demo : https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=f3f64d2fb6e11fd24d1addbe1e50f020

Oracle SQL query to get comma-separated string in single query

I have a data which is represented as below:
Name parent_unit child_unit
--------------------------------
aa 1 0
aa,bc 1 1
aa,de 1 2
bb 2 0
bb,ab 2 1
I have a query as follows which has to be tweaked to get parent and child names respectively
select u.name,u.id, lk.name as parentName, lk.name as childName
from users u, users_unit uu, lk_unit lk
where u.id = uu.user_id
and uu.parent_unit = lk.parent_unit
and uu.child_unit = lk.child_unit
My output should look as follows:
name id parentName childName
----------------------------
X 1 aa
Y 2 aa bc
Z 3 bb ab
I basically want to split the lk.name based on seperator (,) and 1st string before seperator is parentName and 2nd string after seperator is childName. If there are no seperators then its just the parentName.
You can use regexp_substr):
select regexp_substr(name, '[^,]+', 1, 1) as parent_name,
regexp_substr(name, '[^,]+', 1, 2)
Here is a db<>fiddle.

Is there a way to create a groupID for a recursive CTE in SSMS?

I'm building a query that outputs an ownership hierarchy for each root in my database. I'm using a recursive CTE with success in that I can achieve the following data output currently:
rootID RootName RelatedName
1 ABA GPS
1 ABA PIG
1 ABA BBY
1 ABA PIG
2 PIG DDS
2 PIG GPS
What I'm trying to achieve is a group ID column in which the data may look like this:
GroupID rootID RootName RelatedName
100 1 ABA GPS
100 1 ABA PIG
100 1 ABA BBY
100 1 ABA PIG
100 2 PIG DDS
100 2 PIG GPS
and likewise for group 200, 300,...etc. for each tree. What part of the recursive CTE can code be injected such to achieve the above result?
;WITH cte_Rel AS (
SELECT
<columns>
FROM #RawRel r
WHERE 1 = 1
AND <initial Conditions>
UNION ALL
SELECT
<Columns>
FROM #RawRel r
JOIN cte_Rel c ON r.RootName = c.RelatedName
)
SELECT DISTINCT * FROM cte_Rel
OPTION (MAXRECURSION 100)
You can add a row number to the anchor part of the recusive CTE. Multiply by 100 and repeat the same column in the second part of the CTE.
Fiddle in case you prefer interactive code.
Sample data
Without your actual query and sample input data it is hard to perfectly replicate your current output so I generated my own sample data.
create table RelData
(
ParentId int,
Id int,
Name nvarchar(3)
);
insert into RelData (ParentId, Id, Name) values
(null, 1, 'A00'), -- tree A
(1, 2, 'A10'),
(2, 3, 'A11'),
(2, 4, 'A12'),
(1, 5, 'A20'),
(5, 6, 'A21'),
(null, 7, 'B00'), -- tree B
(7, 8, 'B10'),
(8, 9, 'B11');
Solution
WITH cte_Rel AS (
SELECT row_number() over(order by rd.Id) * 100 as TreeId, -- number to roots and multiply the root number by 100
rd.Id, rd.Name, rd.ParentId, convert(nvarchar(3), null) as ParentName
FROM RelData rd
WHERE rd.ParentId is null
UNION ALL
SELECT c.TreeId, -- repeat the tree number
rd.Id, rd.Name, rd.ParentId, c.name
FROM RelData rd
JOIN cte_Rel c ON rd.ParentId = c.Id
)
SELECT c.TreeId, c.ParentId, c.ParentName, c.Name
FROM cte_Rel c
where c.ParentId is not null
order by c.ParentId;
Result
TreeId ParentId ParentName Name
------ -------- ---------- ----
100 1 A00 A10
100 1 A00 A20
100 2 A10 A11
100 2 A10 A12
100 5 A20 A21
200 7 B00 B10
200 8 B10 B11

split 1 record into 4 records(transpose row into column) in oracle

I have one record in table and i want to split this record into 4 records based on condition(if is Multirecord=1) and Q has null so no new record needed.(if Q3 is null then 3rd row not required)
and insert into new table.Details are:
Table: emp
UID name is_Multirecord Q1 P1 T1 .... Q4 P4 T4
1 xyz 1 10 $200 15-03-2019 40 $500 18-03-2019
Output in table EMp_split
record_no UID Nae is_Multi Q P T
1 1 xyz 1 10 $200 15-03-2019
2 1 xyz 1 20 $300 16-03-2019
.....
4. 1 XYZ 1 40 $500 18-03-2019
Please assist.Let me know if any other information required.
You can do the following:
SELECT UID as record_no, name as Nae, is_multirecord as is_Multi, Q1 as Q, P1 as P, T1 as T
WHERE is_multirecord = 1
UNION
SELECT UID as record_no, name as Nae, is_multirecord as is_Multi, Q2 as Q, P2 as P, T2 as T
WHERE is_multirecord = 1
UNION
SELECT UID as record_no, name as Nae, is_multirecord as is_Multi, Q3 as Q, P3 as P, T3 as T
WHERE is_multirecord = 1
UNION
SELECT UID as record_no, name as Nae, is_multirecord as is_Multi, Q4 as Q, P4 as P, T4 as T
WHERE is_multirecord = 1

Postgresql copy data within the tree table

I have table with tree structure, columns are id, category, parent_id
Now I need a copy a node and its child's to a another node, while copying, the category must be same, but with new id and parent_id..
My input will be node to copy & destination node to copy
I have explained the tree structure in the image file..
i need a function to do so..,
PostgreSQL version 9.1.2
Column | Type | Modifiers
-----------+---------+-------------------------------------------------
id | integer | not null default nextval('t1_id_seq'::regclass)
category | text |
parent_id | integer |
Indexes:
"t1_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"fk_t1_1" FOREIGN KEY (parent_id) REFERENCES t1(id)
Referenced by:
TABLE "t1" CONSTRAINT "fk_t1_1" FOREIGN KEY (parent_id) REFERENCES t1(id)
(tested under PostgreSQL 8.4.3)
The following query assigns new IDs to the sub-tree under node 4 (see the nextval) and then finds the corresponding new IDs of parents (see the LEFT JOIN).
WITH RECURSIVE CTE AS (
SELECT *, nextval('t1_id_seq') new_id FROM t1 WHERE id = 4
UNION ALL
SELECT t1.*, nextval('t1_id_seq') new_id FROM CTE JOIN t1 ON CTE.id = t1.parent_id
)
SELECT C1.new_id, C1.category, C2.new_id new_parent_id
FROM CTE C1 LEFT JOIN CTE C2 ON C1.parent_id = C2.id
Result (on your test data):
new_id category new_parent_id
------ -------- -------------
9 C4
10 C5 9
11 C6 9
12 C7 10
Once you have that, it's easy to insert it back to the table, you just have to be careful to reconnect the sub-tree root with the appropriate parent (8 in this case, see the COALESCE(new_parent_id, 8)):
INSERT INTO t1
SELECT new_id, category, COALESCE(new_parent_id, 8) FROM (
WITH RECURSIVE CTE AS (
SELECT *, nextval('t1_id_seq') new_id FROM t1 WHERE id = 4
UNION ALL
SELECT t1.*, nextval('t1_id_seq') new_id FROM CTE JOIN t1 ON CTE.id = t1.parent_id
)
SELECT C1.new_id, C1.category, C2.new_id new_parent_id
FROM CTE C1 LEFT JOIN CTE C2 ON C1.parent_id = C2.id
) Q1
After that, the table contains the following data:
new_id category new_parent_id
------ -------- -------------
1 C1
2 C2 1
3 C3 1
4 C4 2
5 C5 4
6 C6 4
7 C7 5
8 C8 3
9 C4 8
10 C5 9
11 C6 9
12 C7 10