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

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.

Related

query inside the exception and begin block in exception

Hello I want to ask what is the best practise to do.
First example try to retrieve my data through exceptions I use this code in my main application and is working fine but I dont know if its good practise to code inside the exceptions blocks
BEGIN
DECLARE
v_status varchar2(100);
v_flag varchcar2(100);
BEGIN
SELECT STATUS INTO v_status FROM TABLE1 WHERE condition1;
EXCEPTION
when no_data_found then
select FLAG INTO v_flag FROM TABLE2 WHERE condition1; -- THERE WILL BE 100% RECORD
IF v_flag='N' THEN
V_STATUS:='N'
ELSIF v_flag:='O'
V_STATUS:='O'
ELSE
BEGIN
SELECT STATUS INTO V_STATUS FROM TABLE3 WHERE condition1,condition2;
EXCEPTION
V_STATUS:='F';
END;
END IF;
END;
IF V_STATUS='O' THEN
--DO SOMETHING HERE
ELSIF V_STATUS='N' THEN
--DO SOMETHING HERE
ELSE
--DO SOMETHING HERE
END IF;
END;
SECOND EXAMPLE TRY TO RETRIEVE DATA WITH CASES AND SELECT WITH COUNT.
BEGIN
DECLARE
V_CNTR NUMBER;
V_STATUS VARCHAR2(100);
BEGIN
SELECT COUNT(1) INTO V_CNTR FROM TABLE1 WHERE condition1;
CASE
WHEN COUNT=1 THEN
SELECT STATUS INTO V_STATUS FROM TABLE1 WHERE condition1;
ELSE
select FLAG INTO v_flag FROM TABLE2 WHERE condition1; -- THERE WILL BE 100% RECORD
IF v_flag='N' THEN
V_STATUS:='N'
ELSIF v_flag:='O'
V_STATUS:='O'
ELSE
SELECT COUNT(1) INTO V_CNTR FROM TABLE3 WHERE condition1,condition2;
CASE
WHEN count=1 THEN
SELECT STATUS INTO V_STATUS FROM TABLE3 WHERE condition1,condition2;
ELSE
V_STATUS:='F';
END CASE;
END IF;
END CASE;
END;
IF V_STATUS='O' THEN
--DO SOMETHING HERE
ELSIF V_STATUS='N' THEN
--DO SOMETHING HERE
ELSE
--DO SOMETHING HERE
END IF;
END;
From personal experience...
Those nested blocks work fine and it can be useful to use code in exception blocks, but it becomes very unreadable quickly as you show in your example. If your initial code looks like this, then imagine what it will look like a couple of development cycles later.
It is cleaner to move those BEGIN SELECT INTO EXCEPTION WHEN NO_DATA_FOUND THEN... END: blocks into functions. Makes the code a lot more structured, more readable and easier to debug and maintain:
DECLARE
v_status varchar2(100);
v_flag varchcar2(100);
FUNCTION status (argument_i VARCHAR2) RETURN VARCHAR2
IS
l_status VARCHAR2(100);
BEGIN
SELECT STATUS INTO v_status FROM TABLE1 WHERE condition = argument_i;
EXCEPTION WHEN NO_DATA_FOUND THEN
RETURN NULL; -- or -1 or NOTFOUND - whatever you prefer
END;
BEGIN
v_status := status(argument_i => condition);
IF v_status IS NULL THEN
...
ELSE
...
END IF;
END;
Here it's an inline function - within packages you can use standalone functions, private if never called outside the package.
Note, in your 2 examples you declare the variables in the inner block but call them in the outer block - that is something to avoid.
The best practice is always to keep your code clean and readable.
To keep your code clean and readable, make it well structured.
To make it well structured, you have to split code in units where each unit does one thing - has one responsibility.
Read more on SOLID principles, DRY principle to get the idea.
Short answer to your question: coding in exception blocks is not the best practice I'd follow.
Take a look at this presentation on clean PL/SQL coding to get some greater overview.

How to read multiple refcursor return by other procedure to another procedure

i'm having one procedure which returns setof cursors
Now i have to call that procedure to another procedure and access the data
that return by that procedure
is their any way to do this in postgres.
This is code for 1st procedure,
CREATE OR REPLACE FUNCTION public.returns_multiple_cursor( )
RETURNS SETOF refcursor
LANGUAGE 'plpgsql'
COST 100.0
AS $function$
DECLARE
_daily refcursor := 'first_cur';
_fac_hourly refcursor := 'second_cur';
BEGIN
open first_cur for
select * from x;
return next first_cur;
open second_cur for
select * from y;
return second_cur;
END
$function$;
ALTER FUNCTION public.returns_multiple_cursor();
Here code for other second procedure
CREATE OR REPLACE FUNCTION public.access_cursor( )
RETURNS SETOF refcursor
LANGUAGE 'plpgsql'
COST 100.0
AS $function$
DECLARE
BEGIN
-- what code will be here to access the cursor data in this procedure
select public.returns_multiple_cursor();
END;
ALTER FUNCTION public.access_cursor();
Unfortunately, you cannot use the FOR <recordvar> IN <cursor> loop, because it only works for bound cursors (which refcursors are not).
But you can still loop through them, with the old-fashioned FETCH:
declare
rec record;
cur refcursor;
begin
for cur in select returns_multiple_cursor() loop
loop
fetch next from cur into rec;
exit when not found;
-- do whatever you want with the single rows here
end loop;
close cur;
end loop;
end
Unfortunately, there is still another limitation: PostgreSQL caches the first cursor's plan (at least, it seems it does something like that), so you must use cursors, which uses the same column types (you'll have to use the same column names anyway, to be able to refer them in the inner loop, like rec.col1).
Complete, working example: http://rextester.com/FNWG91106 (see f.ex. what happens, when you remove casting from the cursors' queries).
If you have fix number of cursors (like in your example), but differently structured underlying queries, it might be easier to declare your returns_multiple_cursor as:
create or replace function returns_multiple_cursor(out first_cur refcursor,
out second_cur refcursor)
-- returns record (optional)
language plpgsql
-- ...
This way, you could access your cursors more directly in the calling context.
Update: it seems that when you don't use explicit column names, just generic record processing (via f.ex. JSON or hstore), plan caching does not cause any trouble:
http://rextester.com/QHR6096

Exception in CURSOR or other solution

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?

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 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.