Query to join Oracle tables - oracle

I have a 'MASTER' table as shown below:
Entity Cat1 Cat2
A Mary;Steve Jacob
B Alex;John Sally;Andrew
Another table 'PERSON' has associations of person's name (this could be InfoID as well) with emails.
Name Email InfoID
Mary maryD#gmail.com mryD
Steve steveR#gmail.com stvR
Jacob jacobB#gmail.com jacbb
Sally sallyD#gmail.com sallD
Alex AlexT#gmail.com alexT
John JohnP#gmail.com johP
Andrew AndrewV#gmail.com andV
I want to join the person table with master such as:
Entity Cat1 EmailCat1 Cat2 EmailCat2
A Mary;Steve maryD#gmail.com;steveR#gmail.com Jacob jacobB#gmail.com
B Alex;John AlexT#gmail.com;JohnP#gmail.com Sally;Andrew sallyD#gmail.com;AndrewV#gmail.com
any insights on how to go about it?

Honestly, your master table design needs to be normalized. But, in the meantime you could try this query below :
with
needed_rows_for_cat1_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat1, ';')) from Your_bad_master_tab) + 1
)
, needed_rows_for_cat2_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat2, ';')) from Your_bad_master_tab) + 1
)
, split_cat1_val_tab as (
select Entity, Cat1
, substr(Cat1||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 1 cat
from (
select Entity, Cat1, instr(Cat1||';', ';', 1, r1.lvl)pos, r1.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat1_tab r1 on r1.lvl <= regexp_count(Cat1, ';') + 1
)
)
, split_cat2_val_tab as (
select Entity, Cat2
, substr(Cat2||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 2 cat
from (
select Entity, Cat2, instr(Cat2||';', ';', 1, r2.lvl)pos, r2.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat2_tab r2 on r2.lvl <= regexp_count(Cat2, ';') + 1
)
)
select ENTITY
, max(decode(cat, 1, CAT1, null)) CAT1
, listagg(decode(cat, 1, EMAIL, null), ';')within group (order by lvl) EmailCat1
, max(decode(cat, 2, CAT1, null)) CAT2
, listagg(decode(cat, 2, EMAIL, null), ';')within group (order by lvl) EmailCat2
from (
select c.*, p.Email
from split_cat1_val_tab c join Your_person_tab p on (c.val = p.name)
union all
select c.*, p.Email
from split_cat2_val_tab c join Your_person_tab p on (c.val = p.name)
)
group by ENTITY
;
Here are your sample data
--drop table Your_bad_master_tab purge;
create table Your_bad_master_tab (Entity, Cat1, Cat2) as
select 'A', 'Mary;Steve', 'Jacob' from dual union all
select 'B', 'Alex;John', 'Sally;Andrew' from dual
;
--drop table Your_person_tab purge;
create table Your_person_tab (Name, Email, InfoID) as
select 'Mary', 'maryD#gmail.com' ,'mryD' from dual union all
select 'Steve', 'steveR#gmail.com' ,'stvR' from dual union all
select 'Jacob', 'jacobB#gmail.com' ,'jacbb' from dual union all
select 'Sally', 'sallyD#gmail.com' ,'sallD' from dual union all
select 'Alex', 'AlexT#gmail.com' ,'alexT' from dual union all
select 'John', 'JohnP#gmail.com' ,'johP' from dual union all
select 'Andrew', 'AndrewV#gmail.com' ,'andV' from dual
;
DB<>fiddle

Related

ORACLE - How to use LAG to display strings from all previous rows into current row

I have data like below:
group
seq
activity
A
1
scan
A
2
visit
A
3
pay
B
1
drink
B
2
rest
I expect to have 1 new column "hist" like below:
group
seq
activity
hist
A
1
scan
NULL
A
2
visit
scan
A
3
pay
scan, visit
B
1
drink
NULL
B
2
rest
drink
I was trying to solve with LAG function, but LAG only returns one row from previous instead of multiple.
Truly appreciate any help!
Use a correlated sub-query:
SELECT t.*,
(SELECT LISTAGG(activity, ',') WITHIN GROUP (ORDER BY seq)
FROM table_name l
WHERE t."GROUP" = l."GROUP"
AND l.seq < t.seq
) AS hist
FROM table_name t
Or a hierarchical query:
SELECT t.*,
SUBSTR(SYS_CONNECT_BY_PATH(PRIOR activity, ','), 3) AS hist
FROM table_name t
START WITH seq = 1
CONNECT BY
PRIOR seq + 1 = seq
AND PRIOR "GROUP" = "GROUP"
Or a recursive sub-query factoring clause:
WITH rsqfc ("GROUP", seq, activity, hist) AS (
SELECT "GROUP", seq, activity, NULL
FROM table_name
WHERE seq = 1
UNION ALL
SELECT t."GROUP", t.seq, t.activity, r.hist || ',' || r.activity
FROM rsqfc r
INNER JOIN table_name t
ON (r."GROUP" = t."GROUP" AND r.seq + 1 = t.seq)
)
SEARCH DEPTH FIRST BY "GROUP" SET order_rn
SELECT "GROUP", seq, activity, SUBSTR(hist, 2) AS hist
FROM rsqfc
Which, for the sample data:
CREATE TABLE table_name ("GROUP", seq, activity) AS
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL;
All output:
GROUP
SEQ
ACTIVITY
HIST
A
1
scan
null
A
2
visit
scan
A
3
pay
scan,visit
B
1
drink
null
B
2
rest
drink
db<>fiddle here
To aggregate strings in Oracle we use LISAGG function.
In general, you need a windowing_clause to specify a sliding window for analytic function to calculate running total.
But unfortunately LISTAGG doesn't support it.
To simulate this behaviour you may use model_clause of the select statement. Below is an example with explanation.
select
group_
, activity
, seq
, hist
from t
model
/*Where to restart calculation*/
partition by (group_)
/*Add consecutive numbers to reference "previous" row per group.
May use "seq" column if its values are consecutive*/
dimension by (
row_number() over(
partition by group_
order by seq asc
) as rn
)
measures (
/*Other columnns to return*/
activity
, cast(null as varchar2(1000)) as hist
, seq
)
rules update (
/*Apply this rule sequentially*/
hist[any] order by rn asc =
/*Previous concatenated result*/
hist[cv()-1]
/*Plus comma for the third row and tne next rows*/
|| presentv(activity[cv()-2], ',', '') /**/
/*lus previous row's value*/
|| activity[cv()-1]
)
GROUP_ | ACTIVITY | SEQ | HIST
:----- | :------- | --: | :---------
A | scan | 1 | null
A | visit | 2 | scan
A | pay | 3 | scan,visit
B | drink | 1 | null
B | rest | 2 | drink
db<>fiddle here
Few more variants (without subqueries):
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
DBFIddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=9b477a2089d3beac62579d2b7103377a
Full test case with output:
with table_name ("GROUP", seq, activity) AS (
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
GROUP SEQ ACTIV HIST1 HIST2
------ ---------- ----- ------------------------------ ------------------------------
A 1 scan
A 2 visit scan, scan
A 3 pay scan,visit, scan,visit
B 1 drink
B 2 rest drink, drink

How to Choose a specific value from a table and to avoid duplicates?

I have two tables:
MainTable
id AccountNum status
1 11001 active
2 11002 active
3 11003 active
4 11004 active
AddTable
id date description
1 01.2020 ACCOUNT.SET
1 02.2020 ACCOUNT.CHANGE
1 03.2020 ACCOUNT.REMOVE
2 04.2020 ACCOUNT.SET
2 05.2020 ACCOUNT.CHANGE
3 08.2020 ACCOUNT.SET
4 05.2020 ACCOUNT.SET
4 09.2020 ACCOUNT.REMOVE
I need to get a such result:
EffectiveFrom is date when Account was set,
EffectiveTo is date when Account was removed
id AccountNum EffectiveFrom EffectiveTo
1 11001 01.2020 03.2020
2 11002 04.2020 null
3 11003 08.2020 null
4 11004 05.2020 09.2020
The problem is that after joining on AddTable I get the duplicates, but I need just one row on every Id and only dates where the description in ACCOUNT.SET,ACCOUNT.REMOVE.
Are you looking for left join?
select m.id as id,
m.AccountNum as AccountNum,
a.date as EffectiveFrom,
b.date as EffectiveTo
from MainTable m left join
AddTable a on (a.id = m.id and a.description = 'ACCOUNT.SET') left join
AddTable b on (b.id = m.id and b.description = 'ACCOUNT.REMOVE')
order by m.AccountNum
Use a PIVOT and a LEFT OUTER JOIN:
SELECT m.id,
a.EffectiveFrom,
a.EffectiveTo
FROM MainTable m
LEFT OUTER JOIN
(
SELECT *
FROM AddTable
PIVOT( MAX( dt ) FOR description IN (
'ACCOUNT.SET' AS EffectiveFrom,
'ACCOUNT.REMOVE' AS EffectiveTo
) )
) a
ON ( a.id = m.id )
ORDER BY m.id
So for your test data:
CREATE TABLE MainTable ( id, AccountNum, status ) AS
SELECT 1, 11001, 'active' FROM DUAL UNION ALL
SELECT 2, 11002, 'active' FROM DUAL UNION ALL
SELECT 3, 11003, 'active' FROM DUAL UNION ALL
SELECT 4, 11004, 'active' FROM DUAL;
CREATE TABLE AddTable ( id, dt, description ) AS
SELECT 1, DATE '2020-01-01', 'ACCOUNT.SET' FROM DUAL UNION ALL
SELECT 1, DATE '2020-01-02', 'ACCOUNT.CHANGE' FROM DUAL UNION ALL
SELECT 1, DATE '2020-01-03', 'ACCOUNT.REMOVE' FROM DUAL UNION ALL
SELECT 2, DATE '2020-01-04', 'ACCOUNT.SET' FROM DUAL UNION ALL
SELECT 2, DATE '2020-01-05', 'ACCOUNT.CHANGE' FROM DUAL UNION ALL
SELECT 3, DATE '2020-01-08', 'ACCOUNT.SET' FROM DUAL UNION ALL
SELECT 4, DATE '2020-01-05', 'ACCOUNT.SET' FROM DUAL UNION ALL
SELECT 4, DATE '2020-01-09', 'ACCOUNT.REMOVE' FROM DUAL;
This outputs:
ID | EFFECTIVEFROM | EFFECTIVETO
-: | :------------ | :----------
1 | 01-JAN-20 | 03-JAN-20
2 | 04-JAN-20 | null
3 | 08-JAN-20 | null
4 | 05-JAN-20 | 09-JAN-20
db<>fiddle here

sql placeholder rows

I have an apex item P_USERS which can have a value higher than the amount of rows returning from the query below.
I have a classic report which has the following query:
select
first_name,
last_name
from accounts
where account_role = 'Author'
order by account_nr;
I want placeholder rows to be added to the query (first_name = null, last_name = null etc.), if the total rows from the query is lesser than the value in the apex_item P_USERS.
Any tips on how to achieve this? Maybe with a LEFT join?
If you have more result than the minima you defined, you must add the rest with union.
Here is what you could try to adapt to your case:
SELECT i,c FROM (
select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)), (Select Rownum r From dual Connect By Rownum <= 3)
where (i(+)= r)
union select i,c from (select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)) where i>3
You may try to use a LEFT JOIN.
First, create a list of number until the limit you want like suggested here:
-- let's say you want 300 records
Select Rownum r From dual Connect By Rownum <= 300
Then you can use this to left join and have empty records:
SELECT C, R FROM
( select rownum i, c from (select 'a' c from dual union all select 'b' from dual) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r order by r
The above gives you an ordered list starting with 'a', 'b', then null until the end.
So you could adapt it to your case so:
SELECT F,L FROM
( select rownum i, f, l from (
select first_name f, last_name l
from accounts where account_role = 'Author'
order by account_nr) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r

Oracle - Select all values from in clause even if there is no match

I have a dynamic data set such as 'AAA','TTT','CCC','FFF'
I need to match this data against a column C in a table T
e.g. I have in the table T for Column C, 'AAA','BBB','DDD','FFF'
I need to return something like (show null if the value doesn't exist in Column)
'AAA'
'TTT' NULL
'CCC' NULL
'FFF'
I don't want to drop the set into table as my data changes frequently and need to query quickly.
Any ideas greatly appreciated.
Is this what you're after ??
with w_data as (
select 'AAA' c from dual union all
select 'TTT' c from dual union all
select 'CCC' c from dual union all
select 'FFF' c from dual
),
w_table_t as (
select 'AAA' c from dual union all
select 'BBB' c from dual union all
select 'DDD' c from dual union all
select 'FFF' c from dual
)
select d.c,
NVL2(t.c, '', 'NULL' )
from w_data d
LEFT OUTER JOIN
w_table_t t
ON t.c = d.c
/
results:
C NVL2
--- ----
AAA
FFF
TTT NULL
CCC NULL
Try this (EDITED) :
with data_set as (
select 'AAA' col from dual union
select 'TTT' col from dual union
select 'CCC' col from dual union
select 'FFF' col from dual
)
select case when d.col in (select column_C from table_T) then d.col
else d.col||' Null' end as col_name
from data_set d
/

Oracle Hirerachical Query [duplicate]

This question already exists:
Oracle Hierarchical Query
Closed 8 years ago.
I want to write oracle hirerachical query which would return parent value if child has no value.If child has value it should return child value only. If child has no value it has to traverse until its one parent has value.
Select * From (
Select Orgid,Text
From (
Select Cusid,parent_id, Orgid, Rownum R
FROM (
SELECT cus.customerid cusid, org.Id orgid,
cus.customername, org.parent_id
From Boms_Customer Cus, Boms_Organization Org
Where Cus.Organization_Id=Org.Id
) A
START WITH a.cusid=100
Connect By Prior A.Parent_Id = A.Orgid
) X,
(
Select Org.Organization_Id,Org.Text
From FORTRESS.SUPPORT_QUESTION org
) Y
WHERE x.orgid = y.ORGANIZATION_ID ORDER BY x.r
)
But my query is returning all parents data and child data.Please help me inquery getting only childrens data if it has data otherwise it should return parents data.
Well, with a bit of work here is what I get:
with organisation(id, parent_id, name) as
(
select 1, null, 'Cisco' from dual
union all
select 2, 1, 'vodafone' from dual
union all
select 22, 1, 'SFR' from dual
union all
select 3, 1, 'Airtel' from dual
union all
select 4, 2, 'Aircel' from dual
),
question(ID, ORG_ID, QName) as
(
select 1, 1, 'Test' from dual
union all
select 2, 1, 'XYZ' from dual
union all
select 3, 2, 'Testing Network' from dual
),
have_question(org_id) as
(
select distinct org_id from question
),
tree as
(
select o.*, level lev, sys_connect_by_path(id, '/') path
from organisation o
connect by parent_id = prior id
start with parent_id is null
),
ancestors as
(
select t.id, t2.id ancestor, t2.lev ancestor_lev
from tree t, tree t2, have_question h
where t.path || '/' like t2.path || '/%'
and h.org_id (+) = t2.id
and h.org_id is not null
)
select a.id, max(ancestor) keep (dense_rank last order by ancestor_lev) first_ancestor_with_question
from ancestors a
group by a.id
order by 1
;
You only have to do a join with question table to get the corresponding questions.

Resources