You have two objectives
keep the code DRY
and
use bind variables
If you have only a very simple condition (as in your example, use filter or use no filter) you my use IF
statement to open different cursors for both cases.
IF salary_from is null THEN
OPEN v_cursor FOR SELECT LAST_NAME, SALARY FROM hr.employees;
ELSE
OPEN v_cursor FOR SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= salary_from;
END IF;
Note that you should be carefull with the OR
solution using the predicate SALARY >= salary_from OR salary_from is NULL
Why? You use one query for two very different scenarios. The cursor can returns either all data or very limited data, that could require a different access method (e.g. index access vs full table scan). So you may and with suboptimal plan in one case.
The problem with the above approach is that it does not scale. If you have four optional criteria you will need 16-fold IF
with a highly redundant code.
What is the solution to keep the above objectives valid?
Use dynamic SQL, but do not concatenate the criteria values such as
SELECT LAST_NAME, SALARY FROM hr.employees WHERE salary >= 1000 and salary <= 10000
This will invalidate the bind variable objective!
To keep with the example with optional parameter salary_from and salary_to you want to use
open v_cursor for v_sql using salary_from, salary_to;
But this requires, that both bind variables must be defined in the query text - what should be done in case you have only salary_from?
Opening the cursor for this query
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from
will lead to error
ORA-01006: bind variable does not exist
The trick is to use dummy predicates returning always true
, but containing a bind variable (that will be ignored).
So if you have only salary_from as a filter you will create following dynamic SQL
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND (1=1 or SALARY <= :salary_to)
It contains both bind variables and the optimizer (with the *shortcut evaluation) will simplify it to the required
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from
So the relevant code for the salary filter example would be
IF salary_from is NOT null THEN
v_sql := 'SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from';
ELSE
v_sql := 'SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from)';
END IF;
IF salary_to is NOT null THEN
v_sql := v_sql ||' AND SALARY <= :salary_to';
ELSE
v_sql := v_sql ||' AND (1=1 or SALARY <= :salary_to)';
END IF;
open v_cursor for v_sql using salary_from, salary_to;
Below the overview of the generated SQL for the four cases
-- no filter
SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from) AND (1=1 or SALARY <= :salary_to)
-- salary_from
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND (1=1 or SALARY <= :salary_to)
-- salary from, to
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND SALARY <= :salary_to
-- salary_to
SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from) AND SALARY <= :salary_to
Similar topics with credit to this idea: here, here and here