ORACLE join two table with comma separated ids - sql

I have two tables
Table 1
ID NAME
1 Person1
2 Person2
3 Person3
Table 2
ID GROUP_ID
1 1
2 2,3
The IDs in all the columns above refer to the same ID (Example - a Department)
My Expected output (by joining both the tables)
GROUP_ID NAME
1 Person1
2,3 Person2,Person3
Is there a query with which I can achieve this.

It can be done. You shouldn't do it, but perhaps you don't have the power to change the world. (If you have a say in it, you should normalize your table design - in your case, both the input and the output fail the first normal form).
Answering more as good practice for myself... This solution guarantees that the names will be listed in the same order as the id's. It is not the most efficient, and it doesn't deal with id's in the list that are not found in the first table (it simply discards them instead of leaving a marker of some sort).
with
table_1 ( id, name ) as (
select 1, 'Person1' from dual union all
select 2, 'Person2' from dual union all
select 3, 'Person3' from dual
),
table_2 ( id, group_id ) as (
select 1, '1' from dual union all
select 2, '2,3' from dual
),
prep ( id, lvl, token ) as (
select id, level, regexp_substr(group_id, '[^,]', 1, level)
from table_2
connect by level <= regexp_count(group_id, ',') + 1
and prior id = id
and prior sys_guid() is not null
)
select p.id, listagg(t1.name, ',') within group (order by p.lvl) as group_names
from table_1 t1 inner join prep p on t1.id = p.token
group by p.id;
ID GROUP_NAMES
---- --------------------
1 Person1
2 Person2,Person3

select t2.group_id, listagg(t1.name,',') WITHIN GROUP (ORDER BY 1)
from table2 t2, table1 t1
where ','||t2.group_id||',' like '%,'||t1.id||',%'
group by t2.id, t2.group_id
Normalize you data model, this perversion !!! Сomma separated list should not exist in database. Only individual rows per data unit.

Related

Obtaining one description from multiple domain tables with Oracle

How can I merge descriptions obtained from three domain tables into a single description column?
There is a transaction table that has transaction ID's and three domain tables that between them have the descriptions for the transaction IDs - something like this:
Transaction Table: TANS_TBL with columns like TRANS_ID, TRANS_START_TM, TRANS_END_TM, TRANS_RESULT_CD
Domain Table 1: DMN_TANS_DESC_TBL1 with columns like TRANS_ID, DMN1_SHORT_DESC, DMN1_LONG_DESC
Domain Table 2: DMN_TANS_DESC_TBL2 with columns like TRANS_ID, DMN2_SHORT_DESC, DMN2_LONG_DESC
Domain Table 3: DMN_TANS_DESC_TBL3 with columns like TRANS_ID, DMN3_SHORT_DESC, DMN3_LONG_DESC
The rows for the TRANS_ID and short descriptions are not unique
in each table. A TRANS_ID may have multiple rows in a Domain Table.
Only the Short Description is needed; only one row for a
TRANS_ID is wanted.
The column names for descriptions are
different in each domain table.
Any given TRANS_ID will appear in only one domain table (I believe other things in the application will break if that is not true, but I don't see anything to enforce that)
Data needs to be extracted with the column headers like this:
TRANS_ID, TRANS_SHORT_DESC, TRANS_START_TM, TRANS_END_TM
No table modifications or additions are permitted.
Using this, the descriptions can be obtained:
select trns.TRANS_ID, dmn1.DMN1_SHORT_DESC, dmn2.DMN2_SHORT_DESC, dmn3.DMN3_SHORT_DESC
from TRANS_TBL trns
left join DMN_TANS_DESC_TBL1 dmn1 ON dmn1.TRANS_ID=trns.TRANS_ID
left join DMN_TANS_DESC_TBL2 dmn2 ON dmn2.TRANS_ID=trns.TRANS_ID
left join DMN_TANS_DESC_TBL3 dmn3 ON dmn3.TRANS_ID=trns.TRANS_ID;
`
However that has two problems:
Duplicate rows for each domain table description row, and
There are three description columns, two out of three NULL, for each row
One description row from one domain table can be obtained with this:
select TRANS_ID, TRANS_DESC
from ( select dmn1.TRANS_ID, dmn1.DMN1_SHORT_DESC as "TRANS_DESC", row_number()
over( partision by dmn1.TRANS_ID ORDER by dmn1.TRANS_ID) as row_num
from DM_TANS_DESC_TBL1 dmn1
)
where row_num=1;
But I haven't found a way to bring those descriptions from the 3 domain tables into a single transaction ID description column.
You can simply use COALESCE or nested NVL or a DECODE or a CASE statement to combine the columns into one. Something like:
SELECT trans_id,
trans_desc
FROM (SELECT trans_id,
trans_desc,
ROW_NUMBER() OVER (PARTITION BY trans_id ORDER BY DECODE(trans_desc,NULL,2,1) ASC, trans_id DESC) seq
FROM (SELECT trans_tbl.trans_id,
COALESCE(dmn1.dmn1_short_desc,dmn2.dmn2_short_desc,dmn3.dmn3_short_desc) trans_desc
FROM trans_tbl
left join DMN_TANS_DESC_TBL1 dmn1 ON dmn1.TRANS_ID=trns.TRANS_ID
left join DMN_TANS_DESC_TBL2 dmn2 ON dmn2.TRANS_ID=trns.TRANS_ID
left join DMN_TANS_DESC_TBL3 dmn3 ON dmn3.TRANS_ID=trns.TRANS_ID))
WHERE seq = 1
The DECODE in the ROW_NUMBER logic is in order to prefer non-null values over null values.
With your sample data something like here:
WITH
tbl (TRANS_ID, TRANS_START_TM, TRANS_END_TM) AS
(
Select 1, '09:00:00' , '09:01:00' From Dual Union All
Select 2, '09:12:00' , '09:15:00' From Dual Union All
Select 3, '09:16:00' , '09:17:00' From Dual Union All
Select 4, '09:21:00' , '09:22:00' From Dual Union All
Select 5, '09:23:00' , '09:27:00' From Dual
),
desc_tbl_1 (TRANS_ID, DMN1_SHORT_DESC) AS
(
Select 1, 'D1 T1 - some desc' From Dual Union All
Select 1, 'D1 T1 - some desc' From Dual
),
desc_tbl_2 (TRANS_ID, DMN2_SHORT_DESC) AS
(
Select 2, 'D2 T2 - some desc' From Dual Union All
Select 2, 'D2 T2 - some desc' From Dual Union All
Select 3, 'D2 T3 - some desc' From Dual
),
desc_tbl_3 (TRANS_ID, DMN3_SHORT_DESC) AS
(
Select 4, 'D3 T4 - some desc' From Dual Union All
Select 5, 'D3 T5 - some desc ' From Dual
),
... you could create a CTE descriptions to colect them in one column
descriptions (TRANS_ID, TRANS_SHORT_DESC, RN) AS
(
Select TRANS_ID, DMN1_SHORT_DESC, ROW_NUMBER() OVER(Partition By TRANS_ID Order By 1) From desc_tbl_1 Union All
Select TRANS_ID, DMN2_SHORT_DESC, ROW_NUMBER() OVER(Partition By TRANS_ID Order By 1) From desc_tbl_2 Union All
Select TRANS_ID, DMN3_SHORT_DESC, ROW_NUMBER() OVER(Partition By TRANS_ID Order By 1) From desc_tbl_3
)
Main SQL
Select t.TRANS_ID, d.TRANS_SHORT_DESC, t.TRANS_START_TM, t.TRANS_END_TM
From tbl t
Inner Join descriptions d ON(d.TRANS_ID = t.TRANS_ID And d.RN = 1)
Result:
TRANS_ID
TRANS_SHORT_DESC
TRANS_START_TM
TRANS_END_TM
1
D1 T1 - some desc
09:00:00
09:01:00
2
D2 T2 - some desc
09:12:00
09:15:00
3
D2 T3 - some desc
09:16:00
09:17:00
4
D3 T4 - some desc
09:21:00
09:22:00
5
D3 T5 - some desc
09:23:00
09:27:00
NOTE - If there is a possibility that some ID has no description from 3 domains then use Left Join and handle null value.

Select ids from array of numbers that are not in a Table

Given the following table data:
Students.id
-----------
1
2
3
If I want to select ids from a table that are not in an array of numbers, I can do the following:
SELECT id FROM Students
WHERE id NOT IN (1, 3, 5);
Result: (2)
But I want to do the opposite - select numbers from an array that are not in a table. How can I do that?
Here have been my attempts so far:
Attempt 1:
SELECT id FROM TABLE(1, 3, 5)
WHERE id NOT IN (SELECT id FROM Students);
RESULT: ORA-00907: missing right parenthesis
------------------------
Attempt 2:
SELECT (1, 3, 5) FROM dual
WHERE ??? NOT IN (SELECT id from Students); -- not sure what the column name should be
RESULT: Executing the first line alone gives the error ORA-00907: missing right parenthesis
--------------
Attempt 3:
SELECT TABLE(1,3,5) AS ids FROM dual
WHERE ids NOT IN (SELECT id FROM Students);
RESULT: ORA-00936: missing expression
Desired results (from my examples): (5)
Note: I found this related question, but alas it is also unanswered. Like Fuzz, my array of numbers are not stored in a table.
You can create a collection type:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TYPE NumbersList IS TABLE OF NUMBER;
and then you can use it in a query:
Query 1:
SELECT id
FROM students
WHERE id NOT MEMBER OF NumbersList( 1, 3, 5 )
Results:
| ID |
|----|
| 2 |
Or, to reverse it and have the numbers in the collection not in the table, you can use a table collection expression (TABLE(:your_collection)):
Query 2:
SELECT COLUMN_VALUE
FROM TABLE( NumbersList( 1, 3, 5 ) )
WHERE COLUMN_VALUE NOT IN ( SELECT id FROM students )
Results:
| COLUMN_VALUE |
|--------------|
| 5 |
You can even pass the collection as a bind variable (examples passing it from PL/SQL and from a java array).
You could use the table build in proper way eg: using union instead of an array :
select id from (
select 1 id from dual
union
select 3 from dual
union
select 5 from dual ) t
where t.id NOT IN (
SELECT id FROM Student
)
You may try anti joining a CTE containing your ID numbers to the Students table:
WITH cte AS (
SELECT 1 AS id FROM dual UNION ALL
SELECT 3 FROM dual UNION ALL
SELECT 5 FROM dual
)
SELECT t1.id
FROM cte t1
LEFT JOIN Students t2
ON t1.id = t2.id
WHERE t2.id IS NULL
Your requirement is fairly easy to come by, using a join or a few other methods, provided that your data is in a proper table. If you have a long term need for this, then I recommend getting your data into tables.
We could also write the above query using EXISTS, which might have better performance:
SELECT t1.id
FROM cte t1
WHERE NOT EXISTS (SELECT 1 FROM Students t2 WHERE t1.id = t2.id)

Remove duplicates using only where condition

Today, i got a problem from a friend.
Problem - Write a SQL query using UNION ALL(not union) that uses the where clause to eliminate duplicates.
I can not use group by expression
I can not use unique , distinct keywords.
Input -
id(Table 1)
1
2
fk_id(Table 2)
1
1
2
I gave him the solution below query
select id from
(
select id , row_number() over(partition by id order by id) rn from
(
select id from T1
union all
select fk_ID id from T2
)
)where rn = 1;
Output -
id
1
2
which is generating unique id's.
Now suspense by him i also can not use row_number(). i just have to use where condition. i am writing query on oracle database.
Please suggest.
Thanks in advance.
From its name and the data shown, we can assume that id in table t1 is unique.
From its name and the data shown, we can assume that fk_id in table t2 is a foreign key to table1.id.
So the union of the IDs in the two tables are simply the IDs that we find in table t1.
As we are forced to use UNION ALL on the two tables, though, we can use a pseudo UNION ALL not adding anything:
select id from t1
union all
select fk_id from t2 where 1 = 2;
If t2.fk_id were not a foreign key referencing t1.id, we would use NOT EXISTS or NOT IN in the where clause instead. If this is to give a result without duplicates, however, there must be no duplicates in t2 then to start with. (As you are showing that duplicate values in t2 do exist, this approach would not work then.) Here is a query for unique values from t1 plus unique values from t2 that are not referencing the t1 values:
select id from t1
union all
select fk_id from t2 where fk_id not in (select id from t1);
In a more generic case, where you can have duplicates in both tables, this could be a way.
test data:
create table table1(id) as (
select 1 from dual union all
select 1 from dual union all
select 2 from dual union all
select 2 from dual union all
select 1 from dual
)
create table table2(fk_id) as (
select 1 from dual union all
select 1 from dual union all
select 1 from dual union all
select 3 from dual union all
select 4 from dual union all
select 1 from dual union all
select 4 from dual union all
select 2 from dual
)
query:
with tab1_union_all_tab2 as (
select 'tab1'||rownum as uniqueId, id from table1 UNION ALL
select 'tab2'||rownum , fk_id from table2
)
select id
from tab1_union_all_tab2 u1
where not exists ( select 1
from tab1_union_all_tab2 u2
where u1.id = u2.id
and u1.uniqueId < u2.uniqueId
)
result:
ID
----------
3
4
1
2
This should clarify the idea behind:
with tab1_union_all_tab2 as (
select 'tab1'||rownum as uniqueId, id from table1 UNION ALL
select 'tab2'||rownum , fk_id from table2
)
select uniqueId, id,
( select nvl(listagg ( uniqueId, ', ') within group ( order by uniqueId), 'NO DUPLICATES')
from tab1_union_all_tab2 u2
where u1.id = u2.id
and u1.uniqueId < u2.uniqueId
) duplicates
from tab1_union_all_tab2 u1
UNIQUEID ID DUPLICATES
---------- ---------- --------------------------------------------------
tab11 1 tab12, tab15, tab21, tab22, tab23, tab26
tab12 1 tab15, tab21, tab22, tab23, tab26
tab13 2 tab14, tab28
tab14 2 tab28
tab15 1 tab21, tab22, tab23, tab26
tab21 1 tab22, tab23, tab26
tab22 1 tab23, tab26
tab23 1 tab26
tab24 3 NO DUPLICATES
tab25 4 tab27
tab26 1 NO DUPLICATES
tab27 4 NO DUPLICATES
tab28 2 NO DUPLICATES
As rightly observed by Thorsten Kettner, you can easily edit this to use rowid instead of building a unique id by concatenating a string and the rownum:
with tab1_union_all_tab2 as (
select rowid uniqueId, id from table1 UNION ALL
select rowid , fk_id from table2
)
select id
from tab1_union_all_tab2 u1
where not exists ( select 1
from tab1_union_all_tab2 u2
where u1.id = u2.id
and u1.uniqueId < u2.uniqueId
)
write a where statement for the second select in the union all as where id != fk_id

split string into several rows

I have a table with a string which contains several delimited values, e.g. a;b;c.
I need to split this string and use its values in a query. For example I have following table:
str
a;b;c
b;c;d
a;c;d
I need to group by a single value from str column to get following result:
str count(*)
a 1
b 2
c 3
d 2
Is it possible to implement using single select query? I can not create temporary tables to extract values there and query against that temporary table.
From your comment to #PrzemyslawKruglej answer
Main problem is with internal query with connect by, it generates astonishing amount of rows
The amount of rows generated can be reduced with the following approach:
/* test table populated with sample data from your question */
SQL> create table t1(str) as(
2 select 'a;b;c' from dual union all
3 select 'b;c;d' from dual union all
4 select 'a;c;d' from dual
5 );
Table created
-- number of rows generated will solely depend on the most longest
-- string.
-- If (say) the longest string contains 3 words (wont count separator `;`)
-- and we have 100 rows in our table, then we will end up with 300 rows
-- for further processing , no more.
with occurrence(ocr) as(
select level
from ( select max(regexp_count(str, '[^;]+')) as mx_t
from t1 ) t
connect by level <= mx_t
)
select count(regexp_substr(t1.str, '[^;]+', 1, o.ocr)) as generated_for_3_rows
from t1
cross join occurrence o;
Result: For three rows where the longest one is made up of three words, we will generate 9 rows:
GENERATED_FOR_3_ROWS
--------------------
9
Final query:
with occurrence(ocr) as(
select level
from ( select max(regexp_count(str, '[^;]+')) as mx_t
from t1 ) t
connect by level <= mx_t
)
select res
, count(res) as cnt
from (select regexp_substr(t1.str, '[^;]+', 1, o.ocr) as res
from t1
cross join occurrence o)
where res is not null
group by res
order by res;
Result:
RES CNT
----- ----------
a 2
b 2
c 3
d 2
SQLFIddle Demo
Find out more about regexp_count()(11g and up) and regexp_substr() regular expression functions.
Note: Regular expression functions relatively expensive to compute, and when it comes to processing a very large amount of data, it might be worth considering to switch to a plain PL/SQL. Here is an example.
This is ugly, but seems to work. The problem with the CONNECT BY splitting is that it returns duplicate rows. I managed to get rid of them, but you'll have to test it:
WITH
data AS (
SELECT 'a;b;c' AS val FROM dual
UNION ALL SELECT 'b;c;d' AS val FROM dual
UNION ALL SELECT 'a;c;d' AS val FROM dual
)
SELECT token, COUNT(1)
FROM (
SELECT DISTINCT token, lvl, val, p_val
FROM (
SELECT
regexp_substr(val, '[^;]+', 1, level) AS token,
level AS lvl,
val,
NVL(prior val, val) p_val
FROM data
CONNECT BY regexp_substr(val, '[^;]+', 1, level) IS NOT NULL
)
WHERE val = p_val
)
GROUP BY token;
TOKEN COUNT(1)
-------------------- ----------
d 2
b 2
a 2
c 3
SELECT NAME,COUNT(NAME) FROM ( SELECT NAME FROM ( (SELECT rownum as ID, REGEXP_SUBSTR('a;b;c', '[^;]+', 1, LEVEL ) NAME
FROM dual CONNECT BY REGEXP_SUBSTR('a;b;c', '[^;]+', 1, LEVEL) IS NOT NULL))
UNION ALL (SELECT NAME FROM ( (SELECT rownum as ID, REGEXP_SUBSTR('b;c;d', '[^;]+', 1, LEVEL ) NAME
FROM dual CONNECT BY REGEXP_SUBSTR('b;c;d', '[^;]+', 1, LEVEL) IS NOT NULL)))
UNION ALL
(SELECT NAME FROM ( (SELECT rownum as ID, REGEXP_SUBSTR('a;c;d', '[^;]+', 1, LEVEL ) NAME
FROM dual CONNECT BY REGEXP_SUBSTR('a;c;d', '[^;]+', 1, LEVEL) IS NOT NULL)))) GROUP BY NAME
NAME COUNT(NAME)
----- -----------
d 2
a 2
b 2
c 3

Inserting rows where column can have many values

I am writing a stored proc that inserts rows into a table. The issue is that many of the columns can have a list of different values and all of the rows in the db need to reflect these values. For example:
I have a table: Table1(state, number)
state will need to be 1-50 as its value and number is 1-3. There needs to be a row for each state with each number.
(1,1)
(1,2)
(1,3)
(2,1)...etc
There has got to be a nice way to do this but my research has not been fruitful. Does anyone have any suggestions?
A good way to generate the values is using a cross join. Here is an example:
insert into table(state, number)
select s.state, n.number
from (select 'AK' as state union all select 'AL' union all . . .
) s cross join
(select 1 as number union all select 2 union all select 3
) n
You may already have a lists of states and/or numbers, in which case you can use this. For example:
insert into table(state, number)
select s.state, n.number
from (select state from states
) s cross join
(select 1 as number union all select 2 union all select 3
) n
Your need is a cross join between two tables, one containing 50 rows, the other 3 rows.
In Oracle:
select *
from
(
select rownum as state
from dual
connect by rownum <= 50
) t1
,
(
select rownum as num
from dual
connect by rownum <= 3
) t2
Fiddle