Making the most out of PL/SQL's declarative features — and not just its procedural features — can pay huge dividends. Let's investigate.
Join the DZone community and get the full member experience.Join For Free
SQL is a set-oriented, declarative language. A language or statement is declarative if it describes the computation to be performed (or in SQL, the set of data to be retrieved/modified) without specifying how to compute it.
PL/SQL is a procedural language, tightly integrated with SQL, and intended primarily to allow us to build powerful, secure APIs to underlying data (via SQL).
If I try hard, I can maximize the procedural side of PL/SQL and minimize the declarative aspect of SQL (primarily by ignoring or discounting the set-orientation of SQL). That's generally a bad idea. Instead, we Oracle Database developers make the most of the many powerful features of SQL (think: analytic functions, pattern matching, joins, etc.), and minimize processing in PL/SQL.
What we should also do, though, is recognize the make the most of the declarative features of PL/SQL. There are two big reasons to do this:
1. When you don't tell the PL/SQL compiler how to do things, the optimizer has more freedom to improve performance.
2. You can write less code, improving your productivity and reduce the cost of maintaining your code in the future.
Here are a few of my favorite declarative constructs of PL/SQL.
The Cursor FOR Loop
Definitely the best showcase for the benefits of declarative programming in PL/SQL.
I've identified a set of rows and columns you need to do something with (in the example below, simply display the last name of all employees in department 100 whose salary > 5000).
I could do it the hard, procedural way:
DECLARE CURSOR emps_cur IS SELECT * FROM hr.employees ORDER BY last_name; l_emp emps_cur%ROWTYPE; BEGIN OPEN emps_cur; LOOP FETCH emps_cur INTO l_emp; EXIT WHEN emps_cur%NOTFOUND; IF l_emp.department_id = 100 AND l_emp.salary > 5000 THEN DBMS_OUTPUT.put_line (l_emp.last_name); END IF; END LOOP; CLOSE emps_cur; END;
There. Job done. But...really? I am going to fetch all the rows in the employee table and then check the department ID and salary? No, no, no! That should be done in SQL. So it should at least look like this:
DECLARE CURSOR emps_cur IS SELECT * FROM hr.employees WHERE department_id = 100 AND salary > 5000 ORDER BY last_name; l_emp emps_cur%ROWTYPE; BEGIN OPEN emps_cur; LOOP FETCH emps_cur INTO l_emp; EXIT WHEN emps_cur%NOTFOUND; DBMS_OUTPUT.put_line (l_emp.last_name); END LOOP; CLOSE emps_cur; END;
Now at least we are using a little of the power of SQL, and thereby minimizing the number of rows brought needlessly back from the context switch to the SQL engine.
I could, however, make things much simpler — and faster — with a cursor FOR loop:
BEGIN FOR l_emp IN ( SELECT last_name FROM hr.employees WHERE department_id = 100 AND salary > 5000 ORDER BY last_name) LOOP DBMS_OUTPUT.put_line (l_emp.last_name); END LOOP; END;
In this third iteration, I have stepped waaaay back from writing an algorithm (declare cursor, open cursor, fetch next row, stop when no more rows, display data, back to fetch), and instead have told the compiler, in effect:
Please display the last name for all rows identified by that query.
I don't tell it how to get the job done. I let the compiler figure out the best execution path. No need to open a cursor, fetch, check to see if done, close the cursor, etc.
And boy does the compiler figure out a better execution path! Since I no longer explicitly fetch on a row-by-row basis, the PL/SQL optimizer is free to choose a different approach, and it does (with the PL/SQL optimization level set to 2 or higher): it retrieves 100 rows with each "bulk" fetch, resulting in many fewer context switches and much better performance.
Plus fewer lines of code, which will be greatly appreciated by the developers who maintain your code in years to come.
Nested Table Operators
Nested tables are just one of the three different types of collections in PL/SQL (the others are associative arrays and arrays). But there are a whole boatload of set-oriented features available only to nested tables. That's because they were designed from the start to be, like relational tables. We've got...
- MULTISET UNION — similar to SQL UNION
- MULTISET EXCEPT — similar to SQL MINUS
- MULTISET INTERSECT — similar to SQL INTERSECT
- SET — Removes duplicates from a nested table (multisets can have duplicates)
- SUBMULTISET — Returns TRUE if one nested table is entirely contained within another
- MEMBER OF — Is a value an element of (in) the nested table?
- = — Um, do I need to explain what this does?
You could implement the functionality embedded in each one of these, and you might even have some fun doing it. But almost certainly your code would have bugs, or would be lots slower, or would be...lots of code.
In a way, my favorite of all these is "=". It's the clearest demonstration of the power of declarative programming in this section. Suppose I define two nested tables as follows (that's right: a nested table of nested tables!).
CREATE OR REPLACE TYPE numbers_t IS TABLE OF NUMBER / CREATE OR REPLACE TYPE nt_of_numbers_t IS TABLE OF numbers_t / CREATE OR REPLACE PACKAGE nts AUTHID DEFINER IS n1 nt_of_numbers_t := nt_of_numbers_t (numbers_t (1, 2, 3), numbers_t (4, 5, 6)); n2 nt_of_numbers_t := nt_of_numbers_t (numbers_t (4, 5, 6), numbers_t (1, 2, 3)); END; /
Now I want to know if n1 and n2 are equal (that is, they contain the same elements — and order is not significant), I could write something like this:
DECLARE l_equals BOOLEAN := TRUE; BEGIN FOR indx IN 1 .. nts.n1.COUNT LOOP FOR indx2 IN 1 .. nts.n1 (indx).COUNT LOOP l_equals := nts.n1 (indx)(indx2) = nts.n2 (indx)(indx2); EXIT WHEN NOT l_equals; END LOOP; END LOOP; DBMS_OUTPUT.put_line ( CASE WHEN l_equals THEN '=' ELSE '<>' END); END; /
That's already bad enough — but then if you factor in the logic you need to write to ensure that order is not significant...OMG. Sure, we can write that stuff. We all took classes on algorithms in university (or some of us, anyway. My university education in computer science was actually very skimpy). We all know how to type s-t-a-c-k-o-v in Google.
So, yeah, we could muscle our way through it. But why? Instead, I could write nothing more than:
BEGIN DBMS_OUTPUT.put_line ( CASE WHEN nts.n1 = nts.n2 THEN '=' ELSE '<>' END); END; /
And again, in addition to the simplicity of the code, you make it possible for your code to get faster over time, as the very smart folks over at Oracle HQ continually work to optimize the performance of built-in elements of the PL/SQL language.
Here are some links to LiveSQL scripts demonstrating these features:
Okay, I confess that using anchored declarations doesn't always result in less code. Sometimes you will type a few more characters. But over time, you are sure to save many keystrokes when it comes to not having to change your code in the future — and debug it today.
What, you may ask, is an anchored declaration? When you anchor or connect the datatype for your variable or constant declaration back to another, previously-defined element.
You can anchor back to another PL/SQL variable or constant, but the real value of this syntax comes from the ability to anchor back to a column or table. PL/SQL is, after all, a database programming language. So it should be hyper-aware of and able to take advantage of stuff that's in the database.
Suppose I want to declare a variable that "looks like" the last_name column in the employee table. I could look up the definition of the table, see that last_name is VARCHAR2(100), and then write this code:
PROCEDURE do_stuff IS l_last_name VARCHAR2(100);
Fine. They are in synch. Well, only in your mind. When the DBA or another developer comes along later and issues this DDL statement:
ALTER TABLE employees MODIFY last_name VARCHAR2(200)
Your code and your table are now officially out of synch. And if you select a really long last name from the table into that variable, kaboom! Your program fails with a VALUE_ERROR exception.
That's no good. Instead, declare your variable as follows
PROCEDURE do_stuff IS l_last_name employees.last_name%TYPE;
Now you have declaratively linked your variable to this database structure. And, lo and behold, if the definition of this column changes, Oracle Database will automatically invalidate the DO_STUFF procedure. When it is recompiled, it will pick up the new definition of last_name, and your code will be fully in synch with your table.
%TYPE's perfect for individual columns. Use %ROWTYPE when you want to declare a record based on a table, view or cursor (essentially what the cursor FOR loop does for you implicitly).
PROCEDURE do_stuff IS l_employee employees%ROWTYPE;
Bottom line: you tell the PL/SQL compiler "Declare this variable to be like that other thing." and you are done. Oracle Database automatically tracks dependencies and ensures that your code always matches the state of your database objects.
How cool is that?
Do you have a suggestion for another handy-dandy declarative construct? Let me know and I can add it the post (with your name up in lights!).
Published at DZone with permission of Steven Feuerstein, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.