Code You Should Never See in PL/SQL
If you ever see any of these troublemakers, here are the steps you need to take to rectify the situation!
Join the DZone community and get the full member experience.Join For Free
If you ever run across any of the following, apply the suggested cleanup or contact the owner of the code...or run for the hills.
I am pretty sure many of my readers will have suggestions for other code that should never appear in your PL/SQL programs. Let me know in the comments and I will add them to the post (giving me all the credit of course — no, just joking! YOUR NAME will be in bright lights).
Set Default Value to NULL
Whenever you declare a variable, it is assigned a default value of NULL. So you should not explicitly provide NULL as a default value. It won't do any harm, but it will tell others who read your code that your understanding of PL/SQL is, shall we say, incomplete.
DECLARE l_number NUMBER := NULL; BEGIN ... END;
Just remove the assignment.
DECLARE l_number NUMBER; BEGIN ... END;
Select From DUAL For...Just About Anything
A long, long time ago, before PL/SQL was all grown up, it didn't have native implementations for some SQL functions like SYSDATE. So developers would use a "dummy" SELECT against a pre-defined table like DUAL in order to fall back on the SQL engine to get the job done, as in:
DECLARE l_now DATE; BEGIN SELECT SYSDATE INTO l_now FROM dual; END;
Plus, if you wanted to get the next (or current) value of a sequence, you had to call the appropriate function via SQL, like:
DECLARE l_next_pky INTEGER; BEGIN SELECT my_seq.NEXTVAL INTO l_next_pky FROM dual; END;
DECLARE l_now DATE; l_next_pky INTEGER; BEGIN l_now := SYSDATE; l_next_pky := my_seq.NEXTVAL; END;
Declaration of FOR Loop Iterator
When you use a FOR loop (numeric or cursor), PL/SQL automatically declares a variable for the record referenced in that loop and then releases memory for that variable when the loop terminates. If you declare a variable with the same name, your code will compile, but you could easily introduce bugs into that code as you see below:
In both of the blocks below, the "confirming" call to DBMS_OUTPUT.put_line to confirm that the desired action occurred will never be executed. In the first block, the "indx" referenced in the IF statement resolves to the locally declared variable, which is not the same indx as the loop iterator. It is initialized to NULL and stays that well. Same with the explicitly declared rec variable in the second block.
DECLARE indx INTEGER; BEGIN FOR indx IN 1 .. 12 LOOP DBMS_OUTPUT.put_line (TO_DATE ('2018-' || indx || '-01', 'YYYY-MM-DD')); END LOOP; IF indx = 12 THEN DBMS_OUTPUT.put_line ('Displayed all twelve months!'); END IF; END; / DECLARE rec employees%ROWTYPE; BEGIN FOR rec IN (SELECT * FROM employees) LOOP DBMS_OUTPUT.put_line (rec.last_name); END LOOP; IF rec.employee_id IS NOT NULL THEN DBMS_OUTPUT.put_line ('Displayed all employees!'); END IF; END; /
The first step in the cleanup is to remove the declarations of those unused and unnecessary variables. The next step is for you to decide what logic you need to replace the IF statements after the loops. In the first case, why would I need an IF statement? If I got past the loop without an exception, then I certainly did display all the months.
In the second block, it's a little bit trickier. When I use a cursor FOR loop, the PL/SQL engine does everything for me: open the cursor, fetch the rows, close the cursor. So once the cursor is closed, I have no visibility into what happened inside the cursor. If I want to know whether at least one row was fetched, for example, I need to set a local variable as you see below.
BEGIN FOR indx IN 1 .. 12 LOOP DBMS_OUTPUT.put_line (TO_DATE ('2018-' || indx || '-01', 'YYYY-MM-DD')); END LOOP; DBMS_OUTPUT.put_line ('Displayed all twelve months!'); END; / DECLARE l_displayed BOOLEAN := FALSE; BEGIN FOR rec IN (SELECT * FROM employees) LOOP DBMS_OUTPUT.put_line (rec.last_name); l_displayed := TRUE; END LOOP; IF l_displayed THEN DBMS_OUTPUT.put_line ('Displayed all employees!'); END IF; END; /
When NO_DATA_FOUND for Non-query DML
When you execute an implicit query (SELECT-INTO), PL/SQL raises NO_DATA_FOUND if no rows are returned by the query. As in:
DECLARE l_id employees.employee_id%TYPE; BEGIN SELECT employee_id INTO l_id FROM employees WHERE 1 = 2; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.put_line ('Not with that WHERE clause!'); END; / Not with that WHERE clause!
But when I run this block (changing the SELECT to an UPDATE), no output is displayed.
BEGIN UPDATE employees SET last_name = UPPER (last_name) WHERE 1 = 2; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.put_line ('Not with that WHERE clause!'); END; /
That's because the PL/SQL engine does not raise an error for non-query DML that change no rows.
If you need to know that a row was modified (or how many rows were modified), use the SQL%ROWCOUNT attribute.
BEGIN UPDATE employees SET last_name = UPPER (last_name) WHERE 1 = 2; IF SQL%ROWCOUNT = 0 THEN DBMS_OUTPUT.put_line ('Not with that WHERE clause!'); END IF; END; / Not with that WHERE clause!
Loop Containing Non-Query DML Inside
Last up, a biggie. That is, bad code with a potentially enormous negative impact on performance. Here goes:
CREATE TABLE parts ( partnum NUMBER PRIMARY KEY, partname VARCHAR2 (15) UNIQUE NOT NULL ) / BEGIN FOR indx IN 1 .. 10000 LOOP INSERT INTO parts (partnum, partname) VALUES (indx, 'Part' || indx); END LOOP; END; /
When you execute the same DML inside a loop repeatedly, changing only the variables that are bound into the statement, you are unnecessarily (and often dramatically) slowing down your code. The problem here is that I am switching between the PL/SQL and SQL engines 10000 times. Those context switches are relatively expensive.
You should avoid this kind of row-by-row processing whenever possible. The key anti-pattern to look for is a loop with non-query DML (insert, update, delete, merge) inside it.
There are two ways to clean up this slow code.
1. Write it in "pure SQL" if you can. If you don't need PL/SQL algorithms to process the data and can do it all in SQL, then do it that way. For example, in the above case, I could have written:
BEGIN INSERT INTO parts SELECT LEVEL, 'Part ' || LEVEL FROM DUAL CONNECT BY LEVEL <= 1000; END;
If you need PL/SQL or simply cannot figure out how to write it in pure SQL, then use the FORALL statement instead of a FOR loop. This statement results in just one context switch, with PL/SQL sending all the bind variables from the bind array (l_parts) across to SQL with a single INSERT statement (that statement, after all, never changes).
DECLARE TYPE parts_t IS TABLE OF parts%ROWTYPE; l_parts parts_t := parts_t (); BEGIN FOR indx IN 1 .. 10000 LOOP l_parts (indx).partnum := indx; l_parts (indx).partname := 'Part' || indx; END LOOP; FORALL indx IN 1 .. l_parts.COUNT INSERT INTO parts (partnum, partname) VALUES (l_parts (indx).partnum, l_parts (indx).partname); END; /
For a much more detailed demonstration of converting row-by-row processing to bulk processing, check out my LiveSQL script.
Check the doc for lots more information on PL/SQL bulk processing.
Well, I could probably go on and on, but this should be enough to give you some solid tips for writing better PL/SQL code and inspire my readers to offer their own additions to the list.
Published at DZone with permission of Steven Feuerstein, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.