Oracle Self Join to retrieve recursive data - oracle

I have the following schema and data
segmentid paramid paramvalue
103 1 4418
101 1 4834
102 1 5587
104 1 7413
105 1 9965
106 1 7421
107 1 7782
103 2 1990|2000
102 2 2005|2010
105 2 1985|1990
104 2 1981
101 3 F
103 3 M
101 4 M
103 4 S
102 5 SUKKHUR
105 5 LAHORE
106 5 HYDRABAD
107 5 KHAIRPUR
101 5 ISLAMABAD
Now i will have inputs of different param values like Karachi M and date of birth range. I want to retrieve only that segment id whose all parameters are returned to be true.
If any parameter is failed the segment should fail.
Below is my idea but its returning if any paramvalue is true as ive used or but when i and the data is not retrieved.
select tpv.* from tblsegment ts , tblsegmentparameter tsp , tblsegmentparamvalue tpv
where ts.segmentid = tpv.segmentid and tsp.parameterid = tpv.paramid
and
(
(lower(tsp.paramname) like 'city' and tpv.paramvalue = 'KARACHI' and tsp.parameterid = tpv.paramid)
or
(lower(tsp.paramname) like 'gender' and tpv.paramvalue = 'M')
or
(lower(tsp.paramname) like 'maritalstatus' and tpv.paramvalue = 'S')
or
(lower(tsp.paramname) like 'product' and tpv.paramvalue = (select distinct ta.productid from tblcustchannelacct ta ,tblcustomer tc, tblaccount tta
where ta.relationship_id = '5327016301000015=5311' and ta.channel_id = '0001' and ta.account_id = tta.account_id and ta.customer_id = tc.customerid )
)
or
(lower(tsp.paramname) like 'dob' and
(
(
to_char( '1985') between
to_char( REGEXP_SUBSTR ( tpv.paramvalue, '^[^|]*'))
and
to_char(REGEXP_SUBSTR( tpv.paramvalue, '*[^|]*$'))
) or
(
to_char( '1986') = tpv.paramvalue
)
)
)
)
order by tsp.sortorder;

SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE tblsegmentparamvalue ( segmentid, paramid, paramvalue ) AS
SELECT 103, 1, '4418' FROM DUAL
UNION ALL SELECT 101, 1, '4834' FROM DUAL
UNION ALL SELECT 102, 1, '5587' FROM DUAL
UNION ALL SELECT 104, 1, '7413' FROM DUAL
UNION ALL SELECT 105, 1, '9965' FROM DUAL
UNION ALL SELECT 106, 1, '7421' FROM DUAL
UNION ALL SELECT 107, 1, '7782' FROM DUAL
UNION ALL SELECT 103, 2, '1990|2000' FROM DUAL
UNION ALL SELECT 102, 2, '2005|2010' FROM DUAL
UNION ALL SELECT 105, 2, '1985|1990' FROM DUAL
UNION ALL SELECT 104, 2, '1981' FROM DUAL
UNION ALL SELECT 101, 3, 'F' FROM DUAL
UNION ALL SELECT 103, 3, 'M' FROM DUAL
UNION ALL SELECT 101, 4, 'M' FROM DUAL
UNION ALL SELECT 103, 4, 'S' FROM DUAL
UNION ALL SELECT 102, 5, 'SUKKHUR' FROM DUAL
UNION ALL SELECT 105, 5, 'LAHORE' FROM DUAL
UNION ALL SELECT 106, 5, 'HYDRABAD' FROM DUAL
UNION ALL SELECT 107, 5, 'KHAIRPUR' FROM DUAL
UNION ALL SELECT 101, 5, 'ISLAMABAD' FROM DUAL;
Query 1:
WITH segments AS (
SELECT segmentid
FROM tblsegmentparamvalue
GROUP BY segmentid
HAVING ( COUNT( CASE WHEN paramid = 1 THEN 1 END ) = 0
OR COUNT( CASE WHEN paramid = 1 AND paramvalue = '4834' THEN 1 END ) > 0 )
AND ( COUNT( CASE WHEN paramid = 2 THEN 1 END ) = 0
OR COUNT( CASE WHEN paramid = 2 AND '1985' BETWEEN SUBSTR( paramvalue, 1, 4 ) AND SUBSTR( paramvalue, -4 ) THEN 1 END ) > 0 )
AND ( COUNT( CASE WHEN paramid = 3 THEN 1 END ) = 0
OR COUNT( CASE WHEN paramid = 3 AND paramvalue = 'F' THEN 1 END ) > 0 )
AND ( COUNT( CASE WHEN paramid = 4 THEN 1 END ) = 0
OR COUNT( CASE WHEN paramid = 4 AND paramvalue = 'M' THEN 1 END ) > 0 )
AND ( COUNT( CASE WHEN paramid = 5 THEN 1 END ) = 0
OR COUNT( CASE WHEN paramid = 5 AND paramvalue = 'ISLAMABAD' THEN 1 END ) > 0 )
)
SELECT t.*
FROM tblsegmentparamvalue t
INNER JOIN
segments s
ON ( t.segmentid = s.segmentid )
ORDER BY
t.segmentid,
paramid
Results:
| SEGMENTID | PARAMID | PARAMVALUE |
|-----------|---------|------------|
| 101 | 1 | 4834 |
| 101 | 3 | F |
| 101 | 4 | M |
| 101 | 5 | ISLAMABAD |

This is an example how to get results for three paramters:
select segmentid, paramid, paramvalue
from (
select tpv.*, count(distinct paramid) over (partition by tpv.segmentid) cnt
from tblsegmentparamvalue tpv
join tblsegment ts on ts.segmentid = tpv.segmentid
join tblsegmentparameter tsp on tsp.parameterid = tpv.paramid
where (lower(tsp.paramname) like 'city' and tpv.paramvalue = 'KARACHI')
or (lower(tsp.paramname) like 'gender' and tpv.paramvalue = 'M')
or (lower(tsp.paramname) like 'martialstatus' and tpv.paramvalue = 'S') )
where cnt = 3
SQLFiddle demo
Add rest of parameters (dob, product) as you did it in your query and change last line to where cnt=5 to get full results.

Related

Custom aggregate function to collapse vertices to SDO_GEOMETRY

I have multi-part polyline vertices stored as individual rows in an Oracle 18c table.
ASSET_ID PART_NUM VERTEX_NUM X Y M
---------- ---------- ---------- ---------- ---------- ----------
001 1 1 0 5 0
001 1 2 10 10 11.18
001 1 3 30 0 33.54
001 2 1 50 10 33.54
001 2 2 60 10 43.54
DDL db<>fiddle
CTE db<>fiddle
I want to convert the vertices to a multi-part SDO_GEOMETRY polyline (collapsed into a single row).
I've tried a few different ways of doing that (i.e. listagg and PL/SQL block). Additionally, as a learning exercise, I would also like to explore creating a custom aggregate function as a solution.
It might look like this:
select
asset_id,
sdo_geometry(partition by id, part num, vertex order, x, y, m, gtype, srid) as sdo_geom
from
vertices
group by
asset_id
Output:
ASSET_ID: 001
SDO_GEOM: SDO_GEOMETRY(3306, 26917, NULL, MDSYS.SDO_ELEM_INFO_ARRAY(1, 2, 1, 10, 2, 1), MDSYS.SDO_ORDINATE_ARRAY(0, 5, 0, 10, 10, 11.18, 30, 0, 33.54, 50, 10, 33.54, 60, 10, 43.54))
--SDO_GEOMETRY docs: https://docs.oracle.com/en/database/oracle/oracle-database/19/spatl/spatial-datatypes-metadata.html
--Info about multi-part lines: https://community.oracle.com/tech/apps-infra/discussion/4497547/sdo-geometry-output-how-to-know-if-geometry-is-multi-part
Is there a way to create a custom aggregate function to do that?
Create a type to store the point:
CREATE TYPE PointLRS AS OBJECT(
X NUMBER,
Y NUMBER,
M NUMBER
);
Then create a user-defined aggregation type:
CREATE TYPE Line3DAggType AS OBJECT(
ordinates SDO_ORDINATE_ARRAY,
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT Line3DAggType
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT Line3DAggType,
point IN PointLRS
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT Line3DAggType,
returnValue OUT SDO_GEOMETRY,
flags IN NUMBER
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT Line3DAggType,
ctx IN OUT Line3DAggType
) RETURN NUMBER
);
/
CREATE OR REPLACE TYPE BODY Line3DAggType
IS
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT Line3DAggType
) RETURN NUMBER
IS
BEGIN
ctx := Line3DAggType( SDO_ORDINATE_ARRAY() );
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT Line3DAggType,
point IN PointLRS
) RETURN NUMBER
IS
BEGIN
IF point IS NOT NULL
AND point.X IS NOT NULL
AND point.Y IS NOT NULL
AND point.M IS NOT NULL
THEN
self.ordinates.EXTEND(3);
self.ordinates(self.ordinates.COUNT - 2) := point.X;
self.ordinates(self.ordinates.COUNT - 1) := point.Y;
self.ordinates(self.ordinates.COUNT - 0) := point.M;
END IF;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT Line3DAggType,
returnValue OUT SDO_GEOMETRY,
flags IN NUMBER
) RETURN NUMBER
IS
BEGIN
IF self.ordinates.COUNT > 0 THEN
returnValue := SDO_GEOMETRY(
3302,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1,2,1),
self.ordinates
);
ELSE
returnValue := NULL;
END IF;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT Line3DAggType,
ctx IN OUT Line3DAggType
) RETURN NUMBER
IS
BEGIN
FOR i IN 1 .. ctx.ordinates.COUNT LOOP
self.ordinates.EXTEND;
self.ordinates(self.ordinates.COUNT) := ctx.ordinates(i);
END LOOP;
RETURN ODCIConst.SUCCESS;
END;
END;
/
Then define a custom aggregation function:
CREATE FUNCTION Line3DAgg( point PointLRS )
RETURN SDO_GEOMETRY
PARALLEL_ENABLE AGGREGATE USING Line3DAggType;
/
Then you can aggregate the points for each part into a line and then concatenate the lines:
SELECT asset_id,
SDO_AGGR_LRS_CONCAT(SDOAGGRTYPE(part, 0.005)) AS geom
FROM (
SELECT asset_id,
part_num,
Line3DAgg(PointLRS(x, y, m)) AS part
FROM vertices
GROUP BY asset_id, part_num
)
GROUP BY asset_id
db<>fiddle here
This builds the individual linestrings.
with cte as (
select 001 as asset_id, 1 as part_num,1 as vertex_num,0 as x,5 as y, 0 as m from dual union all
select 001 as asset_id, 1 as part_num,2 as vertex_num,10 as x,10 as y,11.18 as m from dual union all
select 001 as asset_id, 1 as part_num,3 as vertex_num,30 as x,0 as y, 33.54 as m from dual union all
select 001 as asset_id, 2 as part_num,1 as vertex_num,50 as x,10 as y,33.54 as m from dual union all
select 001 as asset_id, 2 as part_num,2 as vertex_num,60 as x,10 as y,43.54 as m from dual
)
SELECT asset_id,
part_num,
mdsys.sdo_geometry(
3302,
null,
null,
mdsys.sdo_elem_info_array(1,2,1),
CAST(MULTISET( select case when r.rin = 1 then x
when r.rin = 2 then y
when r.rin = 3 then m
end
from cte b,
(select level rin from dual connect by level < 4) r
where b.asset_id = a.asset_id
and b.part_num = a.part_num
order by b.vertex_num, r.rin
) as mdsys.sdo_ordinate_array
)
) as geom
from cte a
group by asset_id, part_num
order by part_num;
Note how the X, Y and M ordinates are "serialised" into an array (of type mdsys.sdo_ordinate_array) using the MULTISET operator.
Result is:
ASSET_ID PART_NUM GEOM
---------- ---------- ----
1 1 SDO_GEOMETRY(3002, NULL, NULL, SDO_ELEM_INFO_ARRAY(1, 2, 1), SDO_ORDINATE_ARRAY(0, 5, 0, 10, 10, 11.18, 30, 0, 33.54))
1 2 SDO_GEOMETRY(3002, NULL, NULL, SDO_ELEM_INFO_ARRAY(1, 2, 1), SDO_ORDINATE_ARRAY(50, 10, 33.54, 60, 10, 43.54))
Creating a multilinestring involves aggregating the linestrings using the asset_id attribute.
with cte as (
select 001 as asset_id, 1 as part_num,1 as vertex_num,0 as x,5 as y, 0 as m from dual union all
select 001 as asset_id, 1 as part_num,2 as vertex_num,10 as x,10 as y,11.18 as m from dual union all
select 001 as asset_id, 1 as part_num,3 as vertex_num,30 as x,0 as y, 33.54 as m from dual union all
select 001 as asset_id, 2 as part_num,1 as vertex_num,50 as x,10 as y,33.54 as m from dual union all
select 001 as asset_id, 2 as part_num,2 as vertex_num,60 as x,10 as y,43.54 as m from dual
)
SELECT asset_id,
SDO_AGGR_UNION(SDOAGGRTYPE(geom,0.005)) as mGeom
FROM (SELECT asset_id,
part_num,
mdsys.sdo_geometry(
3302,
null,
null,
mdsys.sdo_elem_info_array(1,2,1),
CAST(MULTISET( select case when r.rin = 1 then x
when r.rin = 2 then y
when r.rin = 3 then m
end
from cte b,
(select level rin from dual connect by level < 4) r
where b.asset_id = a.asset_id
and b.part_num = a.part_num
order by b.vertex_num, r.rin
) as mdsys.sdo_ordinate_array
)
) as geom
from cte a
group by asset_id, part_num
order by part_num
) f
GROUP BY asset_id;
Result:
ASSET_ID MGEOM
---------- -----
1 SDO_GEOMETRY(3006, NULL, NULL, SDO_ELEM_INFO_ARRAY(1, 2, 1, 10, 2, 1), SDO_ORDINATE_ARRAY(0, 5, 0, 10, 10, 11.18, 30, 0, 33.54, 50, 10, 33.54, 60, 10, 43.54))
See also my article [Building linestrings from GPX GPS data]: https://www.spdba.com.au/loading-and-processing-gpx-1-1-files-using-oracle-xmldb-2/
You can concatenate the it into a multi-line string of parts and then generate the SDO_GEOMETRY from that string:
SELECT asset_id,
SDO_GEOMETRY(
'MULTILINESTRING (' || LISTAGG(part, ',') WITHIN GROUP (ORDER BY part_num) || ')'
) AS geom
FROM (
SELECT asset_id,
part_num,
'(' || LISTAGG(x || ' ' || y || ' ' || m, ',') WITHIN GROUP (ORDER BY vertex_num) || ')'
AS part
FROM vertices
GROUP BY asset_id, part_num
)
GROUP BY asset_id
db<>fiddle here

Oracle - Parent - child + fill mising hierarchy levels

I have created my fiddle example here: FIDDLE
Here is also athe code from the fiddle:
CREATE TABLE T1(ID INT, CODE INT, CODE_NAME VARCHAR(100), PARENT_ID INT);
INSERT INTO T1 VALUES(100,1,'LEVEL 1', NULL);
INSERT INTO T1 VALUES(110,11,'LEVEL 2', 100);
INSERT INTO T1 VALUES(120,111,'LEVEL 3', 110);
INSERT INTO T1 VALUES(125,112,'LEVEL 3', 110);
INSERT INTO T1 VALUES(130,1111,'LEVEL 4', 120);
INSERT INTO T1 VALUES(200,2,'LEVEL 1', NULL);
INSERT INTO T1 VALUES(210,21,'LEVEL 2', 200);
INSERT INTO T1 VALUES(300,3,'LEVEL 1', NULL);
I have trouble finding the soultuin how to get from that table this result:
| CODE | CODE NAME | CODE 1 |CODE NAME 1| CODE 2 | CODE NAME 2| CODE 3 | CODE NAME 3 |
+--------+------------+--------+-----------+--------+------------+--------+-------------+
| 1 | LEVEL 1 | 11 | LEVEL 2 | 111 | LEVEL 3 | 1111 | LEVEL 4 |
| 1 | LEVEL 1 | 11 | LEVEL 2 | 112 | LEVEL 3 | 112 | LEVEL 3 |
| 2 | LEVEL 1 | 21 | LEVEL 2 | 21 | LEVEL 2 | 21 | LEVEL 2 |
| 3 | LEVEL 1 | 3 | LEVEL 1 | 3 | LEVEL 1 | 3 | LEVEL 1 |
I have tried something with connect by but that is not what I need(I think)...
The max I will ever have is 4 levels and if there are only two levels in the data then the 3rd and the 4th level should be filled wiht the values of the last existing value. The same rule is valid if there are 3 levels or 1 level.
You can use a recursive sub-query:
WITH hierarchy (
code, code_name,
code1, code_name1,
code2, code_name2,
code3, code_name3,
id, depth
) AS (
SELECT code,
code_name,
CAST(NULL AS INT),
CAST(NULL AS VARCHAR2(100)),
CAST(NULL AS INT),
CAST(NULL AS VARCHAR2(100)),
CAST(NULL AS INT),
CAST(NULL AS VARCHAR2(100)),
id,
1
FROM t1
WHERE parent_id IS NULL
UNION ALL
SELECT h.code,
h.code_name,
CASE depth WHEN 1 THEN COALESCE(t1.code, h.code) ELSE h.code1 END,
CASE depth WHEN 1 THEN COALESCE(t1.code_name, h.code_name) ELSE h.code_name1 END,
CASE depth WHEN 2 THEN COALESCE(t1.code, h.code1) ELSE h.code2 END,
CASE depth WHEN 2 THEN COALESCE(t1.code_name, h.code_name1) ELSE h.code_name2 END,
CASE depth WHEN 3 THEN COALESCE(t1.code, h.code2) ELSE h.code3 END,
CASE depth WHEN 3 THEN COALESCE(t1.code_name, h.code_name2) ELSE h.code_name3 END,
t1.id,
h.depth + 1
FROM hierarchy h
LEFT OUTER JOIN t1
ON (h.id = t1.parent_id)
WHERE depth < 4
)
CYCLE code, depth SET is_cycle TO 1 DEFAULT 0
SELECT code, code_name,
code1, code_name1,
code2, code_name2,
code3, code_name3
FROM hierarchy
WHERE depth = 4;
Which, for the sample data:
CREATE TABLE T1(ID, CODE, CODE_NAME, PARENT_ID) AS
SELECT 100, 1, 'LEVEL 1', NULL FROM DUAL UNION ALL
SELECT 110, 11, 'LEVEL 2', 100 FROM DUAL UNION ALL
SELECT 120, 111, 'LEVEL 3', 110 FROM DUAL UNION ALL
SELECT 130, 1111, 'LEVEL 4', 120 FROM DUAL UNION ALL
SELECT 200, 2, 'LEVEL 1', NULL FROM DUAL UNION ALL
SELECT 210, 21, 'LEVEL 2a', 200 FROM DUAL UNION ALL
SELECT 220, 22, 'LEVEL 2b', 200 FROM DUAL UNION ALL
SELECT 230, 221, 'LEVEL 3', 220 FROM DUAL UNION ALL
SELECT 300, 3, 'LEVEL 1', NULL FROM DUAL;
Outputs:
CODE
CODE_NAME
CODE1
CODE_NAME1
CODE2
CODE_NAME2
CODE3
CODE_NAME3
1
LEVEL 1
11
LEVEL 2
111
LEVEL 3
1111
LEVEL 4
3
LEVEL 1
3
LEVEL 1
3
LEVEL 1
3
LEVEL 1
2
LEVEL 1
21
LEVEL 2a
21
LEVEL 2a
21
LEVEL 2a
2
LEVEL 1
22
LEVEL 2b
221
LEVEL 3
221
LEVEL 3
db<>fiddle here
For sample data you posted:
SQL> select * from t1;
ID CODE CODE_NAME PARENT_ID
---------- ---------- ---------- ----------
100 1 LEVEL 1
110 11 LEVEL 2 100
120 111 LEVEL 3 110
130 1111 LEVEL 4 120
200 2 LEVEL 1
210 21 LEVEL 2 200
6 rows selected.
SQL>
an ugly (and who-knows-how-performant) query that, though, returns desired result is
with temp as
(select id, code, code_name, parent_id, level lvl,
row_number() over (partition by level order by id) rn
from t1
start with parent_id is null
connect by prior id = parent_id
),
a as
(select * from temp where lvl = 1),
b as
(select * from temp where lvl = 2),
c as
(select * from temp where lvl = 3),
d as
(select * from temp where lvl = 4)
select
a.code code1, a.code_name code_name1,
coalesce(b.code, a.code) code2, coalesce(b.code_name, a.code_name) code_name2,
coalesce(c.code, b.code, a.code) code3, coalesce(c.code_name, b.code_name, a.code_name) code_name3,
coalesce(d.code, c.code, b.code, a.code) code4, coalesce(d.code_name, c.code_name, b.code_name, a.code_name) code_name4
from a join b on b.rn = a.rn
left join c on c.rn = b.rn
left join d on d.rn = c.rn;
which results in
CODE1 CODE_NAME1 CODE2 CODE_NAME2 CODE3 CODE_NAME3 CODE4 CODE_NAME4
---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
1 LEVEL 1 11 LEVEL 2 111 LEVEL 3 1111 LEVEL 4
2 LEVEL 1 21 LEVEL 2 21 LEVEL 2 21 LEVEL 2
What does it do?
temp CTE creates a hierarchy; additionally, row_number function numbers each row within the same level
a, b, c, d CTEs extract values belonging to their own level value (you said there can be up to 4 levels)
finally, coalesce on column names along with outer join do the job
From your example I assume you want to see one row per root key as your example is not realy a tree but a bamboo
If so this is a trivial PIVOT query - unfortunately limited to some level deep (here example for your 4 levels)
with p (ROOT_CODE, CODE, CODE_NAME, ID, PARENT_ID, LVL) as (
select CODE, CODE, CODE_NAME, ID, PARENT_ID, 1 LVL from t1 where PARENT_ID is NULL
union all
select p.ROOT_CODE, c.CODE, c.CODE_NAME, c.ID, c.PARENT_ID, p.LVL+1 from t1 c
join p on c.PARENT_ID = p.ID),
t2 as (
select ROOT_CODE, CODE,CODE_NAME,LVL from p)
select * from t2
PIVOT
(max(CODE) code, max(CODE_NAME) code_name
for LVL in (1 as "LEV1",2 as "LEV2",3 as "LEV3",4 as "LEV4")
);
ROOT_CODE LEV1_CODE LEV1_CODE_ LEV2_CODE LEV2_CODE_ LEV3_CODE LEV3_CODE_ LEV4_CODE LEV4_CODE_
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
1 1 LEVEL 1 11 LEVEL 2 111 LEVEL 3 1111 LEVEL 4
2 2 LEVEL 1 21 LEVEL 2
The recursive CTE calculates the ROOT_CODE required for the pivot.
I' leaving as an exercise to fill the not defined levels (with COALESCE) with the previous values as in your example.
In case (as commented) you nedd oner row for each leave key a simple solution based on CONNECT_BY_PATHis possible.
I'm using again *recursive CTEcalculating the path from *root* to the *current node* and finaly filtering in the result the *leaves* (IDthat are notPARENT_ID`)
with p ( CODE, CODE_NAME, ID, PARENT_ID, PATH) as (
select CODE, CODE_NAME, ID, PARENT_ID, to_char(CODE)||'|'||CODE_NAME PATH from t1 where PARENT_ID is NULL
union all
select c.CODE, c.CODE_NAME, c.ID, c.PARENT_ID, p.PATH ||'|'||to_char(c.CODE)||'|'||c.CODE_NAME from t1 c
join p on c.PARENT_ID = p.ID)
select PATH from p
where ID in (select ID from T1 MINUS select PARENT_ID from T1)
order by 1;
The result holds for any level deepness and is concatenated string with delimiter
PATH
----------------------------------------------
1|LEVEL 1|11|LEVEL 2|111|LEVEL 3|1111|LEVEL 4
1|LEVEL 1|11|LEVEL 2|112|LEVEL 3
2|LEVEL 1|21|LEVEL 2
3|LEVEL 1
Use substr instr to extract and coalesce for the default values.
Solution using a hierarchical query - we record the code and code_name paths, then we break them apart. Level is used to decide whether we populate data from the paths or from the leaf node. The solution assumes the codes and code names do not contain the forward-slash character (if they may, use another separator in the paths - perhaps some control character like chr(31), the unit separator character in ASCII and Unicode).
To break apart the paths, I used regexp_substr as it's easier to work with (and, moreover, I assumed all codes and code names are non-null - if they may be null, the solution can be adapted easily). If this proves to be slow, that can be changed to use standard string functions.
with
p (code, code_name, parent_id, lvl, code_pth, code_name_pth) as (
select code, code_name, parent_id, level,
sys_connect_by_path(code, '/') || ',' ,
sys_connect_by_path(code_name, '/') || ','
from t1
where connect_by_isleaf = 1
start with parent_id is null
connect by parent_id = prior id
)
select case when lvl = 1 then code
else to_number(regexp_substr(code_pth, '[^/]+', 1, 1)) end as code,
case when lvl =1 then code_name
else regexp_substr(code_name_pth, '[^/]+', 1, 1) end as code_name,
case when lvl <= 2 then code
else to_number(regexp_substr(code_pth, '[^/]+', 1, 2)) end as code_1,
case when lvl <= 2 then code_name
else regexp_substr(code_name_pth, '[^/]+', 1, 2) end as code_name_1,
case when lvl <= 3 then code
else to_number(regexp_substr(code_pth, '[^/]+', 1, 3)) end as code_2,
case when lvl <= 3 then code_name
else regexp_substr(code_name_pth, '[^/]+', 1, 3) end as code_name_2,
code as code_3,
code_name as code_name_3
from p;

multiple columns' row split in oracle

I got a single string row split into rows
For example,
A,B,C,D,E
into
A
B
C
D
E
but what I would like to is multiple columns' single row
| A,B,C | H,I,J,K,L | Q,R,X,Y,Z |
into
A | H | Q
B | I | R
C | J | X
| K | Y
| L |
How can I do this in oracle?
You can use hiearchy query as follows:
SQL> WITH DATAA ( D ) AS (
2 SELECT '| A,B,C | H,I,J,K,L | Q,R,X,Y,Z |'
3 FROM DUAL
4 )
5 -- your query starts from here
6 SELECT TRIM(REGEXP_SUBSTR(REGEXP_SUBSTR(D.D, '[^|]+', 1, 1), '[^,]+', 1, LEVEL)) AS COL1,
7 TRIM(REGEXP_SUBSTR(REGEXP_SUBSTR(D.D, '[^|]+', 1, 2), '[^,]+', 1, LEVEL)) AS COL2,
8 TRIM(REGEXP_SUBSTR(REGEXP_SUBSTR(D.D, '[^|]+', 1, 3), '[^,]+', 1, LEVEL)) AS COL3
9 FROM DATAA D
10 CONNECT BY LEVEL <= (
11 SELECT MAX(REGEXP_COUNT((REGEXP_SUBSTR(D.D, '[^|]+', 1, COLUMN_VALUE)), ',')) + 1
12 FROM DATAA D
13 CROSS JOIN TABLE ( CAST(MULTISET(
14 SELECT LEVEL LVL
15 FROM DUAL
16 CONNECT BY LEVEL <= REGEXP_COUNT(D.D, '[^|]+')
17 ) AS SYS.ODCIVARCHAR2LIST) ) LVLS
18 );
COL1 COL2 COL3
------- ----------- -----------
A H Q
B I R
C J X
K Y
L Z
SQL>

Oracle find the 1st node and last node in a path, CONNECT BY nocycle

I would like to know if there is anyway it return the beginning of the hierarchy and end of hierarchy for the code below
WITH o AS
(
SELECT 'A' as obj,
'C' as link
FROM dual
UNION ALL
SELECT 'C',
'D'
FROM dual
UNION ALL
SELECT 'D',
'E'
FROM dual
UNION ALL
SELECT 'X',
'Y'
FROM dual),
apath AS
(SELECT obj,
link
, sys_connect_by_path(obj || '->' || link, ',') pth
, connect_by_iscycle as cy
, connect_by_isleaf AS lf
, level
FROM o
CONNECT BY nocycle obj = PRIOR link)
SELECT *
FROM apath
where lf = 1
order by pth
I would like have the result like
Begin_Node, End_Node
A E
C E
D E
X Y
Current code returns
D E ,A->C,C->D,D->E 0 1 3
D E ,C->D,D->E 0 1 2
D E ,D->E 0 1 1
X Y ,X->Y 0 1 1
Thanks in advance.
Just use CONNECT_BY_ROOT and filter by CONNECT_BY_ISLEAF:
WITH o ( obj, link ) AS (
SELECT 'A', 'C' FROM DUAL UNION ALL
SELECT 'C', 'D' FROM DUAL UNION ALL
SELECT 'D', 'E' FROM DUAL UNION ALL
SELECT 'X', 'Y' FROM DUAL
)
SELECT CONNECT_BY_ROOT( obj ) AS begin_node,
link AS end_node
FROM o
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY nocycle obj = PRIOR link;
Outputs:
BEGIN_NODE | END_NODE
:--------- | :-------
A | E
C | E
D | E
X | Y
If you want all the columns then:
WITH o ( obj, link ) AS (
SELECT 'A', 'C' FROM DUAL UNION ALL
SELECT 'C', 'D' FROM dual UNION ALL
SELECT 'D', 'E' FROM dual UNION ALL
SELECT 'X', 'Y' FROM dual
)
SELECT CONNECT_BY_ROOT( obj ) AS begin_node,
link AS end_node,
SYS_CONNECT_BY_PATH( obj || '->' || link, ',' ) AS path,
CONNECT_BY_ISCYCLE as iscycle,
CONNECT_BY_ISLEAF AS isleaf,
level
FROM o
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY nocycle obj = PRIOR link;
Which outputs:
BEGIN_NODE | END_NODE | PATH | ISCYCLE | ISLEAF | LEVEL
:--------- | :------- | :-------------- | ------: | -----: | ----:
A | E | ,A->C,C->D,D->E | 0 | 1 | 3
C | E | ,C->D,D->E | 0 | 1 | 2
D | E | ,D->E | 0 | 1 | 1
X | Y | ,X->Y | 0 | 1 | 1
db<>fiddle here
If you're happy with what pth represents, it is simple to extract that info from it:
line #21: remove leading comma
lines #27 and #28 return begin and end nodes
<snip>
18 apath AS
19 (SELECT obj,
20 link
21 , ltrim(sys_connect_by_path(obj || '->' || link, ','), ',') pth
22 , connect_by_iscycle as cy
23 , connect_by_isleaf AS lf
24 , level
25 FROM o
26 CONNECT BY nocycle obj = PRIOR link)
27 SELECT regexp_substr(pth, '^\w+') begin_node,
28 regexp_substr(pth, '\w+$') end_node
29 FROM apath
30 where lf = 1
31 order by pth
32 /
BEGIN_NODE END_NODE
---------- ----------
A E
C E
D E
X Y
SQL>

Age analysis on excel

I would like to distribute a set of data into a table shown below:
RANGE(days) No of days. Amount
0-30 - 0
31-180 1 4,185.78
181-365 2 74,056.86
366 and above 6 587,198.35
TOTAL 9 665,440.99
SOURCE Data
S/N START DATE Details Tran Amt END DATE
1 22/05/2015 A 448,749.14 30/06/2018
2 22/09/2015 B 4,883.02 30/06/2018
3 04/11/2015 C 45,646.27 30/06/2018
4 26/04/2016 D 42,861.99 30/06/2018
5 16/06/2016 E 23,144.23 30/06/2018
6 27/07/2016 F 21,913.70 30/06/2018
7 11/08/2017 G 61,396.94 30/06/2018
8 30/11/2017 H 12,659.92 30/06/2018
9 19/03/2018 I 4,185.78 30/06/2018
TOTAL 665,440.99
Thanks
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE data ( SN, START_DATE, Details, TranAmt ) AS
SELECT 1, DATE '2015-05-22', 'A', 448749.14 FROM DUAL UNION ALL
SELECT 2, DATE '2015-09-22', 'B', 4883.02 FROM DUAL UNION ALL
SELECT 3, DATE '2015-11-04', 'C', 45646.27 FROM DUAL UNION ALL
SELECT 4, DATE '2016-04-26', 'D', 42861.99 FROM DUAL UNION ALL
SELECT 5, DATE '2016-06-16', 'E', 23144.23 FROM DUAL UNION ALL
SELECT 6, DATE '2016-07-27', 'F', 21913.70 FROM DUAL UNION ALL
SELECT 7, DATE '2017-08-11', 'G', 61396.94 FROM DUAL UNION ALL
SELECT 8, DATE '2017-11-30', 'H', 12659.92 FROM DUAL UNION ALL
SELECT 9, DATE '2018-03-19', 'I', 4185.78 FROM DUAL;
Query 1:
WITH end_date ( end_date ) AS (
SELECT DATE '2018-06-30' FROM DUAL
),
ranges ( first, last ) AS (
SELECT 0, 30 FROM DUAL UNION ALL
SELECT 31, 180 FROM DUAL UNION ALL
SELECT 181, 365 FROM DUAL UNION ALL
SELECT 366, NULL FROM DUAL
)
SELECT NVL2(
r.first,
r.first || NVL2(
MAX( r.last ),
' - ' || MAX( r.last ),
' and above'
),
'Total'
) AS range,
COUNT( d.sn ) AS "No of days",
COALESCE( SUM( d.TranAmt ), 0 ) AS Amount
FROM data d
CROSS JOIN end_date e
RIGHT OUTER JOIN ranges r
ON ( d.start_date < e.end_date + 1 - r.first
AND ( r.last IS NULL
OR d.start_date >= e.end_date - r.last ) )
GROUP BY ROLLUP( r.first )
ORDER BY r.first
Results:
| RANGE | No of days | AMOUNT |
|---------------|------------|-----------|
| 0 - 30 | 0 | 0 |
| 31 - 180 | 1 | 4185.78 |
| 181 - 365 | 2 | 74056.86 |
| 366 and above | 6 | 587198.35 |
| Total | 9 | 665440.99 |

Resources