To Exist or Not To Exist
An interesting discussion on the PL/SQL Challenge blog here has led to me changing my mind about “the best way” to loop through a sparse PL/SQL associative array.
Normally, if we know that an array has been filled, with no gaps in indices, we would use a simple FOR LOOP:
DECLARE TYPE t IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; a t; BEGIN SELECT x BULK COLLECT INTO a FROM mytable; FOR i IN a.FIRST..a.LAST LOOP -- process a(i) END LOOP; END;
If, however, the array may be sparsely filled (i.e. there might be one or more gaps in the sequence), this was “the correct way” to loop through it:
Method A (First/Next)
DECLARE TYPE t IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; a t; i BINARY_INTEGER; BEGIN ... i := a.FIRST; LOOP EXIT WHEN i IS NULL; -- process a(i) i := a.NEXT(i); END LOOP; END;
Method A takes advantage of the fact that an associative array in Oracle is implemented internally as a linked list – the fastest way to “skip over” any gaps is to call the NEXT operator on the list for a given index.
Alternatively, one could still just loop through all the indices from the first to the last index; but the problem with this approach is that if an index is not found in the array, it will raise the NO_DATA_FOUND exception. Well, Method B simply catches the exception:
Method B (Handle NDF)
DECLARE TYPE t IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; a t; BEGIN ... FOR i IN a.FIRST..a.LAST LOOP BEGIN -- process a(i) EXCEPTION WHEN NO_DATA_FOUND THEN NULL; END; END LOOP; END;
This code effectively works the same (with one important proviso*) as Method A. The difference, however, is in terms of relative performance. This method is much faster than Method A, if the array is relatively dense. If the array is relatively sparse, Method A is faster.
* It must be remembered that the NO_DATA_FOUND exception may be raised by a number of different statements in a program: if you use code like this, you must make sure that the exception was only raised by the attempt to access a(i), and not by some other code!
A third option is to loop through as in Method B, but call the EXISTS method on the array to check if the index is found, instead of relying on the NO_DATA_FOUND exception.
Method C (EXISTS)
DECLARE TYPE t IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; a t; BEGIN ... FOR i IN a.FIRST..a.LAST LOOP IF a.EXISTS(i) THEN -- process a(i) END IF; END LOOP; END;
The problem with this approach is that it effectively checks the existence of i in the array twice: once for the EXISTS check, and if found, again when actually referencing a(i). For a large array which is densely populated, depending on what processing is being done inside the loop, this could have a measurable impact on performance.
Bottom line: there is no “one right way” to loop through a sparse associative array. But there are some rules-of-thumb about performance we can take away:
- When the array is likely often very sparsely populated with a large index range, use Method A (First/Next).
- When the array is likely often very densely populated with a large number of elements, use Method B (Handle NDF). But watch how you catch the NO_DATA_FOUND exception!
- If you’re not sure, I’d tend towards Method A (First/Next) until performance problems are actually evident.
You probably noticed that I haven’t backed up any of these claims about performance with actual tests or results. You will find some in the comments to the afore-mentioned PL/SQL Challenge blog post; but I encourage you to log into a sandpit Oracle environment and test it yourself.