Writing complicated row level trigger In Oracle - oracle

I have to create a row level trigger on a table called Movies that will compute the five greatest movie ratings from this Movies table and insert a new table called TopMovies with these movies. Each time I put a new rating in the Movies table this trigger fires. I am really struggling to understand how to go about this.
The Movies table has the following attributes (Movie, Director, Rating)
The Top Movies table will have (Rating)
So far I have just the following code. I really am lost as to how to make it so that once I have inserted at least 5 ratings into the movies table, the TopMovies table will delete the lowest rated movie and re-populate it with a new movie. I am not asking for the answer but any help to go in the right direction
Create of replace Trigger top_trigger
After insert or update on Movies
For each row
Begin
insert into TopMovies values (:new.rating),
End

You can use the following triggers. But be aware that concurrent requests may change the expected behavior when you use the triggers.
Trigger solution:
CREATE OR REPLACE TRIGGER TOP_TRIGGER AFTER
INSERT OR UPDATE ON MOVIES
FOR EACH ROW
DECLARE
LV_CNT NUMBER := 0;
LV_RATING TOPMOVIES.RATING%TYPE;
BEGIN
SELECT COUNT(1), MIN(RATING)
INTO LV_CNT, LV_RATING
FROM TOPMOVIES;
IF LV_CNT >= 5 AND :NEW.RATING > LV_RATING THEN
DELETE FROM TOPMOVIES WHERE RATING = LV_RATING;
END IF;
IF LV_CNT < 5 OR ( LV_CNT >= 5 AND :NEW.RATING > LV_RATING ) THEN
INSERT INTO TOPMOVIES VALUES ( :NEW.RATING );
END IF;
END;
/
The better solution is to use the view.
VIEW solution:
CREATE OR REPLACE VIEW TOPMOVIES_VW AS
SELECT * FROM
(
SELECT T.*,
DENSE_RANK() OVER( ORDER BY RATING DESC) AS RANK#
FROM MOVIES T
)
WHERE RANK# <= 5;
Cheers!!

You may try below code -
Create of replace Trigger top_trigger
After insert or update on Movies
Declare
v_movie_count number:= 0;
Begin
select count(*)
into v_movie_count
from Movies;
if v_movie_count >= 5 then
delete from TopMovies;
INSERT INTO TopMovies
SELECT *
FROM (SELECT Rating, ROWNUM RN
FROM Movies
ORDER BY Rating)
WHERE RN <= 5;
/* You can use below statement if you are using 12C or higher*/
/*INSERT INTO TopMovies
SELECT Rating, ROWNUM RN
FROM Movies
ORDER BY Rating DESC
FETCH FIRST 5 ROWS ONLY;*/
end if;
End

Related

Is there any faster way to perform merge on 120k records weekly in oracle pl/sql?

There are around 120k records in the database, and based on a few functions I calculate scores for all the records, weekly I have to update the table with new records and respective scores.
Below is a procedure that I am using to merge data into the table:
create or replace procedure scorecalc
AS
score1 number;
score2 number;
score3 number;
CURSOR cur IS
SELECT Id_number from tableA;
r_num cur%ROWTYPE;
BEGIN
--OPEN cur;
FOR r_num IN cur
LOOP
select functionA(r_num.id_number),functionb(r_num.id_number),functionc(r_num.id_number) into score1, score2,score3 from dual;
Merge into scores A USING
(Select
r_num.id_number as ID, score1 as scorea, score2 as scoreb, score3 as scorec, TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM DUAL) B
ON ( A.ID = B.ID and A.scoredate = B.scoredate)
WHEN NOT MATCHED THEN
INSERT (
ID, scorea, scoreb, scorec, scoredate)
VALUES (
B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate)
WHEN MATCHED THEN
UPDATE SET
A.scorea = B.scorea,
A.scoreb = B.scoreb,
A.scorec = B.scorec;
COMMIT;
END LOOP;
END;
whereas functionA/ B/ C has complex queries, joins in it to calculate the score.
Please suggest me any way to improve the performance because currently with this snippet of code I am only able to insert some 2k records in 1 hour? Can I use parallel DML here?
Thank you!
Why are you doing this in a procedure? This could all be done via DML:
MERGE INTO scores a USING
(SELECT ta.id_number AS ID,
functionA(ta.id) AS scoreA,
functionB(ta.id) AS scoreB,
functionC(ta.id) AS scoreC,
TO_DATE(sysdate, 'DD/MM/YYYY') as scoredate
FROM tableA ta) b
ON (a.id = b.id AND a.scoredate = b.scoredate)
WHEN MATCHED THEN UPDATE SET
a.scorea = b.scorea,
a.scoreb = b.scoreb,
a.scorec = b.scorec
WHEN NOT MATCHED THEN INSERT (ID, scorea, scoreb, scorec, scoredate)
VALUES (B.ID, B.scorea, B.scoreb, B.scorec,B.scoredate);
If you want to try using PARALLEL hint after that, feel free. But you should definitely get rid of that cursor and stop doing "Slow-by-slow" processing.
Something I've had some success with has been inserting from a select statement. It is pretty performant as it doesn't involve row by row inserting.
In your case, I'm thinking it would be something like:
INSERT INTO table (ID, scorea, scoreb, scorec, scoredate)
SELECT functionA(id_number), functionB(id_number), functionC(id_number)
FROM tableA
An example of this can be found at the link below:
https://docs.oracle.com/cd/B12037_01/appdev.101/b10807/13_elems025.htm
To schedule it just put the statement #Del in a procedure block;
create or replace procedure Saturday_Night_Merge is
begin
<Put the merge statement here>
end Saturday_Night_Merge;

Oracle PL/SQL update based on values calculated from multiple tables

I need to write a PL/SQL that sums up the value from 2 tables and update the value to another table.
I have the following tables: ONLINE_SALES, STORE_SALES, TOTAL_SALES
Assume the tables are structured like this:
ONLINE_SALES: OS_ID, STORE_ID, SEQ, ITEM_NAME, PRICE, PURCHASED_DATE
STORE_SALES: SS_ID, STORE_ID, SEQ, ITEM_NAME, PRICE, PURCHASED_DATE
TOTAL_SALES: STORE_ID, YEAR, TOTAL_INCOME
I want to write a PL/SQL that runs monthly and sums up the income (values in PRICE field) made in the month from both ONLINE_SALES and STORE_SALES of each store (identified by STORE_ID) and add the value to record in TOTAL_SALES with relative year.
My idea is to first filter the record by PURCHASED_DATE from both table with a SELECT, loop through all selected rows and sum to a variable and at last update the result with an UPDATE. But I am stuck in the first step since I found that I cannot use SELECT and only SELECT INTO is available.
Any ideas on how such PL/SQL can be written?
This solution assumes your stored procedure takes a DATE which it uses to identify the month and the year which is being totalled. Perhaps your assignment is expecting a different form of input? No worries. The deriving the values of year and date range is separate from the main process, so it is easy to swap in some different logic.
The loop uses an explicit cursor with the FOR UPDATE syntax. This locks the TOTAL_SALES table, which means you are guaranteed to be able to update all the rows.
create or replace calc_total_sales
( p_month in date )
is
cursor c_tot_sales (p_year number) is
select * from total_sales
where year = p_year
for update of total_income;
c_tot_sales c_tot_sales%rowtype;
l_os_sales number;
l_ss_sales number;
l_year number;
1_first_day date;
1_last_day date;
begin
l_year := to_number( to_char( p_month, 'yyyy') );
l_first_day := trunc( p_month, 'mm');
l_last_day := last_day( p_month);
open c_tot_sales( l_year );
loop
fetch c_tot_sales into r_tot_sales;
exit when c_tot_sales%not found;
select sum(price)
into l_os_sales
from online_sales
where store_id = r_tot_sales.store_id
and purchased_date >= l_first_day
and purchased_date <= l_last_day;
select sum(price)
into l_ss_sales
from store_sales
where store_id = r_tot_sales.store_id
and purchased_date >= l_first_day
and purchased_date <= l_last_day;
update total_sales
set total_income = total_income + nvl(l_ss_sales,0) + nvl(l_os_sales,0)
where current of c_total_sales;
end loop;
close c_tot_sales;
commit;
end;
/

Insert into select statement on the same table

I'm currently migrating data from legacy system to the current system.
I have this INSERT statement inside a stored procedure.
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
SELECT LEGACY_ID PRIMARY_ID
, (SELECT COUNT(*) + 1
FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = L1.LEGACY_ID) SEQUENCE_ID
, L1.DESCR
FROM LEGACY_TABLE L1;
However, whenever I have multiple values of LEGACY_ID from LEGACY_TABLE, the query for the SEQUENCE_ID doesn't increment.
Why is this so? I can't seem to find any documentation on how the INSERT INTO SELECT statement works behind the scenes. I am guessing that it selects all the values from the table you are selecting and then inserts them simultaneously after, that's why it doesn't increment the COUNT(*) value?
What other workarounds can I do? I cannot create a SEQUENCE because the SEQUENCE_ID must be based on the number of PRIMARY_ID that are present. They are both primary ids.
Thanks in advance.
Yes, The SELECT will be executed FIRST and only then the INSERT happens.
A Simple PL/SQL block below, will be a simpler approach, though not efficient.
DECLARE
V_SEQUENCE_ID NUMBER;
V_COMMIT_LIMIT:= 20000;
V_ITEM_COUNT := 0;
BEGIN
FOR REC IN (SELECT LEGACY_ID,DESCR FROM LEGACY_TABLE)
LOOP
V_SEQUENCE_ID:= 0;
SELECT COUNT(*)+1 INTO V_SEQUENCE_ID FROM TABLE_1 T1
WHERE T1.PRIMARY_ID = REC.LEGACY_ID
INSERT INTO TABLE_1
(PRIMARY_ID, SEQUENCE_ID, DESCR)
VALUES
(REC.LEGACY_ID,V_SEQUENCE_ID,REC.DESCR);
V_ITEM_COUNT := V_ITEM_COUNT + 1;
IF(V_ITEM_COUNT >= V_COMMIT_LIMIT)
THEN
COMMIT;
V_ITEM_COUNT := 0;
END IF;
END LOOP;
COMMIT;
END;
/
EDIT: Using CTE:
WITH TABLE_1_PRIM_CT(PRIMARY_ID, SEQUENCE_ID) AS
(
SELECT L1.LEGACY_ID,COUNT(*)
FROM LEGACY_TABLE L1
LEFT OUTER JOIN TABLE_1 T1
ON(L1.LEGACY_ID = T1.PRIMARY_ID)
GROUP BY L1.LEGACY_ID
)
INSERT INTO TABLE_1
(SELECT L1.LEGACY_ID,
CTE.SEQUENCE_ID+ (ROW_NUMBER() OVER (PARTITION BY L1.LEGACY_ID ORDER BY null)),
L1.DESCR
FROM TABLE_1_PRIM_CT CTE, LEGACY_TABLE L1
WHERE L1.LEGACY_ID = CTE.PRIMARY_ID);
PS: With your Millions of Rows, this is going to create a temp table
of same size, during execution. Do Explain Plan before actual execution.

Making joins using rowid in PLSQL

I was wondering if the following code is a good practice
CURSOR c_price_hist_parent IS
select tran_type, reason, event,
unit_cost, unit_retail, selling_unit_retail,
selling_uom, multi_units, multi_unit_retail,
multi_selling_uom
from price_hist
where rowid = ( SELECT row_id
from ( SELECT rowid row_id
FROM price_hist
WHERE item = l_item_parent
and tran_type in (4,8)
and loc = l_location
and ACTION_DATE <= l_create_date
order by action_date desc
)
where rownum = 1
);
If we delete a row and then insert the same row all the columns remain same but the rowid doesn't, so in this case it will not match. Please let me know your thoughts.
As mentioned before, using rownum can be tricky, because inserts or deletes can happen at the same time which could change rownums.
If I understand your query correctly, you are simply trying to get the record with the latest action_date. Why not do the following:
CURSOR c_price_hist_parent IS
select sub.tran_type, sub.reason, sub.event,
sub.unit_cost, sub.unit_retail, sub.selling_unit_retail,
sub.selling_uom, sub.multi_units, sub.multi_unit_retail,
sub.multi_selling_uom
from (
select *
from price_hist
where item = l_item_parent
and tran_type in (4,8)
and loc = l_location
and ACTION_DATE <= l_create_date
order by action_date desc
) sub
where rownum = 1
);

How can I return multiple identical rows based on a quantity field in the row itself?

I'm using oracle to output line items in from a shopping app. Each item has a quantity field that may be greater than 1 and if it is, I'd like to return that row N times.
Here's what I'm talking about for a table
product_id, quanity
1, 3,
2, 5
And I'm looking a query that would return
1,3
1,3
1,3
2,5
2,5
2,5
2,5
2,5
Is this possible? I saw this answer for SQL Server 2005 and I'm looking for almost the exact thing in oracle. Building a dedicated numbers table is unfortunately not an option.
I've used 15 as a maximum for the example, but you should set it to 9999 or whatever the maximum quantity you will support.
create table t (product_id number, quantity number);
insert into t values (1,3);
insert into t values (2,5);
select t.*
from t
join (select rownum rn from dual connect by level < 15) a
on a.rn <= t.quantity
order by 1;
First create sample data:
create table my_table (product_id number , quantity number);
insert into my_table(product_id, quantity) values(1,3);
insert into my_table(product_id, quantity) values(2,5);
And now run this SQL:
SELECT product_id, quantity
FROM my_table tproducts
,( SELECT LEVEL AS lvl
FROM dual
CONNECT BY LEVEL <= (SELECT MAX(quantity) FROM my_table)) tbl_sub
WHERE tbl_sub.lvl BETWEEN 1 AND tproducts.quantity
ORDER BY product_id, lvl;
PRODUCT_ID QUANTITY
---------- ----------
1 3
1 3
1 3
2 5
2 5
2 5
2 5
2 5
This question is propably same as this: how to calc ranges in oracle
Update solution, for Oracle 9i:
You can use pipelined_function() like this:
CREATE TYPE SampleType AS OBJECT
(
product_id number,
quantity varchar2(2000)
)
/
CREATE TYPE SampleTypeSet AS TABLE OF SampleType
/
CREATE OR REPLACE FUNCTION GET_DATA RETURN SampleTypeSet
PIPELINED
IS
l_one_row SampleType := SampleType(NULL, NULL);
BEGIN
FOR cur_data IN (SELECT product_id, quantity FROM my_table ORDER BY product_id) LOOP
FOR i IN 1..cur_data.quantity LOOP
l_one_row.product_id := cur_data.product_id;
l_one_row.quantity := cur_data.quantity;
PIPE ROW(l_one_row);
END LOOP;
END LOOP;
RETURN;
END GET_DATA;
/
Now you can do this:
SELECT * FROM TABLE(GET_DATA());
Or this:
CREATE OR REPLACE VIEW VIEW_ALL_DATA AS SELECT * FROM TABLE(GET_DATA());
SELECT * FROM VIEW_ALL_DATA;
Both with same results.
(Based on my article pipelined function)

Resources