Exception in CURSOR or other solution - sql

I have a DB where selling tickets. I have such procedure, where I count all sold money from some race:
CREATE OR REPLACE PROCEDURE Total_money(depart IN RACE.DEPART_PLACE%TYPE,
dest IN RACE.DESTINATION_PLACE%TYPE, total OUT TICKET.PRICE%TYPE)
IS
CURSOR tickets
IS SELECT t.CLIENT_ID, t.PRICE FROM TICKET t JOIN VAGON v ON t.VAGON_ID = v.VAGON_ID
JOIN RACE r ON v.RACE_ID = r.RACE_ID
WHERE r.DEPART_PLACE = depart AND r.DESTINATION_PLACE = dest;
BEGIN
FOR t IN tickets
LOOP
IF t.CLIENT_ID IS NOT NULL THEN
total := total + t.PRICE;
END IF;
END LOOP;
END;
First question: Can I place an exception into CURSOR declaration? Or what can I do, when I pass wrong depart name or destination name of the train? Or these names don't exist in DB. Then it will create an empty cursor. And return 0 money. How to control this?
Second question: After procedure declaration, I run these commands:
DECLARE t TICKET.PRICE%TYPE;
t:=0;
execute total_money('Kyiv', 'Warsaw', t)
But there is an error(PLS-00103 Encountered the symbol...)
First question: How to fix it?

A simple check is just to test that total is non-zero after the loop:
...
END LOOP;
IF total <= 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Toal zero, invalid arguments?');
END IF;
END;
If the total could legitimately be zero (which seems unlikely here, apart from the client ID check) you could have a counter of a flag and check that:
CREATE ... IS
found BOOLEAN := false;
CURSOR ...
BEGIN
total := 0;
FOR t IN tickets
LOOP
found := true;
IF t.CLIENT_ID IS NOT NULL THEN
total := total + t.PRICE;
END IF;
END LOOP;
IF NOT found THEN
RAISE_APPLICATION_ERROR(-20001, 'No records, invalid arguments?');
END IF;
END;
execute is an SQL*Plus command, so I'm not sure which way you want this to work. You can use an anonymous block like this:
DECLARE
t TICKET.PRICE%TYPE;
BEGIN
total_money('Kyiv', 'Warsaw', t);
-- do something with t
END;
/
Or using an SQL*Plus (or SQL Developer) variable you can do:
variable t number;
execute total_money('Kyiv', 'Warsaw', :t);
print t
I'd change it from a procedure to a function though; declare a total within it, initialise it to zero, and return that instead of having an out parameter. Then you can call it from PL/SQL or from SQL, within a simple select.
And as ElectricLlama points out, you don't need a cursor; and don't need to do this in PL/SQL at all - just use an aggregate sum(). I assume this is an exercise to learn about cursors though?

Related

Why does my explicit cursor fetch only specific rows from my database in PL/SQL?

I am very new to PL/SQL and I am trying to use an explicit cursor to iterate over my database, FLEX_PANEL_INSPECTIONS. I would like to fetch each row from the database in turn using an explicit cursor, and depending on the randomly generated 'status' of a given 'panel' in the row, assign the panel a new status value within an if / else statement. The status of the panel is random but Boolean - it is either 1 or 0.
However, when I view the DBMS output, I note that the fetch does not retrieve all values from the database - only those who have a status value of 1. I have included the core code below.
I would be very grateful if anybody is able to help me find a solution, or explain the root cause of my problem, thanks!
create or replace procedure FLEX_SUMMARY_STATUS_PROCEDURE as
old_panel_status number;
new_panel_status number;
cursor panel_cursor is
select FLEX_PANEL_STATUS
from FLEX_PANEL_INSPECTIONS;
begin
open panel_cursor;
loop
fetch panel_cursor into old_panel_status;
exit when panel_cursor%notfound;
if old_panel_status = 0
then new_panel_status := 2;
elsif old_panel_status = 1
then new_panel_status := 3;
--More conditional loops follow (but are irrelevant for this question).
dbms_output.put_line(old_panel_status);
--Test output
--This displays all of the 1's that were randomly generated in the original table.
--It does not display any of the 0's that were generated.
end if;
end loop;
close panel_cursor;
close sensor_cursor;
end FLEX_SUMMARY_STATUS_PROCEDURE;
/
In addition to the main error, which has already been fixed in the accepted answer, the below code shows the newer loop construct. Instead of rec, you can choose any variable name you like. On each iteration, it contains a row (usually with more than one column).
create or replace procedure FLEX_SUMMARY_STATUS_PROCEDURE as
new_panel_status number;
cursor panel_cursor is
select FLEX_PANEL_STATUS
from FLEX_PANEL_INSPECTIONS;
begin
for rec in panel_cursor loop
if rec.flex_panel_status = 0 then
new_panel_status := 2;
elsif rec.flex_panel_status = 1 then
new_panel_status := 3;
--More conditional loops follow (but are irrelevant for this question)
end if;
dbms_output.put_line(rec.flex_panel_status);
end loop;
end FLEX_SUMMARY_STATUS_PROCEDURE;
/
You can even get rid if the explicit cursor if you like:
for rec in (
select FLEX_PANEL_STATUS
from FLEX_PANEL_INSPECTIONS
) loop
If you didn't make a mistake when removing the additional elseif clauses, the issue is in the location of your dbms_output.put_line.
It's located inside the else part, so will only trigger when this clause is called. Move it below the END IF and make sure to use proper indentation, which makes such things way easier to spot.
create or replace procedure FLEX_SUMMARY_STATUS_PROCEDURE as
old_panel_status number;
new_panel_status number;
cursor panel_cursor is
select FLEX_PANEL_STATUS
from FLEX_PANEL_INSPECTIONS;
begin
open panel_cursor;
loop
fetch panel_cursor into old_panel_status;
exit when panel_cursor%notfound;
if old_panel_status = 0
then new_panel_status := 2;
elsif old_panel_status = 1
then new_panel_status := 3;
--More conditional loops follow (but are irrelevant for this question)
end if;
dbms_output.put_line(old_panel_status);
end loop;
close panel_cursor;
close sensor_cursor;
end FLEX_SUMMARY_STATUS_PROCEDURE;
/

Querying the result of a Procedure, PL/SQL ( Oracle DBMS )

I have written a procedure which checks the amount of vacant rooms a property has.
CREATE OR REPLACE PROCEDURE prop_vacancy_query(
p_property_id properties.tracking_id%TYPE
)
IS
property_refcur SYS_REFCURSOR;
v_prop_rooms properties.num_rooms%TYPE;
BEGIN
OPEN property_refcur FOR
'SELECT COUNT(room_status) FROM rooms
JOIN properties ON
properties.property_id = rooms.property_id
WHERE room_status = :status AND properties.tracking_id = :track_id' USING 'VACANT', p_property_id;
LOOP
FETCH property_refcur INTO v_prop_rooms;
EXIT WHEN property_refcur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(' ');
DBMS_OUTPUT.PUT_LINE('Available Rooms: ' || v_prop_rooms);
END LOOP;
CLOSE property_refcur;
END;
I want to use this procedure in a trigger to automatically set the status of the property too 'OCCUPIED' when the amount of rooms available returns as 0.
I have tried
IF prop_vacancy_query(:NEW.property_status) = 0 THEN
:NEW.property_status := 'OCCUPIED';
END IF;
But this does not work. How would I go about calling this procedure with conditional logic in my trigger? Or is this not possible.
NOTE: I am also worried about the performance issues this may entail, I am not sure how else I could handle auto updating when the DB updates, any pointer to how I could solve this problem would be greatly appreciated.
First, if you have a piece of code whose only purpose is to run queries and return a value, use a FUNCTION, not a PROCEDURE. Procedures should be doing some sort of manipulation of the data.
Second, if you do not need dynamic SQL, don't use dynamic SQL. It's generally a bit slower but, more importantly, it is much, much harder to write, support, and debug. Plus, you're turning compile-time exceptions into run-time exceptions so you won't find your syntax errors until you try to run your code.
You can simplify your code rather significantly
CREATE OR REPLACE FUNCTION get_num_vacancies(
p_property_id properties.tracking_id%TYPE
)
RETURN NUMBER
IS
v_prop_rooms properties.num_rooms%TYPE;
BEGIN
SELECT COUNT(room_status)
INTO v_prop_rooms
FROM rooms
JOIN properties ON
properties.property_id = rooms.property_id
WHERE room_status = 'VACANT'
AND properties.tracking_id = p_property_id;
RETURN v_prop_rooms;
END;
Then you can call the function in the way that you originally wanted
IF prop_vacancy_query(:NEW.property_status) = 0 THEN
:NEW.property_status := 'OCCUPIED';
END IF;
CREATE OR REPLACE PROCEDURE prop_vacancy_query(
p_property_id properties.tracking_id%TYPE
status OUT NUMBER
)
.
.
.
status := 0
END;
/
And call this way
outstatus NUMBER:= -1;
prop_vacancy_query(:NEW.property_id,outstatus);
IF out status = 0 THEN
:NEW.property_status := 'OCCUPIED';
END IF;

Oracle: IN function within an IF block

I have a cursor c2, with one column 'note', containing numbers.
I want to check, whether the column contains 5 and change my local variable if so.
this:
CREATE OR REPLACE PROCEDURE proc
IS
result varchar(50);
cursor c2 is
SELECT note
FROM student;
BEGIN
IF c2.note IN(5) THEN
result := 'contains 5';
DBMS_OUTPUT.PUT_LINE(result);
END;
/
doesnt work.
please help!
In your code, you're declaring a cursor but you are never opening it and never fetching data from it. You'd need, presumably, some sort of loop to iterate through the rows that the cursor returned. You'll either need to explicitly or implicitly declare a record into which each particular row is fetched. One option to do that is something like
CREATE OR REPLACE PROCEDURE proc
IS
result varchar(50);
cursor c2 is
SELECT note
FROM student;
BEGIN
FOR rec IN c2
LOOP
IF rec.note IN(5) THEN
result := 'contains 5';
DBMS_OUTPUT.PUT_LINE(result);
END IF;
END LOOP;
END;
/
Note that you also have to have an END IF that corresponds to your IF statement. Naming a cursor c2 is also generally a bad idea-- your variables really ought to be named meaningfully.
You have too loop over the records/rows returned in the cursor; you can't refer to the cursor itself like that:
CREATE OR REPLACE PROCEDURE proc
IS
result varchar(50);
cursor c2 is
SELECT note
FROM student;
BEGIN
FOR r2 IN c2 LOOP
IF r2.note = 5 THEN -- IN would also work but doesn't add anything
result := 'contains 5';
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE(result);
END;
/
But if you're just testing for the existence of any record with value 5 then you don't need a cursor:
CREATE OR REPLACE PROCEDURE proc
IS
result varchar(50);
BEGIN
SELECT max('contains 5')
INTO result
FROM student
WHERE note = 5;
DBMS_OUTPUT.PUT_LINE(result);
END;
/
If there are any rows with five you'll get the 'contains 5' string; if not you'll get null. The max() stops an exception being thrown if there are either zero or more than one matching records in the table.
You are missing an END IF and need to LOOP over the cursor. The procedure name is included in the final END.
But using IN should work. Like this:
CREATE OR REPLACE PROCEDURE proc
IS
result varchar(50);
cursor c2 is
SELECT note
FROM student;
BEGIN
FOR rec in c2
LOOP
IF rec.note IN (5) THEN
result := 'contains 5';
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE(result);
END proc;
/

When should I nest PL/SQL BEGIN...END blocks?

I've been somewhat haphazardly grouping subsections of code in BEGIN...END blocks when it seems right. Mostly when I'm working on a longer stored procedure and there's a need for a temporary variable in one spot I'll declare it just for that portion of the code. I also do this when I want to identify and handle exceptions thrown for a specific piece of code.
Any other reasons why one should nest blocks within a procedure, function or another larger block of PL/SQL?
When you want to handle exceptions locally like this:
begin
for emp_rec in (select * from emp) loop
begin
my_proc (emp_rec);
exception
when some_exception then
log_error('Failed to process employee '||emp_rec.empno);
end;
end loop;
end;
In this example, the exception is handled and then we carry on and process the next employee.
Another use is to declare local variables that have limited scope like this:
declare
l_var1 integer;
-- lots of variables
begin
-- lots of lines of code
...
for emp_rec in (select * from emp) loop
declare
l_localvar integer := 0;
begin
-- Use l_localvar
...
end
end loop;
end;
Mind you, wanting to do this is often a sign that your program is too big and should be broken up:
declare
l_var1 integer;
-- lots of variables
...
procedure local_proc (emp_rec emp%rowtype):
l_localvar integer := 0;
begin
-- Use l_localvar
...
end
begin
-- lots of lines of code
...
for emp_rec in (select * from emp) loop
local_proc (emp_rec);
end loop;
end;
I tend to nest blocks when I want to create procedures that are specific to data that only exists within the block. Here is a contrived example:
BEGIN
FOR customer IN customers LOOP
DECLARE
PROCEDURE create_invoice(description VARCHAR2, amount NUMBER) IS
BEGIN
some_complicated_customer_package.create_invoice(
customer_id => customer.customer_id,
description => description,
amount => amount
);
END;
BEGIN
/* All three calls are being applied to the current customer,
even if we're not explicitly passing customer_id.
*/
create_invoice('Telephone bill', 150.00);
create_invoice('Internet bill', 550.75);
create_invoice('Television bill', 560.45);
END;
END LOOP;
END;
Granted, it's not usually necessary, but it has come in really handy when a procedure can be called from many locations.
One reason to have nested BEGIN/END blocks is to be able to handle exceptions for a specific local section of the code and potentially continue processing if the exception is processed.

Oracle PL/SQL - Are NO_DATA_FOUND Exceptions bad for stored procedure performance?

I'm writing a stored procedure that needs to have a lot of conditioning in it. With the general knowledge from C#.NET coding that exceptions can hurt performance, I've always avoided using them in PL/SQL as well. My conditioning in this stored proc mostly revolves around whether or not a record exists, which I could do one of two ways:
SELECT COUNT(*) INTO var WHERE condition;
IF var > 0 THEN
SELECT NEEDED_FIELD INTO otherVar WHERE condition;
....
-or-
SELECT NEEDED_FIELD INTO var WHERE condition;
EXCEPTION
WHEN NO_DATA_FOUND
....
The second case seems a bit more elegant to me, because then I can use NEEDED_FIELD, which I would have had to select in the first statement after the condition in the first case. Less code. But if the stored procedure will run faster using the COUNT(*), then I don't mind typing a little more to make up processing speed.
Any hints? Am I missing another possibility?
EDIT
I should have mentioned that this is all already nested in a FOR LOOP. Not sure if this makes a difference with using a cursor, since I don't think I can DECLARE the cursor as a select in the FOR LOOP.
I would not use an explicit cursor to do this. Steve F. no longer advises people to use explicit cursors when an implicit cursor could be used.
The method with count(*) is unsafe. If another session deletes the row that met the condition after the line with the count(*), and before the line with the select ... into, the code will throw an exception that will not get handled.
The second version from the original post does not have this problem, and it is generally preferred.
That said, there is a minor overhead using the exception, and if you are 100% sure the data will not change, you can use the count(*), but I recommend against it.
I ran these benchmarks on Oracle 10.2.0.1 on 32 bit Windows. I am only looking at elapsed time. There are other test harnesses that can give more details (such as latch counts and memory used).
SQL>create table t (NEEDED_FIELD number, COND number);
Table created.
SQL>insert into t (NEEDED_FIELD, cond) values (1, 0);
1 row created.
declare
otherVar number;
cnt number;
begin
for i in 1 .. 50000 loop
select count(*) into cnt from t where cond = 1;
if (cnt = 1) then
select NEEDED_FIELD INTO otherVar from t where cond = 1;
else
otherVar := 0;
end if;
end loop;
end;
/
PL/SQL procedure successfully completed.
Elapsed: 00:00:02.70
declare
otherVar number;
begin
for i in 1 .. 50000 loop
begin
select NEEDED_FIELD INTO otherVar from t where cond = 1;
exception
when no_data_found then
otherVar := 0;
end;
end loop;
end;
/
PL/SQL procedure successfully completed.
Elapsed: 00:00:03.06
Since SELECT INTO assumes that a single row will be returned, you can use a statement of the form:
SELECT MAX(column)
INTO var
FROM table
WHERE conditions;
IF var IS NOT NULL
THEN ...
The SELECT will give you the value if one is available, and a value of NULL instead of a NO_DATA_FOUND exception. The overhead introduced by MAX() will be minimal-to-zero since the result set contains a single row. It also has the advantage of being compact relative to a cursor-based solution, and not being vulnerable to concurrency issues like the two-step solution in the original post.
An alternative to #Steve's code.
DECLARE
CURSOR foo_cur IS
SELECT NEEDED_FIELD WHERE condition ;
BEGIN
FOR foo_rec IN foo_cur LOOP
...
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END ;
The loop is not executed if there is no data. Cursor FOR loops are the way to go - they help avoid a lot of housekeeping. An even more compact solution:
DECLARE
BEGIN
FOR foo_rec IN (SELECT NEEDED_FIELD WHERE condition) LOOP
...
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END ;
Which works if you know the complete select statement at compile time.
#DCookie
I just want to point out that you can leave off the lines that say
EXCEPTION
WHEN OTHERS THEN
RAISE;
You'll get the same effect if you leave off the exception block all together, and the line number reported for the exception will be the line where the exception is actually thrown, not the line in the exception block where it was re-raised.
Stephen Darlington makes a very good point, and you can see that if you change my benchmark to use a more realistically sized table if I fill the table out to 10000 rows using the following:
begin
for i in 2 .. 10000 loop
insert into t (NEEDED_FIELD, cond) values (i, 10);
end loop;
end;
Then re-run the benchmarks. (I had to reduce the loop counts to 5000 to get reasonable times).
declare
otherVar number;
cnt number;
begin
for i in 1 .. 5000 loop
select count(*) into cnt from t where cond = 0;
if (cnt = 1) then
select NEEDED_FIELD INTO otherVar from t where cond = 0;
else
otherVar := 0;
end if;
end loop;
end;
/
PL/SQL procedure successfully completed.
Elapsed: 00:00:04.34
declare
otherVar number;
begin
for i in 1 .. 5000 loop
begin
select NEEDED_FIELD INTO otherVar from t where cond = 0;
exception
when no_data_found then
otherVar := 0;
end;
end loop;
end;
/
PL/SQL procedure successfully completed.
Elapsed: 00:00:02.10
The method with the exception is now more than twice as fast. So, for almost all cases,the method:
SELECT NEEDED_FIELD INTO var WHERE condition;
EXCEPTION
WHEN NO_DATA_FOUND....
is the way to go. It will give correct results and is generally the fastest.
If it's important you really need to benchmark both options!
Having said that, I have always used the exception method, the reasoning being it's better to only hit the database once.
Yes, you're missing using cursors
DECLARE
CURSOR foo_cur IS
SELECT NEEDED_FIELD WHERE condition ;
BEGIN
OPEN foo_cur;
FETCH foo_cur INTO foo_rec;
IF foo_cur%FOUND THEN
...
END IF;
CLOSE foo_cur;
EXCEPTION
WHEN OTHERS THEN
CLOSE foo_cur;
RAISE;
END ;
admittedly this is more code, but it doesn't use EXCEPTIONs as flow-control which, having learnt most of my PL/SQL from Steve Feuerstein's PL/SQL Programming book, I believe to be a good thing.
Whether this is faster or not I don't know (I do very little PL/SQL nowadays).
Rather than having nested cursor loops a more efficient approach would be to use one cursor loop with an outer join between the tables.
BEGIN
FOR rec IN (SELECT a.needed_field,b.other_field
FROM table1 a
LEFT OUTER JOIN table2 b
ON a.needed_field = b.condition_field
WHERE a.column = ???)
LOOP
IF rec.other_field IS NOT NULL THEN
-- whatever processing needs to be done to other_field
END IF;
END LOOP;
END;
you dont have to use open when you are using for loops.
declare
cursor cur_name is select * from emp;
begin
for cur_rec in cur_name Loop
dbms_output.put_line(cur_rec.ename);
end loop;
End ;
or
declare
cursor cur_name is select * from emp;
cur_rec emp%rowtype;
begin
Open cur_name;
Loop
Fetch cur_name into Cur_rec;
Exit when cur_name%notfound;
dbms_output.put_line(cur_rec.ename);
end loop;
Close cur_name;
End ;
May be beating a dead horse here, but I bench-marked the cursor for loop, and that performed about as well as the no_data_found method:
declare
otherVar number;
begin
for i in 1 .. 5000 loop
begin
for foo_rec in (select NEEDED_FIELD from t where cond = 0) loop
otherVar := foo_rec.NEEDED_FIELD;
end loop;
otherVar := 0;
end;
end loop;
end;
PL/SQL procedure successfully completed.
Elapsed: 00:00:02.18
The count(*) will never raise exception because it always returns actual count or 0 - zero, no matter what. I'd use count.
The first (excellent) answer stated -
The method with count() is unsafe. If another session deletes the row that met the condition after the line with the count(*), and before the line with the select ... into, the code will throw an exception that will not get handled.
Not so. Within a given logical Unit of Work Oracle is totally consistent. Even if someone commits the delete of the row between a count and a select Oracle will, for the active session, obtain the data from the logs. If it cannot, you will get a "snapshot too old" error.