Oracle CONNECT BY with multiple tables - oracle

I have 4 tables containing my data:
Table COMP: definition of my component data
COMPID | NAME | DESCRIPTION
--------+-----------+------------
000123 | Comp. 1 | A44.123
000277 | Comp. 2 | A96.277
000528 | Comp. 3 | 1235287
001024 | Comp. 4 | Lollipop
004711 | Comp. 5 | Yippie
Table COMPLIST: containing the sub-components of each component
COMPID | POS | SUBCOMPID | QUANTITY
--------+------+------------ +-----------
000123 | 1 | 000277 | 3
000123 | 2 | 000528 | 1
000528 | 1 | 004711 | 1
Table COMPSUPPLIER: definition of the components suppliers
COMPID | SUPPLIER | ORDERNUMBER
--------+-----------+-------------
000123 | Supp1 | A44.123
000277 | Supp1 | A96.277
000528 | Supp2 | 1235287
001024 | Supp2 | ux12v39
004711 | Supp1 | 123456
Table ASSEMBLY: definition of my assembly
ASSYID | POS | COMPID | QUANTITY
--------+------+---------+----------
5021 | 1 | 000123 | 1
5021 | 2 | 001024 | 2
I want to get all components used in an assembly with their supplier and order number (Edited: added Position):
POS | COMPID | NAME | SUPPLIER | ORDERNUMBER | QUANTITY
-------|---------+---------+----------+-------------+----------
1 | 000123 | Comp. 1 | Supp1 | A44.123 | 1
1.1 | 000277 | Comp. 2 | Supp1 | A96.277 | 3
1.2 | 000528 | Comp. 3 | Supp2 | 1235287 | 1
1.2.1 | 004711 | Comp. 5 | Supp1 | 123456 | 1
2 | 001024 | Comp. 4 | Supp2 | ux12v39 | 2
My idea was to use a SELECT in combination with CONNECT BY but I can't get it working right.
My current approach (Edited: updated with GurV's input):
SELECT c.COMPID, c.NAME, cs.SUPPLIER, cs.ORDERNUMBER
FROM COMP c
JOIN COMPSUPPLIER cs ON c.COMPID = cs.COMPID
WHERE c.COMPID in (
SELECT COMPID
FROM ASSEMBLY
WHERE ASSYID = '5021'
UNION ALL
SELECT SUBCOMPID
FROM COMPLIST
CONNECT BY NOCYCLE PRIOR SUBCOMPID = COMPID
START WITH COMPID in (
SELECT COMPID
FROM ASSEMBLY
WHERE ASSYID = '5402')
);
With this I get all my sub components but not the position. Is it possible to get also the position column somehow?

A standard hierarchical query will work for this problem. I see in your desired output that you don't have a column for assyid; if you have more than one assembly in your business, that's a flaw. Also, I thought at some point you will want to compute the total quantity of a sub-component for an assembly (say, screws are used in component a and also in component b, both part of assembly 1000, and you would need the total number of screws); but, since you want to show everything "in its proper hierarchy" (as reflected in the pos column), it seems you aren't interested in that, at least in this query. That would be harder to do with a standard hierarchical query and easier to do in a recursive query, but that doesn't seem to be the case here.
The idea is to union all between complist and assembly, adding a flag column to use in the start with clause of the hierarchical query. Everything else is pretty standard.
with
comp ( compid, name, description ) as (
select '000123', 'Comp. 1', 'A44.123' from dual union all
select '000277', 'Comp. 2', 'A96.277' from dual union all
select '000528', 'Comp. 3', '1235287' from dual union all
select '001024', 'Comp. 4', 'Lollipop' from dual union all
select '004711', 'Comp. 5', 'Yippie' from dual
),
Complist ( compid, pos, subcompid, quantity ) as (
select '000123', 1, '000277', 3 from dual union all
select '000123', 2, '000528', 1 from dual union all
select '000528', 1, '004711', 1 from dual
),
compsupplier ( compid, supplier, ordernumber ) as (
select '000123', 'Supp1', 'A44.123' from dual union all
select '000277', 'Supp1', 'A96.277' from dual union all
select '000528', 'Supp2', '1235287' from dual union all
select '001024', 'Supp2', 'ux12v39' from dual union all
select '004711', 'Supp1', '123456' from dual
),
assembly ( assyid, pos, compid, quantity ) as (
select '5021', 1, '000123', 1 from dual union all
select '5021', 2, '001024', 2 from dual
)
select h.assyid, ltrim(h.pos, '.') as pos, h.compid,
c.name, s.supplier, s.ordernumber, h.quantity
from (
select subcompid as compid, quantity,
connect_by_root compid as assyid,
sys_connect_by_path(pos, '.') as pos
from ( select complist.*, 'f' as flag from complist
union all
select assembly.*, null as flag from assembly
)
start with flag is null
connect by compid = prior subcompid
) h
left outer join comp c on h.compid = c.compid
left outer join compsupplier s on h.compid = s.compid
;
Output:
ASSYID POS COMPID NAME SUPPLIER ORDERNUMBER QUANTITY
------ -------- ------ ------- -------- ----------- ----------
5021 1 000123 Comp. 1 Supp1 A44.123 1
5021 1.1 000277 Comp. 2 Supp1 A96.277 3
5021 1.2 000528 Comp. 3 Supp2 1235287 1
5021 1.2.1 004711 Comp. 5 Supp1 123456 1
5021 2 001024 Comp. 4 Supp2 ux12v39 2
5 rows selected.

If I'm following your logic, you can use recursive subquery factoring instead of a hierarchical query, which makes cycles etc. a bit easier to cope with:
with rcte (position, compid, name, supplier, ordernumber, quantity) as (
select to_char(a.pos), a.compid, c.name, cs.supplier, cs.ordernumber, a.quantity
from assembly a
join compsupplier cs on cs.compid = a.compid
join comp c on c.compid = cs.compid
where a.assyid = 5021
union all
select rcte.position ||'.' || cl.pos, cl.subcompid, c.name,
cs.supplier, cs.ordernumber, cl.quantity
from rcte
join complist cl on cl.compid = rcte.compid
join compsupplier cs on cs.compid = cl.subcompid
join comp c on c.compid = cs.compid
)
select *
from rcte;
POSITION COMPID NAME SUPPL ORDERNU QUANTITY
---------- ------ ------- ----- ------- ----------
1 000123 Comp. 1 Supp1 A44.123 1
2 001024 Comp. 4 Supp2 ux12v39 2
1.1 000277 Comp. 2 Supp1 A96.277 3
1.2 000528 Comp. 3 Supp2 1235287 1
1.2.1 004711 Comp. 5 Supp1 123456 1
The anchor member gets the first two rows direct from the assembly data, including the position from that table - that's essentially your original (pre-Gurv) query, plus the position.
The recursive member then looks at complist for each generated row's compid existing as a subcompid, and appends its position to the parent's while getting the other relevant data from the other tables.
If you want to preserve the order as you showed it in the question, you can add additional columns to the recursive CTE that track the original position and the level you're currently at (possibly with other info to break ties, if they are possible), and exclude those from the final select list:
with rcte (position, compid, name, supplier, ordernumber, quantity,
order_by_1, order_by_2)
as (
select to_char(a.pos), a.compid, c.name, cs.supplier, cs.ordernumber, a.quantity,
a.pos, 1
from assembly a
join compsupplier cs on cs.compid = a.compid
join comp c on c.compid = cs.compid
where a.assyid = 5021
union all
select rcte.position ||'.' || cl.pos, cl.subcompid, c.name,
cs.supplier, cs.ordernumber, cl.quantity,
rcte.order_by_1, rcte.order_by_2 + 1
from rcte
join complist cl on cl.compid = rcte.compid
join compsupplier cs on cs.compid = cl.subcompid
join comp c on c.compid = cs.compid
)
select position, compid, name, supplier, ordernumber, quantity
from rcte
order by order_by_1, order_by_2;
POSITION COMPID NAME SUPPL ORDERNU QUANTITY
---------- ------ ------- ----- ------- ----------
1 000123 Comp. 1 Supp1 A44.123 1
1.1 000277 Comp. 2 Supp1 A96.277 3
1.2 000528 Comp. 3 Supp2 1235287 1
1.2.1 004711 Comp. 5 Supp1 123456 1
2 001024 Comp. 4 Supp2 ux12v39 2

Related

SELECT Records > 0 and with NO NULL Values

I have a query in which I am producing results with rows that contain 0 values. I would like to exclude any rows in which columns B or C = 0. To exclude such rows, I have added the T2.A <> 0 and T2.A != 0. When I do this, the 0 values are replaced with NULLs. Thus I also added T2.A IS NOT NULL.
My results still produce the columns that I do not need which show (null) and would like to exclude these.
SELECT
(SELECT
SUM(T2.A) as prem
FROM Table_2 T2, Table_2 T1
WHERE T2.ENT_REF = T1.ENT_REF
AND UPPER(T2.PER) = 'HURR'
AND UPPER(T2.ENT_TYPE) = 'POL'
AND T2.Cov NOT IN ('OUTPROP','COV')
AND T2.A <> 0
AND T2.A IS NOT NULL
) as B,
(SELECT
SUM(T2.A) as prem
FROM Table_2 T2, Table_2 T1
WHERE T2.ENT_REFE = T1.ENT_REF
AND UPPER(T2.PER) IN ('I', 'II', 'II')
AND UPPER(T2.ENT_TYPE) = 'POL'
AND T2.Cov NOT IN ('OUTPROP','COV')
AND T2.A <> 0
AND T2.A IS NOT NULL
) as C
Ideally the result will go from:
+----+--------+--------+
| ID | B | C |
+----+--------+--------+
| 1 | 24 | 123 |
| 2 | 65 | 78 |
| 3 | 43 | 89 |
| 3 | 0 | 0 |
| 4 | 95 | 86 |
| 5 | 43 | 65 |
| 5 | (null) | (null) |
+----+--------+--------+
To something similar to the following:
+----+-----+-----+
| ID | B | C |
+----+-----+-----+
| 1 | 24 | 123 |
| 2 | 65 | 78 |
| 3 | 43 | 89 |
| 4 | 95 | 86 |
| 5 | 43 | 65 |
+----+-----+-----+
I have also attempted distinct values, but I have other columns such as dates which are different per row. Although I need to include dates, they are not as important to me as only getting B and C columns with only values > 0. I have also tried using a GROUP BY ID statement, but I get an error that states 'ORA-00979: not a GROUP BY expression'
You have written all the conditions in the SELECT clause.
You are facing the issue because the WHERE clause decides the number of rows to be fetched and SELECT clause decides values to be returned.
In your case, something like the following is happening:
Simple Example:
-- MANUAL DATA
WITH DATAA AS (
SELECT
1 KEY,
'VALS' VALUE,
1 SEQNUM
FROM
DUAL
UNION ALL
SELECT
2,
'IDEAL OPTION',
2
FROM
DUAL
UNION ALL
SELECT
10,
'EXCLUDE',
3
FROM
DUAL
)
-- QUERY OF YOUR TYPE
SELECT
(
SELECT
KEY
FROM
DATAA I
WHERE
I.KEY = 1
AND O.KEY = I.KEY
) AS KEY, -- DECIDE VALUES TO BE SHOWN
(
SELECT
KEY
FROM
DATAA I
WHERE
I.SEQNUM = 1
AND O.SEQNUM = I.SEQNUM
) AS SEQNUM -- DECIDE VALUES TO BE SHOWN
FROM
DATAA O
WHERE
O.KEY <= 2; -- DECIDES THE NUMBER OF RECORDS
OUTPUT:
If you don't want to change much logic in your query then just use additional WHERE clause outside your final query like:
SELECT <bla bla bla>
FROM <YOUR FINAL QUERY>
WHERE B IS NOT NULL AND C IS NOT NULL
Cheers!!
I guess you were on the right track, trying to group values.
In order to do that, columns (that are supposed to be distinct) will be left alone (such as ID in the following example), while the rest should be aggregated (using min, max or any other you find appropriate).
For example, as you said that there's some date column you don't care about - I mean, which one of them you'll select - then select the first one (i.e. min(date_column)). Similarly, you'd do with the rest. The group by clause should contain all non-aggregated columns (id in this example).
select id,
sum(a) a,
sum(b) b,
min(date_column) date_column
from your_current_query
group by id
If I understand your query right, it would be much easier and more performant, to avoid the lookups in the Select clause. Try to bring it all in one Query:
SELECT * FROM (
SELECT T2.ENT_REF AS ID,
SUM(CASE WHEN UPPER(T2.PER) = 'HURR' THEN T2.A END) AS B,
SUM(CASE WHEN UPPER(T2.PER) IN ('I', 'II', 'II') THEN T2.A END) as C
FROM Table_2 T2
WHERE UPPER(T2.ENT_TYPE) = 'POL'
AND T2.Cov NOT IN ('OUTPROP','COV')
GROUP BY T2.ENT_REF
)
WHERE B IS NOT NULL
OR C IS NOT NULL

Selecting only distinct record from table in oracle

I have table with following records;
ID | NN | MBL | IC | OTHER
---+-----+------+----+------
1 | 123 | | | ac
2 | | 544 | | dc
3 | | | 524| df
4 |527 | | 124| ff
5 |123 | | | tr // duplicate NN of ID 1
6 | | 544 | | op // duplicate MBL of ID 2
7 | | | 124| ii // duplicate for IC ID 4
When querying with select I need just records with single entry, skipping second occurrence,
select
ID, NN, MBL, IC, OTHER
from
TABLE1 // this should return only one entry of any NN, MBL and IC
How do I get this, I cannot use distinct for multiple columns and I also need ID and OTHER column to display in select query
Expecting result like this:
1 | 123 | | | ac
2 | | 544 | | dc
3 | | | 524| df
4 |527 | | 124| ff
You can use the analytical function ROW_NUMBER() to calculate ranks over each column you want and filter only these rows with rank = 1.
Here is an example:
WITH testdata AS (
SELECT 1 AS ID, 123 AS NN, NULL AS MBL, NULL AS IC, 'ac' AS OTHER FROM DUAL UNION ALL
SELECT 2, NULL, 544 , NULL, 'dc' FROM DUAL UNION ALL
SELECT 3, NULL, NULL, 524 , 'df' FROM DUAL UNION ALL
SELECT 4, 527, NULL, 124, 'ff' FROM DUAL UNION ALL
SELECT 5, 123, NULL, NULL, 'tr' FROM DUAL UNION ALL
SELECT 6, NULL, 544, NULL, 'op' FROM DUAL UNION ALL
SELECT 7, NULL, NULL , 124, 'ii' FROM DUAL
)
SELECT *
FROM(SELECT ID,
NN,
CASE WHEN NN IS NULL THEN 1 ELSE ROW_NUMBER() OVER (PARTITION BY NN ORDER BY ID) END AS NN_RANG,
MBL,
CASE WHEN MBL IS NULL THEN 1 ELSE ROW_NUMBER() OVER (PARTITION BY MBL ORDER BY ID) END AS MBL_RANG,
IC,
CASE WHEN IC IS NULL THEN 1 ELSE ROW_NUMBER() OVER (PARTITION BY IC ORDER BY ID) END AS IC_RANG,
OTHER
FROM testdata
)
WHERE NN_RANG = 1
AND MBL_RANG = 1
AND IC_RANG = 1
;
Hope it helps.

Oracle Lead by group

Hello I've got a problem with lead and retrieving the next value from the next group.
I've got this table:
TableA
-----------------
ID | value
-----------------
1 | 2.5
1 | 1
1 | 4.5
2 | 7
2 | 2
3 | 3
4 | 1
4 | 7
Expected result:
------------------------------
ID | value | lead_id
------------------------------
1 | 2.5 | 2
1 | 1 | 2
1 | 4.5 | 2
2 | 7 | 3
2 | 2 | 3
3 | 3 | 4
4 | 1 | NULL
4 | 7 | NULL
My SQL:
select ID, value, lead(id) OVER (order by id) lead_id from TableA
Is it possible to get that result ?
You can do this by adding in a windowing clause into the first_value analytic function:
with tablea as (select 1 id, 2.5 value from dual union all
select 1 id, 1 value from dual union all
select 1 id, 4.5 value from dual union all
select 2 id, 7 value from dual union all
select 2 id, 2 value from dual union all
select 3 id, 3 value from dual union all
select 4 id, 1 value from dual union all
select 4 id, 7 value from dual)
select id,
value,
first_value(id) over (order by id
range between 1 following and unbounded following) lead_id
from tablea;
ID VALUE LEAD_ID
---------- ---------- ----------
1 2.5 2
1 1 2
1 4.5 2
2 7 3
2 2 3
3 3 4
4 1
I think this gives the right output:
WITH g AS
(SELECT ID, lead(ID) OVER (ORDER BY ID) lead_id
FROM (SELECT DISTINCT ID FROM TableA) )
SELECT ID, VALUE, lead_id
FROM TableA
JOIN g USING (ID)
ORDER BY 1;
SELECT tablea.*, b.nextid FROM tablea
INNER JOIN (SELECT id, LEAD (id) OVER (ORDER BY id) nextid
FROM ( SELECT DISTINCT id
FROM tablea
ORDER BY id)) b
ON tablea.id = b.id
This should work.

Oracle "partition" a table at each new value

I have an Oracle table I need to "partition" :I use the terme loosely, I just need to detect groups and would like to display the group through a SELECT. Here's an example that might serve as a sample data (the four columns):
ID | Ref | Rank | Partition_group (only available for the 1st member)
1 | 1 | 1 | 1_A
2 | 1 | 2 | (null)
3 | 1 | 3 | 1_B
4 | 2 | 1 | (null)
5 | 2 | 2 | 2_A
...
It is sorted (the sort key would be the 'Ref' and a creation date). What I would need here, is to extract three groups:
IDs 1 and 2
ID 3
ID 5
What happens with ID 4 is not really important: it may be in its own group, or with the ID 5.
Two IDS should be in the same group if they have the same 'Ref' and if there hasn't been any 'Partition_group' change. In other words, at each change of 'Ref' or (logical or) 'Partition_group', I need to detect a new group. For instance, we could return something like that:
ID | Ref | Rank | Partition_group | Group
1 | 1 | 1 | 1_A | 1_A
2 | 1 | 2 | (null) | 1_A
3 | 1 | 3 | 1_B | 1_B
4 | 2 | 1 | (null) | (null) (or 2_A)
5 | 2 | 2 | 2_A | 2_A
...
I thought about writing a function or something, but it appears I don't have the rights to do so (yeah...) so I have to use plain Oracle SQL (11g).
I've been looking at CONNECT BY and OVER (analytical functions) but they don't seem to do the trick.
Has anyone been faced to such a problem? How would you resolve it?
Thanks in advance.
Assuming the input data is the first four columns, how about something like:
with sample_data as (select 1 id, 1 ref, 1 rank, '1_A' ptn_group from dual union all
select 2 id, 1 ref, 2 rank, null ptn_group from dual union all
select 3 id, 1 ref, 3 rank, '1_B' ptn_group from dual union all
select 4 id, 2 ref, 1 rank, null ptn_group from dual union all
select 5 id, 2 ref, 2 rank, '2_A' ptn_group from dual)
select id,
ref,
rank,
ptn_group,
last_value(ptn_group ignore nulls) over (partition by ref order by rank, id) grp1,
case when last_value(ptn_group ignore nulls) over (partition by ref order by rank, id) is null then
first_value(ptn_group ignore nulls) over (partition by ref order by rank, id rows between current row and unbounded following)
else last_value(ptn_group ignore nulls) over (partition by ref order by rank, id)
end grp2
from sample_data;
ID REF RANK PTN_GROUP GRP1 GRP2
---------- ---------- ---------- --------- ---- ----
1 1 1 1_A 1_A 1_A
2 1 2 1_A 1_A
3 1 3 1_B 1_B 1_B
4 2 1 2_A
5 2 2 2_A 2_A 2_A
I've given you two options to generate the grp, based on how you want to deal with rows where the first rows of the ptn_group are null - leave them null or pick up the first non-null value in the group.

How to make an efficient UNION in Oracle?

I'm using Oracle 11g.
I have 2 related tables: stored values (A) and new values to insert (B). Both are related between them with an id of 3 columns (client, group and personcode). Each table has about 20 other columns (let's call them attributes).
I have to match them so I can know which values are new (id in B and not in A) so I insert them in A, which are equals (id in B and in A with the same attributes) and which are not in the new values (id in A but not in B anymore), so I delete them from the stored values (A).
For instance:
A:
client | group | personcode | name | surname
_________________________________________________
1 | 1 | 1 | joe | doe
1 | 1 | 2 | carl | john
1 | 1 | 3 | john | john
B:
client | group | personcode | name | surname
_________________________________________________
1 | 1 | 1 | joe | doe
1 | 1 | 3 | john | john
1 | 1 | 4 | mary | con
In this example, person 4 is new, person 2 should be deleted and 1 and 3 remains the same.
So, I need a query which returns the following results:
client | group | personcode | action
_________________________________________
1 | 1 | 1 | equal
1 | 1 | 2 | remove
1 | 1 | 3 | equal
1 | 1 | 4 | new
What I've made is the following query:
WITH
A AS (
-- select from A table
),
B AS
(
-- select from B table
),
delete AS
(
-- select from A WHERE NOT EXISTS (B.id = A.ID)
),
news AS
(
-- select from B WHERE NOT EXISTS (A.id = B.ID)
),
eq AS
(
-- select A.* from A, B WHERE A.id = B.id AND A.attributes = B.attributes
)
select action.client, action.group, action.personcode, 'remove' from delete action
UNION ALL
select action.client, action.group, action.personcode, 'new' from news action
UNION ALL
select action.client, action.group, action.personcode, 'equal' from eq action
;
The problem is that, although each of those 3 lasts selects runs in less than 10 seconds, when I merge them using UNION or UNION ALL, the complete query lasts about 90 seconds, even if delete or new or equal are empty. It could be more than 3000 rows in A or in B.
Is there any way to get this results in a better, faster way?
You could outer join the tables to produce a log of the differences between them.
select coalesce(a.id,b.id) id,
case when a.id is null
then 'new'
when b.id is null
then 'remove'
when a.col1 = b.col1 and a.col2 = b.col2 ...
then 'same'
else 'different'
end
from a full outer join b on (a.id = b.id)
If the table B has the data that you want, why do you not use that table instead of that in table A? Create a synonym that points to the one with the correct data in it and reference that.
Well, thanks all for your reply.
I've finally made a view to which I pass some parameters to filter the first two queries, using the strategy described in this blog
The complete process lasts 30 secs now, and 0 if there are no rows at A or B (before, it lasts 90 secs always).
This is the solution which less affects my current procedures.

Resources