How to copy data along a dimension while avoiding unique name constraint issues in Oracle - oracle

Database is Oracle 18c
Data info:
We have the temporal notion of Seasons, and we have a set of Business Groups in each Season. My table, AGROUP, has columns(ID*[auto-generated], NAME, SEASON_ID). Group Names must be unique by Season. Said another way, I can have two groups with the same name ONLY if they are in different seasons.
Task At Hand:
I need to create a stored procedure that, given a source season and a target season, can copy all the groups from the source season to the target season. This part is easy enough. The naive solution is:
PROCEDURE COPY_GROUPS(IN_SOURCE_SEASON_ID IN SEASON.ID%TYPE,
IN_TARGET_SEASON_ID IN SEASON.ID%TYPE)
AS
WITH SOURCE AS
(SELECT AG.ID AS AG_ID,
AG.NAME,
AG.SEASON_ID
FROM AGROUP AG
WHERE AG.SEASON_ID = IN_SOURCE_SEASON_ID)
INSERT INTO AGROUP (NAME, SEASON_ID)
SELECT SRC.NAME,
IN_TARGET_SEASON_ID AS SEASON_ID,
FROM SOURCE SRC;
COMMIT;
END;
The tricky bit is that it is possible that there exists a group in the target season that already has the same name as a group in the source season. Without intervention, this would generate a unique constraint error. In this case, we want to copy the group but re-name it to something unique. Any method to rename is fine, but my first thought is simply to rename with a suffixed number. To do this, I suppose I would change things around to open a REFCURSOR and loop through, inserting one at a time and catching the unique constraint error and responding with a re-attempt to insert with a number suffixed. I'm really in-experienced with routing error handling like this in Oracle however. I could really use some pointing in the right direction.
Thanks for the help!

I played around with this a bit. I'd say to try to do the conditional logic in a case statement as per the following example. You'll need a better way to generate sequences though.
/* INSERT INTO AGROUP (id, NAME, SEASON_ID) */
WITH agroup as
(
select 1 id, 'A' name, 1 season_id from dual union
select 2 id, 'A' name, 2 season_id from dual union
select 3 id, 'B' name, 1 season_id from dual union
select 4 id, 'C' name, 2 season_ud from dual
)
, SOURCE AS
(SELECT rownum row_num, AG.ID AS AG_ID,
AG.NAME,
AG.SEASON_ID
FROM AGROUP AG
WHERE AG.SEASON_ID = 1 /* source season */
)
SELECT /* sequence generation */ (select max(id) from agroup) + row_num as id
, case when SRC.NAME in (select name
from agroup
where agroup.season_id = 2 /* target season */
and agroup.name = src.name
)
then src.name || ' ' || to_char(sysdate, 'DD-MON-YYYY hh24:MI:SS')
else src.name
end as name
, 2 /* target season */ AS SEASON_ID
FROM SOURCE SRC
;

Related

Applying where condition on Casted number coulmn in ORACLE from a inner query result

For Sample purpose lets create a table with below schema and fill some sample values
CREATE TABLE games(ID INT ,Name VARCHAR(20));
INSERT INTO games(ID,Name) VALUES (2008,'Beijing');
INSERT INTO games(ID,Name) VALUES (2012,'London');
INSERT INTO games(ID,Name) VALUES (2012,12);
INSERT INTO games(ID,Name) VALUES (2012,654);
Output:
ID NAME
2008 Beijing
2012 London
2012 12
2012 654
In the above table we have both number and string data in the name column lets write a query that uses the REGX to filter only the numerical rows
SELECT TO_NUMBER(Name)as Trimmed FROM games where REGEXP_LIKE(Name, '(?<=\s|^)\d+(?=\s|$)', '')
Output:
TRIMMED
12
654
Now here is the problem if write a where clause of getting values greater than 12 from the above result it throws invalid number.
Select * from (SELECT TO_NUMBER(Name)as Trimmed FROM games where REGEXP_LIKE(Name, '(?<=\s|^)\d+(?=\s|$)', '')) T1 where T1.Trimmed >12 ;
I found this is how the oracle query planning works but is there any other way i can achieve this
This will work:
Select * from
(SELECT Name as Trimmed
FROM games where REGEXP_LIKE(Name, '(?<=\s|^)\d+(?=\s|$)', '')) T1
where to_number(T1.Trimmed) >12 ;
Unfortunately you need a subquery. It can't be done with one where.
This can be done in a single query:
with
inputs as (
select 2008 as id, 'Beijing' as name from dual union all
select 2012 , 'London' from dual union all
select 2012 , '12' from dual union all
select 2012 , '654' from dual
)
select id, name
from inputs
where translate(name, 'a0123456789', 'a') is null
and to_number(regexp_replace(name, '[^[:digit:]]', '')) > 12
;
ID NAME
---------- -------
2012 654
1 row selected.
regexp_replace removes all the characters except digits, so the test can be done regardless of what the name is. If there are no digits in the name, the result is NULL, which can be converted to number (it is still null).
The translate solution for testing for "all-digits" is more efficient than using regexp_like. The odd-looking 'a' in translate is needed due to an oddity in the translate function itself (see the documentation). This test is not needed if all the names are either "all letters" or "all digits" (if "all letters", the second test with regexp_replace would suffice); but the first test is needed if names like "Sydney 2000" are possible in the name column.

can I do insert in update of merge(Implementation SCD type 2)

I have source table and a target table I want to do merge such that there should always be insert in the target table. For each record updated there should ne a flag updated to 'Y' and when this in something is changed then record flag value should be chnaged to 'N' and a new row of that record is inserted in target such that the information of record that is updated should be reflected. Basically I want to implement SCD type2 . My input data is-
student_id name city state mobile
1 suraj bhopal m.p. 9874561230
2 ravi pune mh 9874563210
3 amit patna bihar 9632587410
4 rao banglore kr 9236547890
5 neel chennai tn 8301456987
and when my input chnages-
student_id name city state mobile
1 suraj indore m.p. 9874561230
And my output should be like-
surr_key student_id name city state mobile insert_Date end_date Flag
1 1 suraj bhopal m.p.9874561230 31/06/2015 1/09/2015 N
2 1 suraj indore m.p.9874561230 2/09/2015 31/12/9999 Y
Can anyone help me how can I do that?
You can do this with the use of trigger ,you can create before insert trigger on your target table which will update flag column of your source table.
Or you can have after update trigger on source table which will insert record in your target table.
Hope this helps
Regards,
So this should be the outline of your procedure steps. I used different columns in source and target for simplification.
Source (tu_student) - STUDENT_ID, NAME, CITY
Target (tu_student_tgt)- SKEY, STUDENT_ID, NAME, CITY, INSERT_DATE, END_DATE, IS_ACTIVE
The basic idea here is
Find the new records from source which are missing in target and Insert it. Set start_date as sysdate, end_date as 9999 and IsActive to 1.
Find the records which are updated (like your Bhopal -> Indore case). So we have to do 2 operations in target for it
Update the record in target and set end date as sysdate and IsActive to 0.
Insert this record in target which has new values. Set start_date as sysdate, end_date as 9999 and IsActive = 1.
-- Create a new oracle sequence (test_utsav_seq in this example)
---Step 1 - Find new inserts (records present in source but not in target
insert into tu_student_tgt
(
select
test_utsav_seq.nextval as skey,
s.student_id as student_id,
s.name as name,
s.city as city,
sysdate as insert_date,
'31-DEC-9999' as end_date,
1 as Flag
from tu_student s
left outer join
tu_student_tgt t
on s.student_id=t.student_id
where t.student_id is null)
----Step 2 - Find skey which needs to be updated due to data chage from source and target. So get the active records from target and compare with source data. If mismatch found, we need to
-- a update this recods in target and mark it as Inactive.
-- b Insert a new record for same student_id with new data and mark it Active.
-- part 2a - find updates.
--these records need update. Save these skey and use it one by one while updating.
select t.skey
from tu_student s inner join
tu_student_tgt t
on s.student_id=t.student_id
where t.Flag = 1 and
(s.name!=t.name or
s.city!=t.city)
--2 b ) FInd the ids which needs to be inserted as they changed in source from target. Now as above records are marked inactive,
select s.student_id
from tu_student s inner join
tu_student_tgt t
on s.student_id=t.student_id
where t.Flag = 1 and
(s.name!=t.name or
s.city!=t.city)
---2a - Implement update
-- Now use skey from 2a in a loop and run update statements like below. Replace t.key = with the keys which needs to be updated.
update tu_student_tgt t
set t.student_id = (select s.student_id from tu_student s,tu_student_tgt t where s.student_id=t.student_id and t.key= -- id from 2a step . )
, t.name=(select s.name from tu_student s,tu_student_tgt t where s.student_id=t.student_id and t.key= --id from 2a step. )
, end_date = sysdate
, is_active = 0
where t.skey = -- id from 2a step
---2b Implement Insert use student_id found in 2a
--Insert these student id like step 1
insert into tu_student_tgt
(
select
test_utsav_seq.nextval as skey,
s.student_id as student_id,
s.name as name,
s.city as city,
sysdate as insert_date,
'31-DEC-9999' as end_date,
1 as Flag
from tu_student s
where s.student_id = -- ID from 2b step - Repeat for other ids
I cannot give you a simple example of SCD-2. If you understand SCD-2, you should understand this implementation.

recursive cte working very slow

I want to Group the rows based on certain columns, i.e. if data is same in these columns in continuous rows, then assign same Group Number to them, and if its changed, assign new one. This become complex as the same data in the columns could appear later in some other rows, so they have to be given another Group Number as they are not in continuous rows with previous group.
I used cte for this purpose and it is giving correct output also, but is so slow that iterating over 75k+ rows takes about 15 minutes. The code I used is:
WITH
cte AS (SELECT ROW_NUMBER () OVER (ORDER BY Patient_ID, Opnamenummer, SPECIALISMEN, Opnametype, OntslagDatumTijd) AS RowNumber,
Opnamenummer, Patient_ID, AfdelingsCode, Opnamedatum, Opnamedatumtijd, Ontslagdatum, Ontslagdatumtijd, IsSpoedopname, OpnameType, IsNuOpgenomen, SpecialismeCode, Specialismen
FROM t_opnames)
SELECT * INTO #ttt FROM cte;
WITH cte2 AS (SELECT TOP 1 RowNumber,
1 AS GroupNumber,
Opnamenummer, Patient_ID, AfdelingsCode, Opnamedatum, Opnamedatumtijd, Ontslagdatum, Ontslagdatumtijd, IsSpoedopname, OpnameType, IsNuOpgenomen, SpecialismeCode, Specialismen
FROM #ttt
ORDER BY RowNumber
UNION ALL
SELECT c1.RowNumber,
CASE
WHEN c2.Afdelingscode <> c1.Afdelingscode
OR c2.Patient_ID <> c1.Patient_ID
OR c2.Opnametype <> c1.Opnametype
THEN c2.GroupNumber + 1
ELSE c2.GroupNumber
END AS GroupNumber,
c1.Opnamenummer,c1.Patient_ID,c1.AfdelingsCode,c1.Opnamedatum,c1.Opnamedatumtijd,c1.Ontslagdatum,c1.Ontslagdatumtijd,c1.IsSpoedopname,c1.OpnameType,c1.IsNuOpgenomen, SpecialismeCode, Specialismen
FROM cte2 c2
JOIN #ttt c1 ON c1.RowNumber = c2.RowNumber + 1
)
SELECT *
FROM cte2
OPTION (MAXRECURSION 0) ;
DROP TABLE #ttt
I tried to improve performance by putting output of cte in a temp table. That increased the performance, but still its too slow. So, how can I increase the performance of this code to run it under 10 seconds for 75k+ records? The output before cancelling the query is: Screenshot. As visible from the image, data is same in columns Afdelingscode,Patient_ID and Opnametype in RowNumber 3,5 and 6, but they have different GroupNumber because of concurrency of the rows.
Without data its not that easy to test but i would try first to not use temporary table and just use both cte from start to end, ie;
;WITH
cte AS (...),
cte2 AS (...)
select * from cte2
OPTION (MAXRECURSION 0);
Without knowing indices etc... for instance, you do a lot of ordering in the first cte. Is this supported by indices (or one multicolumn index) or not?
Without the data i don't have the option to play with it but looking at this:
CASE
WHEN c2.Afdelingscode <> c1.Afdelingscode
OR c2.Patient_ID <> c1.Patient_ID
OR c2.Opnametype <> c1.Opnametype
THEN c2.GroupNumber + 1
ELSE c2.GroupNumber
i would try to take a look at partition by statement in row_number
So try to run this:
WITH
cte AS (
SELECT ROW_NUMBER () OVER (PARTITION BY Afdelingscode , Patient_ID ,Opnametype ORDER BY Patient_ID, Opnamenummer, SPECIALISMEN, Opnametype, OntslagDatumTijd ) AS RowNumber,
Opnamenummer, Patient_ID, AfdelingsCode, Opnamedatum, Opnamedatumtijd, Ontslagdatum, Ontslagdatumtijd, IsSpoedopname, OpnameType, IsNuOpgenomen
FROM t_opnames)

Fastest way of doing field comparisons in the same table with large amounts of data in oracle

I am recieving information from a csv file from one department to compare with the same inforation in a different department to check for discrepencies (About 3/4 of a million rows of data with 44 columns in each row). After I have the data in a table, I have a program that will take the data and send reports based on a HQ. I feel like the way I am going about this is not the most efficient. I am using oracle for this comparison.
Here is what I have:
I have a vb.net program that parses the data and inserts it into an extract table
I run a procedure to do a full outer join on the two tables into a new table with the fields in one department prefixed with '_c'
I run another procedure to compare the old/new data and update 2 different tables with detail and summary information. Here is code from inside the procedure:
DECLARE
CURSOR Cur_Comp IS SELECT * FROM T.AEC_CIS_COMP;
BEGIN
FOR compRow in Cur_Comp LOOP
--If service pipe exists in CIS but not in FM and the service pipe has status of retired in CIS, ignore the variance
If(compRow.pipe_num = '' AND cis_status_c = 'R')
continue
END IF
--If there is not a summary record for this HQ in the table for this run, create one
INSERT INTO t.AEC_CIS_SUM (HQ, RUN_DATE)
SELECT compRow.HQ, to_date(sysdate, 'DD/MM/YYYY') from dual WHERE NOT EXISTS
(SELECT null FROM t.AEC_CIS_SUM WHERE HQ = compRow.HQ AND RUN_DATE = to_date(sysdate, 'DD/MM/YYYY'))
-- Check fields and update the tables accordingly
If (compRow.cis_loop <> compRow.cis_loop_c) Then
--Insert information into the details table
INSERT INTO T.AEC_CIS_DET( Fac_id, Pipe_Num, Hq, Address, AutoUpdatedFl,
DateTime, Changed_Field, CIS_Value, FM_Value)
VALUES(compRow.Fac_ID, compRow.Pipe_Num, compRow.Hq, compRow.Street_Num || ' ' || compRow.Street_Name,
'Y', sysdate, 'Cis_Loop', compRow.cis_loop, compRow.cis_loop_c);
-- Update information into the summary table
UPDATE AEC_CIS_SUM
SET cis_loop = cis_loop + 1
WHERE Hq = compRow.Hq
AND Run_Date = to_date(sysdate, 'DD/MM/YYYY')
End If;
END LOOP;
END;
Any suggestions of an easier way of doing this rather than an if statement for all 44 columns of the table? (This is run once a week if it matters)
Update: Just to clarify, there are 88 columns of data (44 of duplicates to compare with one suffixed with _c). One table lists each field in a row that is different so one row can mean 30+ records written in that table. The other table keeps tally of the number of discrepencies for each week.
First of all I believe that your task can be implemented (and should be actually) with staight SQL. No fancy cursors, no loops, just selects, inserts and updates. I would start with unpivotting your source data (it is not clear if you have primary key to join two sets, I guess you do):
Col0_PK Col1 Col2 Col3 Col4
----------------------------------------
Row1_val A B C D
Row2_val E F G H
Above is your source data. Using UNPIVOT clause we convert it to:
Col0_PK Col_Name Col_Value
------------------------------
Row1_val Col1 A
Row1_val Col2 B
Row1_val Col3 C
Row1_val Col4 D
Row2_val Col1 E
Row2_val Col2 F
Row2_val Col3 G
Row2_val Col4 H
I think you get the idea. Say we have table1 with one set of data and the same structured table2 with the second set of data. It is good idea to use index-organized tables.
Next step is comparing rows to each other and storing difference details. Something like:
insert into diff_details(some_service_info_columns_here)
select some_service_info_columns_here_along_with_data_difference
from table1 t1 inner join table2 t2
on t1.Col0_PK = t2.Col0_PK
and t1.Col_name = t2.Col_name
and nvl(t1.Col_value, 'Dummy1') <> nvl(t2.Col_value, 'Dummy2');
And on the last step we update difference summary table:
insert into diff_summary(summary_columns_here)
select diff_row_id, count(*) as diff_count
from diff_details
group by diff_row_id;
It's just rough draft to show my approach, I'm sure there is much more details should be taken into account. To summarize I suggest two things:
UNPIVOT data
Use SQL statements instead of cursors
You have several issues in your code:
If(compRow.pipe_num = '' AND cis_status_c = 'R')
continue
END IF
"cis_status_c" is not declared. Is it a variable or a column in AEC_CIS_COMP?
In case it is a column, just put the condition into the cursor, i.e. SELECT * FROM T.AEC_CIS_COMP WHERE not (compRow.pipe_num = '' AND cis_status_c = 'R')
to_date(sysdate, 'DD/MM/YYYY')
That's nonsense, you convert a date into a date, simply use TRUNC(SYSDATE)
Anyway, I think you can use three single statements instead of a cursor:
INSERT INTO t.AEC_CIS_SUM (HQ, RUN_DATE)
SELECT comp.HQ, trunc(sysdate)
from AEC_CIS_COMP comp
WHERE NOT EXISTS
(SELECT null FROM t.AEC_CIS_SUM WHERE HQ = comp.HQ AND RUN_DATE = trunc(sysdate));
INSERT INTO T.AEC_CIS_DET( Fac_id, Pipe_Num, Hq, Address, AutoUpdatedFl, DateTime, Changed_Field, CIS_Value, FM_Value)
select comp.Fac_ID, comp.Pipe_Num, comp.Hq, comp.Street_Num || ' ' || comp.Street_Name, 'Y', sysdate, 'Cis_Loop', comp.cis_loop, comp.cis_loop_c
from T.AEC_CIS_COMP comp
where comp.cis_loop <> comp.cis_loop_c;
UPDATE AEC_CIS_SUM
SET cis_loop = cis_loop + 1
WHERE Hq IN (Select Hq from T.AEC_CIS_COMP)
AND trunc(Run_Date) = trunc(sysdate);
They are not tested but they should give you a hint how to do it.

Need Oracle query for a BOM IMPLOSION or BOM "WHERE-USED" on Oracle 10g

Working with Oracle DB 10g.
I am tasked with writing an Oracle query for a BOM IMPLOSION commonly known as BOM "WHERE-USED." Essentially, given an item or part, I need to provide a list of parent items that contain that item or part, if any.
I have recently coded a BOM EXPLOSION using the following SQL which utilizes the START WITH and CONNECT BY syntax to create a heirarchy downward from a parent item. I found inspiration for the BOM EXPLOSION query at http://www.confluentminds.com/Trainings/SCM/Topic1.1_Ch1_Part5.html
Current BOM EXPLOSION code:
/* BOM EXPLOSION */
select distinct
level,
sys_connect_by_path(msib.segment1, ' / ') as "PATH",
msib2.segment1 as "CHILD ITEM AT LEVEL/PATH"
/*bic.component_item_id,*/
/*msib.inventory_item_id,*/
/*msib2.inventory_item_id*/
from bom.bom_components_b bic,
bom.bom_structures_b bom,
inv.mtl_system_items_b msib,
inv.mtl_system_items_b msib2
where 1=1
and bic.bill_sequence_id = bom.bill_sequence_id
and bic.disable_date is null
and bom.assembly_item_id = msib.inventory_item_id
and bom.organization_id = msib.organization_id
and bic.component_item_id = msib2.inventory_item_id
and bom.organization_id = msib2.organization_id
and bom.organization_id = #### /* organization id here */
and bic.effectivity_date < sysdate
and bom.alternate_bom_designator is null
start with msib.segment1 = '$$$$$$$$$$' /* top parent item here */
connect by nocycle prior bic.component_item_id = msib.inventory_item_id
order by level
Now, I need go from any child item and list all parent items that contain that child item.
I have searched for "oracle bom implosion" and "oracle bom where used" but nothing is obvious to me. The BOM IMPLOSION seems far less straightforward than the BOM EXPLOSION.
Any help or advice is greatly appreciated.
Clint Van Zee
EDIT 02-NOV-2011:
Yes, I want to traverse up a BOM heirarchy and list those items or components where a specified component is used in the bill.
Based on your answer, I looked into the "connect by" relationship and "start with." I think I have a way to do this. I may have been close to the answer without knowing it.
Craig, here is your mock-up with modifications added to prove this out. I also modified the "connect by" and "start with." It {should!} start with the child component and go "upwards" to list those models or components that "use" the specified starting component. To do this, I also removed the "prior" keyword.
with data
as
(
select 'topmodel1' id, 'component1' child_id from dual union all
select 'topmodel1' id, 'component3' child_id from dual union all
select 'component2' id, 'component5' child_id from dual union all
select 'component3' id, 'component4' child_id from dual union all
select 'component4' id, 'component5' child_id from dual union all
select 'component5' id, null child_id from dual union all
select 'topmodel2' id, 'component1' child_id from dual union all
select 'topmodel2' id, 'component5' child_id from dual union all
select 'component5' id, null child_id from dual
)
select distinct
sys_connect_by_path(id, '/') path, child_id, level
from data
start with child_id = 'component5'
connect by id = child_id
order by level
This produces the following result:
PATH CHILD_ID LEVEL
----------- ---------- -----
/component2 component5 1
/component4 component5 1
/topmodel2 component5 1
Looking at the mock data, component 5 is "used by" component2, component4, and topmodel2. So, this change seems to do what I intend. I had to add the dreaded "distinct" as it was traversing the same paths more than once.
I think this is getting me closer.
Here are the minimal changes to my BOM EXPLOSION code to do this in reverse:
START WITH msib.segment1 = '$$$$$$$$$$' /* child item here */
CONNECT BY nocycle msib.inventory_item_id = bic.component_item_id
ORDER BY level
When I apply this change to my SQL script and try it against the actual Oracle data, the CBO is reporting that the cost of searching just one component is out into the stratosphere. So, this method needs tuning.
I am not 100% sure what you are looking for, but it sounds like you are just wanting to climb up the tree instead of down. This is still possible using the hierarchical query, you just need to change your start with and connect by criteria.
So take a simple example:
with data
as
(
select 1 id, 2 child_id from dual union all
select 1 id, 3 child_id from dual union all
select 2 id, 5 child_id from dual union all
select 3 id, 4 child_id from dual union all
select 4 id, 5 child_id from dual union all
select 5 id, null child_id from dual
)
select sys_connect_by_path(id, '/') path, level
from data
start with id = 1
connect by prior child_id = id;
That will give you the child paths starting from the top of the tree. To change this to start at a point in the tree and move upwards, you would simply replace the start with / connect by with something like:
start with id = 4 -- or wherever in the tree you want to start and move upwards
connect by prior id = child_id;
Hopefully that helps. If not, if you could give an example of what you are wanting your output to look like it would be very beneficial.

Resources