Oracle Cursor with getting data from each row - oracle

I am new in programming. I want to use procedure to get all the substr in address and put it in my new column 'info'. My procedure now only get the first row of string from address and place it all over the 'info' column. Am I need a loop or something to make my procedure get the right data from each row?
create or replace PROCEDURE update_data
IS
data_1 VARCHAR2(13 CHAR);
CURSOR c1 IS
SELECT substr(t.address, 3, 4)
FROM table_1 t
WHERE t.age > 21
BEGIN
OPEN c1;
fetch c1 INTO data_1;
Update table_1 t
SET t.info = data_1;
CLOSE c1;
END;
Thanks for helping me out!!

One major problem is that you're not limiting your UPDATE statement. You're actually updating EVERY ROW in the entire table. Watch out for that whenever you're writing UPDATE and DELETE statements or you will cause major problems and your co-workers won't thank you. I would suggest you write all of these types of statements as SELECT statements for to ensure that you're properly limiting with the WHERE clause and then convert it to a more dangerous statement.
As for your specific question - yes - you need to loop through every record in the cursor and then update the SINGLE corresponding row in the table.
Here's an updated example:
CREATE OR REPLACE PROCEDURE update_data
IS
CURSOR c1
IS
SELECT primary_key_column, /* SELECT THIS SO THAT YOU CAN IDENTIFY THE SINGLE ROW TO UPDATE */
SUBSTR(t.address, 3, 4) AS info /*NAME THIS SO IT CAN BE REFERENCED IN THE LOOP */
FROM table_1 t
WHERE t.age > 21;
BEGIN
FOR r /* NAME THE ROW */ IN c1 LOOP /* ORACLE AUTMOATICALLY OPENS THE CURSOR HERE */
UPDATE table_1 t
SET t.info = r.info
WHERE t.primary_key_column = r.primary_key_column; /* LIMIT THE UPDATE TO THIS ROW ONLY */
END LOOP; /* ORACLE AUTOMATICALLY CLOSES THE CURSOR HERE */
END;
You could use the OPEN, FETCH, CLOSE method you are currently using with a loop, but I personally think the FOR ... IN syntax is more readable, easier to write, and handles opening and closing cursors for you, so I've used it here.
The main thing to notice is that the UPDATE is now limited to one single record, so that every iteration through the loop will update the same record that you're running SUBSTR on - and NOT the entire table.
Alternatively, this could all be done with a single statement:
UPDATE table_1 t
SET t.info = SUBSTR(t.address, 3, 4)
WHERE t.age > 21;
Just make sure you're properly limiting your UPDATE.

Related

why if block not working within a procedure while comparing two columns of two tables

I need to check whether std_id is present same as in students table if this matches with std_id in std_grace_marks then I need to add that grace_marks of that std_id with marks column in students table.
I have created a procedure and created two cursors to fetch records in loop and I wrote if condition to check whether std_id matches if it is then I am adding marks with grace_marks but if condition is not working here... so please can anyone tell where I am going wrong
My code:
create or replace procedure std_info
IS
CURSOR stdcur IS SELECT std_id,std_name, marks,mark_status FROM students;
CURSOR gracer IS SELECT std_id,grace_marks from student_grace_marks;
myvar stdcur%ROWTYPE;
mycur gracer%ROWTYPE;
BEGIN
OPEN stdcur;
OPEN gracer;
LOOP
FETCH stdcur INTO myvar;
FETCH gracer INTO mycur;
EXIT WHEN stdcur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( myvar.std_id || ' '|| myvar.std_name||' '||myvar.marks );
if(myvar.std_id=mycur.std_id) then
update students set marks=myvar.marks+mycur.grace_marks;
end if;
END LOOP;
CLOSE stdcur;
CLOSE gracer;
END;
I'm not sure this will ever work. You're fetching a row of each resultset in a row variable, assuming that row 1 for the first resultset will match the std_id of row 1 for the 2nd resultset. There are 2 things wrong with this assumption
There is no ORDER BY clause in the statements, so std_id for the firs row of cursor stdcur could be 10 while std_id for the first row of cursor gracer could be 99. Oracle does not guarantee the order of the a resultset unless an ORDER BY clause is included in the statement.
If table student_grace_marks has more than 1 row, or no rows for a std_id, it will start failing from that row onwards since the count will no longer match.
One solution is to use explicit cursor for loops:
DECLARE
BEGIN
FOR r_student IN (SELECT * FROM students) LOOP
FOR r_student_gm IN (SELECT * FROM student_grace_marks WHERE std_id = r_student.std_id) LOOP
UPDATE students SET marks = marks + r_student_gm.grace_marks WHERE std_id = r_student.std_id;
END LOOP;
END LOOP;
END;
/
.. or .. if you want to use explicit cursors:
In this case the CURSOR gracer will have a where clause to only select the relevant row(s) for that particular student.
Note that I fixed some errors as well
DECLARE
--declare variable to be used in the where clause of the select for cursor gracer.
l_std_id students.std_id%TYPE;
CURSOR stdcur IS SELECT std_id,std_name, marks FROM students;
CURSOR gracer IS SELECT std_id,grace_marks from student_grace_marks WHERE std_id = l_std_id;
myvar stdcur%ROWTYPE;
mycur gracer%ROWTYPE;
BEGIN
OPEN stdcur;
LOOP
FETCH stdcur INTO myvar;
EXIT WHEN stdcur%NOTFOUND;
l_std_id := myvar.std_id;
OPEN gracer;
LOOP
FETCH gracer INTO mycur;
EXIT WHEN gracer%NOTFOUND;
-- use marks, not myvar.marks. If there is >1 record in student_grace_marks it will only add the last value.
-- add the where clause or every update will update all rows and every grace_marks will be added for every student in the table.
UPDATE students set marks= marks+mycur.grace_marks WHERE std_id = l_std_id;
END LOOP;
CLOSE gracer;
END LOOP;
CLOSE stdcur;
END;
/

Getting the corresponding record of a Cursor/Select when in a Cursor LOOP statement

The question seems easy. I have built a package, where there is a quite massive cursor, let's say on all invoices of my company for the whole year.
CURSOR c_invoices(p_year IN INTEGER) IS
SELECT all_invoices.invoicenumber,
all_invoices.invoicedate,
all_invoices.customernumber
FROM all_invoices
WHERE all_invoices.year = p_year
;
After opening it and using a LOOP statement, I want to get some data from another table (forbidden_customers), but only if the customer is in this very last table.
What I'd like to do, is to open another cursor (or a SELECT ?) at the very beginning of my package, browsing the whole table(forbidden_customers), and then getting to the corresponding record when in my invoices LOOP.
So, something like :
CURSOR c_forbidden_customers IS
SELECT forbidden_customers.customernumber,
forbidden_customers.customeradress
FROM forbidden_customers
;
And then :
OPEN c_invoices(v_year);
LOOP FETCH c_invoices INTO invoices_cursor;
BEGIN
EXIT WHEN c_invoices%NOTFOUND;
*IF invoices_cursor.customernumber IS FOUND IN c_forbidden_customers ...
THEN ...*
This is what I do meanwhile (I know it is bad):
SELECT COUNT(*)
INTO v_exist /*INTEGER*/
FROM forbidden_customers
WHERE forbidden_customers.customernumber= p_customernumber
IF v_exist <> 0
THEN...
I tried to make it as clear as possible. Thank you for your time
Don't do it twice; join both tables in the same cursor and use it. Also, if you switch to a cursor FOR loop, you'll save yourself from some typing as Oracle will do most of boring stuff for you (declaring cursor variable, opening the cursor, closing it, exiting the loop ...):
create or replace procedure p_test (p_year in integer) is
begin
for c_invoices in
(select a.invoicenumber,
a.invoicedate,
a.customernumber,
c.customeraddress
from all_invoices a join forbidden_customers c on c.customernumber = a.customernumber
where a.year = p_year)
loop
-- do something
end loop;
end;
If the table forbidden_customers is not large and it will fit oracle's session memory, you can use a pl/sql table to store all id's from forbidden_customers and check it later. The check is done in memory only, so it is much faster than any regular select.
create table all_invoices
(id number,
year number,
customer_number number);
create table forbidden_customers
(customer_number number);
CREATE OR REPLACE TYPE t_number_table IS TABLE OF number
/
CREATE OR REPLACE PROCEDURE test23
IS
forbidden_customers_list t_number_table;
CURSOR c_invoices (p_year IN INTEGER)
IS
SELECT all_invoices.customer_number
FROM all_invoices
WHERE all_invoices.year = p_year;
BEGIN
SELECT customer_number
BULK COLLECT INTO forbidden_customers_list
FROM forbidden_customers;
FOR rec_invoices in c_invoices(2022) loop
if forbidden_customers_list.exists(rec_invoices.customer_number) then
null;
end if;
end loop;
end;
/

How to remove a row from an Oracle cursor but not the database table

I have a FOR LOOP cursor and while looping through it I need to delete some of the rows from the cursor but not from the database table. Is that possible to do?
What I am trying to accomplish is to be left with only those rows in the cursor my code did not process by removing each processed row that met certain criteria
Bulk collect the cursor rows into a collection. Then as each row in processed delete it from the collection. What's left will be the rows not originally processed. The following provides a skeleton for the process needed:
declare
cursor c_cursor
is
select ... ;
type c_cursor_t is table of c_cursor%rowtype;
l_cursor_data c_cursor_t;
l_cursor_indx integer;
begin
open c_cursor;
fetch c_cursor
bulk collect
into l_cursor_data;
close c_cursor;
l_cursor_indx := l_cursor_data.first; -- set collection index to 1st value
while l_cursor_indx is not null
loop
if <row should be processed> -- determine which rows to process
then
<process_row>; -- and process them
l_cursor_data.delete(l_cursor_indx); -- then delete processed rows
end if ;
l_cursor_indx := l_cursor_data.next(l_cursor_indx); -- set collection index to next row or null if no morw rows.
end loop;
--- Handle anything left in l_cursor_data collection as they have not been processed.
--- THE SAME LOOP STRUCTURE AN BE USED FOR THE COLLECTION IF NEEDED.
end ;
Of course as #MT0 it would be much easier to eliminate those that will not be processed from the query to begin with. Just retrieve the rows you want to process is always the best practice.
You cannot modify a cursor while you are reading it; if you want to exclude rows then you will need to do it when you are generating the cursor.
Use the WHERE clause to exclude the rows from the cursor:
DECLARE
OPEN cursor_name FOR
SELECT *
FROM my_table
WHERE primary_key_column NOT IN ( 1, 2, 3 ); -- criteria to exclude.
BEGIN
-- process cursor with excluded rows.
END;
/
Load a collection with the results from your query, making sure the collection contains a 'processed' flag that is initialized to False. Then loop through the collection, processing as you need to. Flip the flag to True when done.
Then you could loop through the collection again where the processed_flag is False to get your untouched rows.

How to have my Oracle PL/SQL block update all entries related to the row_ids referenced by my cursor

I currently have a Oracle stored procedure that is taking data from tables. The problem is I am aggregating /grouping , and I don't want to grab the IDs otherwise that will throw the grouping off. I want to update a column called 'correlated_flag_id' to '1' (done) in the value table after ive inserted the aggregated/grouped result set. I only want to grab IDs that are correlated to the
values that my first cursor grabbed to derive the results. Below is my attempt (which I don't think is correct):
Create or Replace PROCEDURE PROC is
CURSOR c1 is
select sum(v.value_tx) as sum_of_values
, max(v.create_dt) as latest_create_dt
, v.data_date
from value v
group by v.data_date, max(v.create_dt)
BEGIN
Open c1;
LOOP
Fetch c1 into l_var;
insert into value (value_id, value_tx, create_dt, data_date)
values (null, l_var.sum_of_values, l_var.latest_create_dt, l_var.data_Date);
END LOOP;
Close c1;
commit;
--- the bottom is not correct, but i've reached a roadblock
Update value
set correlated_flag_id = 777
where value_id in (select v.value_id from value where trunc(create_dt) <> trunc(sysdate)) (???));
commit;
END PROC;
Thanks in advance and please let me know if there is any more details that I need to provide.
cursor's select is kind of wrong; why are you grouping by a MAX function? It isn't allowed here
switch to cursor FOR loop as it is easier to maintain. You don't have to open a cursor, fetch, exit the loop (which you did not do at all), close the cursor
I'm not sure what VALUE_930 table is doing here, you never mentioned it
your words say "update correlated ID to 1", while code says "update it to 777"
don't commit in a procedure; let caller decide whether it should be done
I'd suggest you to either use a tool which offers code formatting, or format it yourself. Your procedure is not a result of a flood, so don't treat it that way
Finally, here's a suggestion which might (or might not) work as we don't have your tables nor data, but - at least - looks decent.
create or replace procedure proc is
begin
for cur_r in (select v.data_date,
sum(v.value_tx) as sum_of_values,
max(v.create_dt) as latest_create_dt
from value v
group by v.data_date)
loop
insert into value (value_id, value_tx, create_dt, data_date)
values (null, cur_r.sum_of_values, cur_r.latest_create_dt, cur_r.data_date);
update value set
correlated_flag_id = 777
where data_date = cur_r.data_date;
end loop;
end proc;
/

Efficient way to get updated column names on an after update trigger

I've come up with the following trigger to extract all the column names which are updated when a table row update statement is executed...
but the problem is if there are more columns(atleast 100 cols), the performance/efficiency comes into concern
sample trigger code:
set define off;
create or replace TRIGGER TEST_TRIGG
AFTER UPDATE ON A_AAA
FOR EACH ROW
DECLARE
mytable varchar2(32) := 'A_AAA';
mycolumn varchar2(32);
updatedcols varchar2(3000);
cursor s1 (mytable varchar2) is
select column_name from user_tab_columns where table_name = mytable;
begin
open s1 (mytable);
loop
fetch s1 into mycolumn;
exit when s1%NOTFOUND;
IF UPDATING( mycolumn ) THEN
updatedcols := updatedcols || ',' || mycolumn;
END IF;
end loop;
close s1;
--do a few things with the list of updated columns
dbms_output.put_line('updated cols ' || updatedcols);
end;
/
Is there any alternative way to get the list?
Maybe with v$ tables (v$transaction or anything similar)?
No its the best way to get UPDATED column by UPDATING()
and you can change your code using implicit cursor like this, it will be a little bit faster
set define off;
create or replace TRIGGER TEST_TRIGG
AFTER UPDATE ON A_AAA
FOR EACH ROW
DECLARE
updatedcols varchar2(3000);
begin
for r in (select column_name from user_tab_columns where table_name ='A_AAA')
loop
IF UPDATING(r.column_name) THEN
updatedcols := updatedcols || ',' || r.column_name;
END IF;
end loop;
dbms_output.put_line('updated cols ' || updatedcols);
end;
/
Faced with a similar task, we ended up writing a pl/sql procedure which lists the columns of the table and generates the full trigger body for us, with static code referencing :new.col and :old.col. The execution of such trigger should probably be faster (though we didn't compare).
However, the downside is that when you later add a new column to the table, it's easy to forget to update the trigger body. It probably can be managed somehow with a monitoring job or elsehow, but for now it works for us.
P.S. I became curious what that updating('COL') feature does, and checked it now. I found out that it returns true if the column is present in the update statement, even if the value of the column actually didn't change (:old.col is equal to :new:col). This might generate unneeded history records, if the table is being updated by something like Java Hibernate library, which (by default) always specifies all columns in the update statements it generates. In such a case you might want to actually compare the values from inside the trigger body and insert the history record only in case the new value differs from the old value.

Resources