Combine two table in Oracle Sql - sql

I have two tables.
One is
ID REFPARTS
-- --------
100 '1,2'
101 '1'
Second table
PART_ID AMOUNT
------ --------
1 50
2 25
Final Table
ID REFPARTS AMOUNT
-- -------- -------
100 '1,2' 75
101 '1' 50
How can I get final table?
Thanks in advance.

This is a horrible data model. You should not be storing lists of numeric ids in a comma delimited string. SQL has a wonderful way to represent lists. It is called a "table", not a "string".
In your case, you could do:
select t1.id, sum(t2.amount)
from table1 t1 join
table2 t2
on replace(t1.refparts, ',', ''',''') like '%''' || t2.partid || '''%'
group by t1.id;
Even though you can do this using string manipulation, you should put your efforts into fixing the data model.

I am splitting the comma seperated string to rows in my with clause "data" this is followed by a join with the second table and grouping by the id values.
create table t(id int, refparts varchar2(100))
insert into t values(100,'1,2');
insert into t values(101,'1');
create table t2(part_id int, amount int);
insert into t2 values(1,50);
insert into t2 values(2,25);
with data
as (
select a.id
,rtrim(
substr(a.refparts|| ','
,instr(','||a.refparts||',',',',1,lvl)
,instr(','||a.refparts||',',',',1,lvl+1) - instr(','||a.refparts||',',',',1,lvl)
)
,',') as col2
from t a
join (select level as lvl
from dual
connect by level<=10) b
on b.lvl <=length(a.refparts) - length(replace(a.refparts,',','')) + 1
)
select a.id
,sum(b.amount) as summed_val
from data a
join t2 b
on a.col2=b.part_id
group by a.id

WITH tab1 AS (
SELECT 100 AS id_, '1,2' AS refparts FROM dual
UNION ALL
SELECT 101 AS id_, '1' AS refparts FROM dual
),
tab2 AS (
SELECT 1 AS part_id , 50 AS amount FROM dual
UNION ALL
SELECT 2 AS part_id , 25 AS amount FROM dual
)
SELECT t1.id_, t1.refparts, SUM(t2.amount)
FROM (
SELECT DISTINCT id_, trim(regexp_substr(refparts, '[^,]+', 1, LEVEL)) refparts
FROM tab1
CONNECT BY instr(refparts, ',', 1, LEVEL - 1) > 0
) t1Splited
INNER JOIN tab1 t1 ON t1.id_ = t1Splited.id_
INNER JOIN tab2 t2 ON t2.part_id = t1Splited.refparts
GROUP BY t1.id_, t1.refparts
ORDER BY t1.id_
;

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.

Can "value in list or list is empty" be written shorter?

Given this SQL:
select * from table1
where
table1.columnFoo = 123
and
(
some_value is null
or
some_value in (select column1 from table2 where table1.colX=table2.colY)
or
not exists (select column1 from table2 where table1.colX=table2.colY)
);
-- some_value is a constant or an input parameter in an (PL/)SQL procedure
-- if it is non null, then we want to filter by it. Except if the list selection is empty.
Is there a way to write the "in list or list is empty" part shorter?
Preferably in a way that contains the list only once (see the Don't_repeat_yourself principle )
I'm interested for Oracle SQL or PL/SQL, but other information is also welcome.
As requested, a MRE that works in SQL*Plus:
create table table1 as select 1 id, 'one' name , 12 price from dual
union select 2 , 'two' , 22 from dual
union select 3 , 'thr' , 33 from dual;
create table table2 as select 1 id1, 88 idX, sysdate-1 validDate from dual -- valid
union select 1 , 99 , sysdate+2 from dual -- these two are not valid (yet)
union select 2 , 99 , sysdate+3 from dual;
var some_value number
--exec :some_value := 3 -- uncomment for non null values
with cte as (select id1,idX from table2 where validDate<sysdate)
select * from table1
where
table1.price > 10
and
(
:some_value is null
or
:some_value in (select idX from cte where table1.id=cte.id1)
or
not exists (select idX from cte where table1.id=cte.id1)
);
From Oracle 12, you could use a LATERAL join with conditional aggregation:
SELECT t1.*
FROM table1 t1
CROSS JOIN LATERAL(
SELECT 1 AS matched
FROM table2 t2
WHERE t1.colX=t2.colY
HAVING COUNT(*) = 0
OR COUNT(CASE t2.column1 WHEN :some_value THEN 1 END) > 0
) t2
WHERE t1.columnFoo = 123
AND ( :some_value is null OR t2.matched = 1);
Or a similar technique using EXISTS:
select *
from table1
WHERE columnFoo = 123
AND ( :some_value is null
OR EXISTS(
SELECT 1
FROM table2
WHERE table1.colX=colY
HAVING COUNT(*) = 0
OR COUNT(CASE column1 WHEN :some_value THEN 1 END) > 0
)
);
db<>fiddle here

Query to return dynamic number of rows using SQL in SQL Server 2012

I have a unique requirement to return number of result rows in multiples of 10. Example, if actual data rows are 3, I must add another 7 blank rows to make it 10. If actual data rows are 16, I must add another 4 blank rows to make it 20, and so on.
Without using a procedure, is it possible to achieve this using SELECT statement?
The blank rows can simply contain NULL values or spaces or zeroes.
You can assume any simple query for data rows; the objective is to understand how to return rows dynamically in multiples of 10.
Example:
Select EmpName FROM Employees
If there are 3 employees, I should still return 10 rows, with the balance 7 rows containing either NULL value or blanks.
I am using SQL Server 2012.
This is very raw idea how it can be achieved:
WITH data(r) AS (
SELECT 1 r FROM dual
UNION ALL
SELECT r+1 r FROM data WHERE r < 10
)
SELECT sd.*
FROM data d
left join some_data sd on d.r = sd.id
This is dual table structure:
create table dual (dummy varchar(1));
insert into dual values ('x');
Fiddle: http://sqlfiddle.com/#!6/5ffcc/4
One of the possible options is this:
WITH data(r) AS (
SELECT 1 r FROM dual
UNION ALL
SELECT r+1 r FROM data WHERE r < 10
)
SELECT sd.*
FROM
(select r, row_number() over (order by r) rn from data) d
left join (
select id, name, row_number() over (order by id) rn from some_data sd
) sd
on d.rn = sd.rn
The obvious disadvantages of this colutions:
'r' value generation rule most probably is not as simple in your
case.
Number of rows must be known before query execution.
But maybe it will help you to find better solution.
Here's another, fairly easy, way to handle it...
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
DROP TABLE #TestData;
CREATE TABLE #TestData (
EmpID INT NOT NULL,
EmpName VARCHAR(20) NOT NULL
);
INSERT #TestData(EmpID, EmpName) VALUES
(47, 'Bob'),(33, 'Mary'), (88, 'Sue');
-- data as it exists...
SELECT
td.EmpID,
td.EmpName
FROM
#TestData td;
-- the desired output...
WITH
cte_AddRN AS (
SELECT
td.EmpID,
td.EmpName,
RN = ROW_NUMBER() OVER (ORDER BY td.EmpName)
FROM
#TestData td
),
cte_TenRows AS (
SELECT n.RN FROM ( VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10) ) n (RN)
)
SELECT
ar.EmpID,
ar.EmpName
FROM
cte_TenRows tr
LEFT JOIN cte_AddRN ar
ON tr.RN = ar.RN
ORDER BY
tr.RN;
Results...
-- data as it exists...
EmpID EmpName
----------- --------------------
47 Bob
33 Mary
88 Sue
-- the desired output...
EmpID EmpName
----------- --------------------
47 Bob
33 Mary
88 Sue
NULL NULL
NULL NULL
NULL NULL
NULL NULL
NULL NULL
NULL NULL
NULL NULL
Based on the above 2 answers, here is what I did:
WITH DATA AS
(SELECT EmpName FROM Employees),
DataSummary AS
(SELECT COUNT(*) AS NumDataRows FROM DATA),
ReqdDataRows AS
(SELECT CEILING(NumDataRows/10.0)*10 AS NumRowsReqd FROM DataSummary),
FillerRows AS
(
SELECT 1 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 2 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 3 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 4 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 5 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 6 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 7 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 8 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 9 AS SLNO, '00000' AS FillerCol
UNION ALL
SELECT 10 AS SLNO, '00000' AS FillerCol
)
SELECT * FROM DATA
--UNION ALL
--SELECT CONVERT(VARCHAR(10), NumDataRows) FROM DataSummary
--UNION ALL
--SELECT CONVERT(VARCHAR(10), NumRowsReqd) FROM ReqdDataRows
UNION ALL
SELECT FillerCol FROM FillerRows
WHERE (SELECT NumDataRows FROM DataSummary) + SLNO <= (SELECT NumRowsReqd FROM ReqdDataRows)
This gives me the output what I want. This avoids use of ROW_NUMBER and ORDERing. The table FillerRows can be further simplified using SELECT * FROM (VALUES...), and the 2nd and 3rd table DataSummary and ReqdDataRows can be merged into a single SELECT statement.
This approach is a step by step approach and easy to understand and debug, like:
Get the actual data rows
Get count of the data rows
Calculate required no. data rows
UNION the actual data rows with filler rows
Any suggestions on further simplifying this are welcome.

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

ORACLE join two table with comma separated ids

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.