Grouping and Sorting By distinct values in Oracle's SQL - sql

I have table like this:
id
full_name
1
John Smith
2
Smith John
3
Jim Jonson
4
JimJonson
I want to get something like this:
id
full_name
1
John Smith
3
Jim Jonson
So, I need SELECT DISTINCT full_name FROM table, so that
John Smith and Smith John to be one and the same, also
Jim Jonson and JimJonson
I hope I explained it well. Can you help me?

You can split the full_name values by initial capitals of name and surnames in unpivoted manner, and sort alphabetically, and combine by using LISTAGG() function, and apply MIN() aggregation at the last step such as
WITH t(id,full_name) AS
(
SELECT 1, 'John Smith' FROM dual UNION ALL
SELECT 2, 'Smith John' FROM dual UNION ALL
SELECT 3, 'Jim Jonson' FROM dual UNION ALL
SELECT 4, 'JimJonson' FROM dual
), t2 AS
(
SELECT id,
TRIM(SUBSTR(full_name,column_value,LEAD(column_value,1,LENGTH(full_name)) OVER (PARTITION BY id ORDER BY id,column_value)-1)) AS names
FROM t,
TABLE(CAST(MULTISET(SELECT REGEXP_INSTR(full_name,'[A-Z]+',1,level)
FROM dual
CONNECT BY level <= REGEXP_COUNT(full_name,'[A-Z]')) AS sys.odcivarchar2list ))
), t3 AS
(
SELECT id, LISTAGG(names,' ') WITHIN GROUP (ORDER BY id,names) AS full_name
FROM t2
GROUP BY id
)
SELECT MIN(id) AS min_id, full_name
FROM t3
GROUP BY full_name
ORDER BY min_id
Demo

Step-by-step. Read comments within code.
SQL> with test (id, full_name) as
2 -- sample data
3 (select 1, 'John Smith' from dual union all
4 select 2, 'Smith John' from dual union all
5 select 3, 'Jim Jonson' from dual union all
6 select 4, 'JimJonson' from dual
7 ),
8 temp as
9 -- split full name to rows
10 (select id,
11 regexp_substr(full_name, '[^ ]+', 1, column_value) val,
12 column_value cv
13 from test cross join
14 table(cast(multiset(select level from dual
15 connect by level <= regexp_count(full_name, ' ') + 1
16 ) as sys.odcinumberlist))
17 ),
18 temp2 as
19 -- aggregate full name with no space between "words"
20 (select id,
21 listagg(val, '') within group (order by val) full_name
22 from temp
23 group by id
24 ),
25 temp3 as
26 -- fetch only distinct values
27 (select min(b.id) id,
28 b.full_name
29 from temp2 b
30 group by b.full_name
31 )
32 -- finally, join TEMP3 and sample data
33 select b.id,
34 a.full_name
35 from test a join temp3 b on b.id = a.id
36 order by a.id;
ID FULL_NAME
---------- ----------------------------------------
1 John Smith
3 Jim Jonson
SQL>

Related

How to substitute with Comma Separate values in Oracle SQL

I have a scenario like below (Oracle's SQL).
Table:Employee
S.No
Name
Role
1
a
ELE,PLU,OTH
2
b
MAN,DIR
3
c
DIR,FND
4
d
Table: Role_master
Role
Role name
ELE
Electrician
PLU
Plumber
MAN
Manager
DIR
Director
FND
Founder
OTH
Other
With the above tables, i would like to join both and expecting the output like below. Please help on the better way.
S.No
Name
Role
1
a
Electrician,Plumber,Other
2
b
Manager,Director
3
c
Director,Founder
4
d
Here's one option:
sample data in lines #1 - 12; query begins at line #14
split employee.role into rows (i.e. separate values) so that you could join them to role_master.role
aggregate them back (using listagg)
SQL> with
2 employee (sno, name, role) as
3 (select 1, 'a', 'ELE,PLU,OTH' from dual union all
4 select 2, 'b', 'MAN,DIR' from dual
5 ),
6 role_master (role, role_name) as
7 (select 'ELE', 'Electrician' from dual union all
8 select 'PLU', 'Plumber' from dual union all
9 select 'OTH', 'Other' from dual union all
10 select 'MAN', 'Manager' from dual union all
11 select 'DIR', 'Director' from dual
12 )
13 --
14 select e.sno,
15 e.name,
16 listagg(m.role_name, ',') within group (order by column_value) role
17 from employee e cross join
18 table(cast(multiset(select level from dual
19 connect by level <= regexp_count(e.role, ',') + 1
20 ) as sys.odcinumberlist))
21 join role_master m on m.role = regexp_substr(e.role, '[^,]+', 1, column_value)
22 group by e.sno, e.name;
SNO NAME ROLE
---------- ---- ----------------------------------------
1 a Electrician,Plumber,Other
2 b Manager,Director
SQL>
Another solution could use this logic :
First, generate the maximum number of rows needed for the split step (required_rows_v)
Then, make left join between the three data sources like below
Then, use listagg function to re-aggregate rows
With required_rows_v (lvl) as (
select level lvl
from dual
connect by level <= ( select max( regexp_count( e.Role, '[^,]+' ) ) from employee e )
)
select e.SNO,
e.NAME,
listagg(rm.Role_name, ',')within group (order by e.SNo, v.lvl) Role
from employee e
left join required_rows_v v on v.lvl <= regexp_count( e.Role, '[^,]+' )
left join Role_master rm on rm.Role = regexp_substr( e.Role, '[^,]+', 1, v.lvl )
group by e.SNO, e.NAME
demo

Oracle listagg - Can I pull data from other table based on the values selected by listagg

I have two tables and want to get data from one table based on the values got from Listtagg in the second table:
T1
ID Name
==============
1 Name1
2 Person2
3 Someone3
4 Mr.4
T2
ID Acct
===============
1 1234
1 5678
2 1234
3 5678
3 8769
4 1234
My listagg query on T2 has returned the following:
Acct Id
====== ========
1234 1,2,4
5678 1,3
I need the result with Names from other table something like:
Acct Id Name
====== ======== ==========
1234 1,2,4 Name1, Person2, Mr.4
5678 1,3 Name1, Someone3
Why would you first aggregate IDs, and then put effort in splitting them to collect NAMEs? Do it immediately. Not that it can't be done (it can, in a relatively simple manner, but - why?!?).
Sample data is from line #1 - 15; query you might need begins at line #16.
SQL> with
2 t1 (id, name) as
3 (select 1, 'Name1' from dual union all
4 select 2, 'Person2' from dual union all
5 select 3, 'Someone3' from dual union all
6 select 4, 'Mr4' from dual
7 ),
8 t2 (id, acct) as
9 (select 1, 1234 from dual union all
10 select 1, 5678 from dual union all
11 select 2, 1234 from dual union all
12 select 3, 5678 from dual union all
13 select 3, 8769 from dual union all
14 select 4, 1234 from dual
15 )
16 select b.acct,
17 listagg(b.id, ', ') within group (order by b.id) id,
18 listagg(a.name, ', ') within group (order by b.id) name
19 from t1 a join t2 b on a.id = b.id
20 group by b.acct;
ACCT ID NAME
---------- ---------- --------------------
1234 1, 2, 4 Name1, Person2, Mr4
5678 1, 3 Name1, Someone3
8769 3 Someone3
SQL>
#Littlefoot's answer is absolutely correct. But just as an addition: don't use listagg if you are going to split those aggregated values. Just use collect() aggregate function to get needed data as a collection.
For example:
select
cast(collect(level) as sys.odcinumberlist) as varray_of_numbers,
cast(collect(level) as ORA_MINING_NUMBER_NT) as nested_table_of_numbers,
cast(collect(sys.ku$_objnum(level)) as sys.KU$_OBJNUMSET) as nested_table_of_objnum
from dual connect by level<=3;
--Result:
VARRAY_OF_NUMBERS NESTED_TABLE_OF_NUMBERS NESTED_TABLE_OF_OBJNUM(OBJ_NUM)
------------------------- ------------------------------ ------------------------------------------------------------
ODCINUMBERLIST(1, 2, 3) ORA_MINING_NUMBER_NT(1, 2, 3) KU$_OBJNUMSET(KU$_OBJNUM(1), KU$_OBJNUM(2), KU$_OBJNUM(3))
Update: This is a query for your tables, as you asked in the comment:
select b.acct,
cast(collect(b.id) as ORA_MINING_NUMBER_NT) as nested_table_of_numbers,
cast(collect(a.name) as ORA_MINING_VARCHAR2_NT) as nested_table_of_varchar2
-- listagg(b.id, ', ') within group (order by b.id) id,
-- listagg(a.name, ', ') within group (order by b.id) name
from t1 a join t2 b on a.id = b.id
group by b.acct;
Full example:
with
t1 (id, name) as
(select 1, 'Name1' from dual union all
select 2, 'Person2' from dual union all
select 3, 'Someone3' from dual union all
select 4, 'Mr4' from dual
),
t2 (id, acct) as
(select 1, 1234 from dual union all
select 1, 5678 from dual union all
select 2, 1234 from dual union all
select 3, 5678 from dual union all
select 3, 8769 from dual union all
select 4, 1234 from dual
)
select b.acct,
cast(collect(b.id) as ORA_MINING_NUMBER_NT) as nested_table_of_numbers,
cast(collect(a.name) as ORA_MINING_VARCHAR2_NT) as nested_table_of_varchar2
-- listagg(b.id, ', ') within group (order by b.id) id,
-- listagg(a.name, ', ') within group (order by b.id) name
from t1 a join t2 b on a.id = b.id
group by b.acct;

Oracle Finding a string match from multiple database tables

This is somewhat a complex problem to describe, but I'll try to explain it with an example. I thought I would have been able to use the Oracle Instr function to accomplish this, but it does not accept queries as parameters.
Here is a simplification of my data:
Table1
Person Qualities
Joe 5,6,7,8,9
Mary 7,8,10,15,20
Bob 7,8,9,10,11,12
Table2
Id Desc
5 Nice
6 Tall
7 Short
Table3
Id Desc
8 Angry
9 Sad
10 Fun
Table4
Id Desc
11 Boring
12 Happy
15 Cool
20 Mad
Here is somewhat of a query to give an idea of what I'm trying to accomplish:
select * from table1
where instr (Qualities, select Id from table2, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
I'm trying to figure out which people have at least 1 quality from each of the 3 groups of qualities (tables 2,3, and 4)
So Joe would not be returned in the results because he does not have the quality from each of the 3 groups, but Mary and Joe would since they have at least 1 quality from each group.
We are running Oracle 12, thanks!
Here's one option:
SQL> with
2 table1 (person, qualities) as
3 (select 'Joe', '5,6,7,8,9' from dual union all
4 select 'Mary', '7,8,10,15,20' from dual union all
5 select 'Bob', '7,8,9,10,11,12' from dual
6 ),
7 table2 (id, descr) as
8 (select 5, 'Nice' from dual union all
9 select 6, 'Tall' from dual union all
10 select 7, 'Short' from dual
11 ),
12 table3 (id, descr) as
13 (select 8, 'Angry' from dual union all
14 select 9, 'Sad' from dual union all
15 select 10, 'Fun' from dual
16 ),
17 table4 (id, descr) as
18 (select 11, 'Boring' from dual union all
19 select 12, 'Happy' from dual union all
20 select 15, 'Cool' from dual union all
21 select 20, 'Mad' from dual
22 ),
23 t1new (person, id) as
24 (select person, regexp_substr(qualities, '[^,]+', 1, column_value) id
25 from table1 cross join table(cast(multiset(select level from dual
26 connect by level <= regexp_count(qualities, ',') + 1
27 ) as sys.odcinumberlist))
28 )
29 select a.person,
30 count(b.id) bid,
31 count(c.id) cid,
32 count(d.id) did
33 from t1new a left join table2 b on a.id = b.id
34 left join table3 c on a.id = c.id
35 left join table4 d on a.id = d.id
36 group by a.person
37 having ( count(b.id) > 0
38 and count(c.id) > 0
39 and count(d.id) > 0
40 );
PERS BID CID DID
---- ---------- ---------- ----------
Bob 1 3 2
Mary 1 2 2
SQL>
What does it do?
lines #1 - 22 represent your sample data
T1NEW CTE (lines #23 - 28) splits comma-separated qualities into rows, per every person
final select (lines #29 - 40) are outer joining t1new with each of "description" tables (table2/3/4) and counting how many qualities are contained in there for each of person's qualities (represented by rows from t1new)
having clause is here to return only desired persons; each of those counts have to be a positive number
Maybe this will help:
{1} Create a view that categorises all qualities and allows you to SELECT quality IDs and categories . {2} JOIN the view to TABLE1 and use a join condition that "splits" the CSV value stored in TABLE1.
{1} View
create or replace view allqualities
as
select 1 as category, id as qid, descr from table2
union
select 2, id, descr from table3
union
select 3, id, descr from table4
;
select * from allqualities order by category, qid ;
CATEGORY QID DESCR
---------- ---------- ------
1 5 Nice
1 6 Tall
1 7 Short
2 8 Angry
2 9 Sad
2 10 Fun
3 11 Boring
3 12 Happy
3 15 Cool
3 20 Mad
{2} Query
-- JOIN CONDITION:
-- {1} add a comma at the start and at the end of T1.qualities
-- {2} remove all blanks (spaces) from T1.qualities
-- {3} use LIKE and the qid (of allqualities), wrapped in commas
--
-- inline view: use UNIQUE, otherwise we may get counts > 3
--
select person
from (
select unique person, category
from table1 T1
join allqualities A
on ',' || replace( T1.qualities, ' ', '' ) || ',' like '%,' || A.qid || ',%'
)
group by person
having count(*) = ( select count( distinct category ) from allqualities )
;
-- result
PERSON
Bob
Mary
Tested w/ Oracle 18c and 11g. DBfiddle here.

How to select a number of rows according to a column

So I have got two columns in an Oracle database:
Name / count
I would like to print the name x times, x being the count.
E.g. Paul / 5 would mean Paul being printed 5 times.
Sam / 6 would mean Sam being printed 6 times
Tried row_number over but not sure how it works?
You can use connect by as following:
SQL> WITH YOUR_TABLE AS
2 (SELECT 'paul' as NAME, 5 AS COUNT FROM DUAL UNION ALL
3 SELECT 'sam' as NAME, 6 AS COUNT FROM DUAL
4 ) -- YOUR ACTUAL QUERY STARTS FROM LINE#5
5 Select t.name, m.lvl
6 from your_table t
7 join
8 (Select level as lvl
9 from
10 (Select max(count) as maxcount
11 from your_table)
12 Connect by level <= maxcount) m
13 On (t.count >= m.lvl)
14 ORDER BY 1,2;
NAME LVL
---- ----------
paul 1
paul 2
paul 3
paul 4
paul 5
sam 1
sam 2
sam 3
sam 4
sam 5
sam 6
11 rows selected.
SQL>
Cheers!!
you need recursive query to achieve this.
with cte(nam, ctr) as (
select 'Paul' as nam, 5 as ctr from dual
union all
select 'Sam', 6 as ctr from dual
),
cte2(nam, ct, ctr) as (
select nam, 1 as ct, ctr from cte
union all
select nam, ct + 1, ctr from cte2
where ct<ctr
)select nam, ct from cte2
order by nam asc
output:
See sqlfiddle
this will work:
select name
from Table1,
(select level lvl
from dual
connect by level <= (select max(cnt) from Table1 )
)
where lvl <= cnt
order by name;
check fiddle:http://sqlfiddle.com/#!4/14a67/1
Thanks!!!
Yet another option (your query starts at line #4):
SQL> with your_table as
2 (select 'paul' as name, 5 as count from dual union all
3 select 'sam' as name, 6 as count from dual)
4 select name
5 from your_table cross join table (cast (multiset (select level from dual
6 connect by level <= count
7 ) as sys.odcinumberlist));
NAME
----
paul
paul
paul
paul
paul
sam
sam
sam
sam
sam
sam
11 rows selected.
SQL>
You can use a connect by level <= some_number logic containing cross join to link with your table tab :
with tab(Name,"count") as
( select 'Paul', 5 from dual union all select 'Sam', 6 from dual )
select name, level as seq
from dual d
cross join tab t
connect by level <= t."count"
and prior name = name
and prior sys_guid() is not null;
Demo

Join two tables with a column with multiple entries for the other table

I have the following problem.
I want to join two tables.
The first table has entries like the following:
T1
PK Info
1 one
2 two
3 three
The second table is build like this:
T2
PK FKT1
1 1,3
2 1,2,3
3 2
My Result should show the following
PK2 FKT1 InfoT1
1 1,3 One,Three
2 1,2,3 One,two,Three
3 2 Two
I just cant get an idea how to solve this.
Is this possible only using sql selects or is a function needed?
kind regards
It's not that difficult, but - as you were told, you'd rather NOT do that.
SQL> with
2 t1 (pk, info) as
3 (select 1, 'one' from dual union
4 select 2, 'two' from dual union
5 select 3, 'three' from dual
6 ),
7 t2 (pk, fkt1) as
8 (select 1, '1,3' from dual union
9 select 2, '1,2,3' from dual union
10 select 3, '2' from dual
11 ),
12 t2rows as
13 (select pk, regexp_substr(fkt1, '[^,]+', 1, column_value) fkt1, column_value rn
14 from t2,
15 table(cast(multiset(select level from dual
16 connect by level <= regexp_count(fkt1, ',') + 1
17 ) as sys.odcinumberlist))
18 )
19 select t2r.pk,
20 listagg(t2r.fkt1, ',') within group (order by t2r.rn) fkt1,
21 listagg(t1.info, ',') within group (order by t2r.rn) infot1
22 from t2rows t2r join t1 on t2r.fkt1 = t1.pk
23 group by t2r.pk
24 order by t2r.pk;
PK FKT1 INFOT1
---------- -------------------- --------------------
1 1,3 one,three
2 1,2,3 one,two,three
3 2 two
SQL>