WHERE column1 = NVL(:B1, column1)
AND column2 = NVL(:B2, column2)
The intention of this WHERE clause is to allow 4 different scenarios to be covered.
1) If :B1 is null and :B2 is null, then retrieve all rows.
2) If :B1 is null and :B2 is not null, then retrieve only rows where column2 = :B2.
3) If :B2 is null and :B1 is not null, then retrieve only rows where column1 = :B1.
4) If :B1 is not null and :B2 is not null, then retrieve only rows where column1 = :B1 and column2 = :B2.
The logic is that since the two conditions are AND'ed, that both have to evaluate to true in order to produce any rows. If, however, you wish to be flexible and allow for all 4 cases to be possible with a single query, then you have to find a way to ensure that both sides of the AND evaluate TRUE regardless of what inputs you receive. By using the NVL, you can accomplish that. You either pass in a null value or a not null value and the NVL check will make sure that the condition always evaluates to true. You get to either qualify which rows are returned by passing in a not null bind variable or you can leave the selection of rows unqualified by passing in a null value. Simple, huh?
Well, it may be simple in terms of the way the code looks, but what it does to the optimizer isn't that simple. The optimizer has to come up with a plan that can best serve all 4 possibilities equally. It can't/won't come up with 4 different plans.
Let me show you a few tests. I'll be using a test table I had in place from some previous tests for another post. It's a simple 2500 row table with 2 indexes (T2_I3 on the hair column and T2_I6 on the work column). Take a look at what happens when I execute my query 4 times using the 4 different scenarios I noted above.
SQL>set serveroutput off
SQL>variable b1 number
SQL>variable b2 number
SQL>exec :b1 := null;
PL/SQL procedure successfully completed.
SQL>exec :b2 := null ;
PL/SQL procedure successfully completed.
SQL>
SQL>select /*+ gather_plan_statistics */ count(*)
2 from bitmap_t2
3 where hair = NVL(:b1, hair)
4 and work = NVL(:b2, work) ;
COUNT(*)
---------------
2500
1 row selected.
SQL>
SQL>@allstats_last
PLAN_TABLE_OUTPUT
------------------
SQL_ID 16jagac5xq0r4, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ count(*) from bitmap_t2 where hair = NVL(:b1, hair) and work
= NVL(:b2, work)
Plan hash value: 2153464131
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.08 | 2504 | 342 |
| 2 | CONCATENATION | | 1 | | 2500 |00:00:00.07 | 2504 | 342 |
|* 3 | FILTER | | 1 | | 2500 |00:00:00.06 | 2504 | 342 |
|* 4 | TABLE ACCESS FULL | BITMAP_T2 | 1 | 357 | 2500 |00:00:00.03 | 2504 | 342 |
|* 5 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | 0 |
|* 6 | TABLE ACCESS BY INDEX ROWID| BITMAP_T2 | 0 | 5 | 0 |00:00:00.01 | 0 | 0 |
|* 7 | INDEX RANGE SCAN | T2_I6 | 0 | 32 | 0 |00:00:00.01 | 0 | 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter(:B2 IS NULL)
4 - filter(("HAIR"=NVL(:B1,"HAIR") AND "WORK" IS NOT NULL))
5 - filter(:B2 IS NOT NULL)
6 - filter("HAIR"=NVL(:B1,"HAIR"))
7 - access("WORK"=:B2)
SQL>
SQL>exec :b2 := 45 ;
PL/SQL procedure successfully completed.
SQL>
SQL>select /*+ gather_plan_statistics */ count(*)
2 from bitmap_t2
3 where hair = NVL(:b1, hair)
4 and work = NVL(:b2, work) ;
COUNT(*)
---------------
32
1 row selected.
SQL>
SQL>@allstats_last
PLAN_TABLE_OUTPUT
------------------
SQL_ID 16jagac5xq0r4, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ count(*) from bitmap_t2 where hair = NVL(:b1, hair) and work
= NVL(:b2, work)
Plan hash value: 2153464131
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 67 | 4 |
| 2 | CONCATENATION | | 1 | | 32 |00:00:00.01 | 67 | 4 |
|* 3 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | 0 |
|* 4 | TABLE ACCESS FULL | BITMAP_T2 | 0 | 357 | 0 |00:00:00.01 | 0 | 0 |
|* 5 | FILTER | | 1 | | 32 |00:00:00.01 | 67 | 4 |
|* 6 | TABLE ACCESS BY INDEX ROWID| BITMAP_T2 | 1 | 5 | 32 |00:00:00.01 | 67 | 4 |
|* 7 | INDEX RANGE SCAN | T2_I6 | 1 | 32 | 32 |00:00:00.01 | 35 | 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter(:B2 IS NULL)
4 - filter(("HAIR"=NVL(:B1,"HAIR") AND "WORK" IS NOT NULL))
5 - filter(:B2 IS NOT NULL)
6 - filter("HAIR"=NVL(:B1,"HAIR"))
7 - access("WORK"=:B2)
SQL>
SQL>exec :b1 := 5;
PL/SQL procedure successfully completed.
SQL>exec :b2 := null ;
PL/SQL procedure successfully completed.
SQL>
SQL>select /*+ gather_plan_statistics */ count(*)
2 from bitmap_t2
3 where hair = NVL(:b1, hair)
4 and work = NVL(:b2, work) ;
COUNT(*)
---------------
357
1 row selected.
SQL>
SQL>@allstats_last
PLAN_TABLE_OUTPUT
------------------
SQL_ID 16jagac5xq0r4, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ count(*) from bitmap_t2 where hair = NVL(:b1, hair) and work
= NVL(:b2, work)
Plan hash value: 2153464131
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.04 | 2504 | 342 |
| 2 | CONCATENATION | | 1 | | 357 |00:00:00.04 | 2504 | 342 |
|* 3 | FILTER | | 1 | | 357 |00:00:00.03 | 2504 | 342 |
|* 4 | TABLE ACCESS FULL | BITMAP_T2 | 1 | 357 | 357 |00:00:00.03 | 2504 | 342 |
|* 5 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | 0 |
|* 6 | TABLE ACCESS BY INDEX ROWID| BITMAP_T2 | 0 | 5 | 0 |00:00:00.01 | 0 | 0 |
|* 7 | INDEX RANGE SCAN | T2_I6 | 0 | 32 | 0 |00:00:00.01 | 0 | 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter(:B2 IS NULL)
4 - filter(("HAIR"=NVL(:B1,"HAIR") AND "WORK" IS NOT NULL))
5 - filter(:B2 IS NOT NULL)
6 - filter("HAIR"=NVL(:B1,"HAIR"))
7 - access("WORK"=:B2)
SQL>
SQL>exec :b1 := 5;
PL/SQL procedure successfully completed.
SQL>exec :b2 := 45 ;
PL/SQL procedure successfully completed.
SQL>
SQL>select /*+ gather_plan_statistics */ count(*)
2 from bitmap_t2
3 where hair = NVL(:b1, hair)
4 and work = NVL(:b2, work) ;
COUNT(*)
---------------
5
1 row selected.
SQL>
SQL>@allstats_last
PLAN_TABLE_OUTPUT
------------------
SQL_ID 16jagac5xq0r4, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ count(*) from bitmap_t2 where hair = NVL(:b1, hair) and work
= NVL(:b2, work)
Plan hash value: 2153464131
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 67 | 5 |
| 2 | CONCATENATION | | 1 | | 5 |00:00:00.02 | 67 | 5 |
|* 3 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | 0 |
|* 4 | TABLE ACCESS FULL | BITMAP_T2 | 0 | 357 | 0 |00:00:00.01 | 0 | 0 |
|* 5 | FILTER | | 1 | | 5 |00:00:00.02 | 67 | 5 |
|* 6 | TABLE ACCESS BY INDEX ROWID| BITMAP_T2 | 1 | 5 | 5 |00:00:00.02 | 67 | 5 |
|* 7 | INDEX RANGE SCAN | T2_I6 | 1 | 32 | 32 |00:00:00.01 | 35 | 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter(:B2 IS NULL)
4 - filter(("HAIR"=NVL(:B1,"HAIR") AND "WORK" IS NOT NULL))
5 - filter(:B2 IS NOT NULL)
6 - filter("HAIR"=NVL(:B1,"HAIR"))
7 - access("WORK"=:B2)
Note how with each different set of bind variable values, the query produces a different result. But the plan remains the same (check the SQL_ID, child_number and plan hash values...they all match). You can also see how one set of the plan operations doesn't execute (note the 0's in the A-Rows, Buffers and Reads columns).
What this tells you is that the optimizer created a plan that would work dynamically. Well, sorta. Basically, the checks (step 3 and step 5) are made to see if :B2 IS NULL. If it is null, then step 4 executes. If it is not null, then steps 6-7 execute. This works fine for when you want the whole table (both bind values are null) or when you want to retrieve rows only where the work column equals some value. But, when you want to retrieve rows where you need to qualify both the work and hair columns or when you want to access rows for a particular hair value, the plan is sub-optimal.
So, OK...you're batting .500. Not bad I suppose. ;) But what if your SLA says you have to bat .900 or better? This plan just won't do. You need each different scenario to produce as optimal a plan as possible. How can you do that?
One way is to write your code to build the WHERE clause on the fly and use EXECUTE IMMEDIATE. The pseudo-code to do this might go something like this:
full_string := 'SELECT (rest of stuff).....';
if :b1 is null and :b2 is null then
where_string := null;
elsif :b1 is not null and :b2 is not null then
where_string := ' WHERE hair = :B1 AND work = :B2';
elsif :b1 is null and :b2 is not null then
where_string := ' WHERE work = :B2';
elsif :b1 is not null and :b2 is null then
where_string := ' WHERE hair = :B1';
full_string := full_string || where_string;
EXECUTE IMMEDIATE...
This way, you have 4 different statements and therefore 4 different plans. Each one (hopefully) optimal.
Another option would be to change your indexing strategy. Perhaps you could create a multi-column index using both columns. But, even with this option, you're not going to get "perfect" results. Depending on which column you decide to lead the index with, one of the index scans would likely have to do a skip scan or a full scan which wouldn't be as effective.
So, while the idea to use a single statement with conditional logic (in this case, NVL) and binds looks appealing, just remember that the plan you end up with may not provide the performance you want/need for all possible scenarios. If you use this approach, be prepared to create indexes properly so as to get the best possible coverage for all scenarios. I think the bottom line is that if you truly want dynamic SQL, then really use dynamic SQL. The attempt to simplify or to have a single statement that covers many different possible needs can cause performance to suffer for a some percentage (perhaps a significant %) of the scenarios.