Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue with Oracle bind variables not using index properly

In my scenario, the following query runs fast (0.5 seconds on a table with 70 million rows):

select * from Purchases
where (purchase_id = 1700656396)

and, it even runs fast using bind variables:

var purchase_id number := 1700656396
select * from Purchases
where (purchase_id = :purchase_id)

These run fast because I have an index on the purchase_id column. (Keep reading...)

I need to create a query that allows "filtering" on arbitrary columns. This means providing several input variables, and filtering on each one unless it is null. This works fine at first.

For example, the following query runs fast (0.5 seconds), too:

select * from Purchases
where (1700656396 IS NULL OR purchase_id    = 1700656396)
and   (NULL       IS NULL OR purchase_name  = NULL)
and   (NULL       IS NULL OR purchase_price = NULL)

But, when I attempt to parameterize the query, either by bind variables or stored procedure, the query slows down dramatically (1.5 minutes), as if it is ignoring any indexes:

var purchase_id    number   := 1700656396
var purchase_name  varchar2 := NULL
var purchase_price number   := NULL
select * from Purchases
where (:purchase_id    IS NULL OR purchase_id    = :purchase_id)
and   (:purchase_name  IS NULL OR purchase_name  = :purchase_name)
and   (:purchase_price IS NULL OR purchase_price = :purchase_price)

Right now, in my application, I am forced to dynamically construct my query at run-time in order to get decent performance. This means I lose all the advantages of parameterized queries, and forces me to worry about SQL injection.

Is it possible to avoid dynamically-constructed queries while still maintaining the same logic?

like image 872
Jay Sullivan Avatar asked Jul 16 '13 15:07

Jay Sullivan


People also ask

Why is Oracle not using my index?

Oracle not using an index can be due to: · Bad/incomplete statistics - Make sure to re-analyze the table and index with dbms_stats to ensure that the optimizer has good metadata.

How do you bind variables in Oracle?

You simply have to write a command which starts with keyword VARIABLE followed by the name of your bind variable which is completely user defined along with the data type and data width. That's how we declare a bind variable in Oracle database.

How can we avoid hard parsing in Oracle?

so Oracle will always run the same sql (select * from emp where id = :id) but value of variable will be changed so it will bring an another row(s) for you and also this will prevent hard parsing because Oracle already know this sql.

What are the advantages of bind variables in Oracle?

The advantage of using bind variables is that, if the same query is executed multiple times with different values being bound in, then the same execution plan is used because the query itself hasn't actually changed (so no hard parsing and determining the best plan has to be performed, saving time and resources).


3 Answers

This is a bigger topic really, but this is the approach that I think is easiest to implement and works well. The trick is to use dynamic SQL, but implement it so that you always pass the same number of parameters (needed), AND you allow Oracle to short-circuit when you don't have a value for a parameter (what you are lacking in your current approach). For example:

set serveroutput on
create or replace procedure test_param(p1 in number default null, p2 in varchar2 default null) as
  l_sql varchar2(4000);
  l_cur sys_refcursor;
  l_rec my_table%rowtype;
  l_ctr number := 0;
begin

  l_sql := 'select * from my_table where 1=1';
  if (p1 is not null) then
    l_sql := l_sql || ' and my_num_col = :p1';
  else
    -- short circuit for optimizer (1=1)
    l_sql := l_sql || ' and (1=1 or :p1 is null)';
  end if;

  if (p2 is not null) then
    l_sql := l_sql || ' and name like :p2';
  else
    -- short circuit for optimizer (1=1)
    l_sql := l_sql || ' and (1=1 or :p2 is null)';
  end if;

  -- show what the SQL query will be
  dbms_output.put_line(l_sql);

  -- note always have same param list (using)
  open l_cur for l_sql using p1,p2;

  -- could return this cursor (function), or simply print out first 10 rows here for testing
  loop
    l_ctr := l_ctr + 1;
    fetch l_cur
    into l_rec;
    exit when l_cur%notfound OR l_ctr > 10;

    dbms_output.put_line('Name is: ' || l_rec.name || ', Address is: ' || l_rec.address1);
  end loop;
  close l_cur;
end;

To test, simply run it. For example:

set serveroutput on
-- using 0 param
exec test_param();
-- using 1 param
exec test_param(123456789);
-- using 2 params
exec test_param(123456789, 'ABC%');

On my system, the table used is over 100mm rows with an index on the number field and name field. Returns almost instantly. Also note that you may not want to do a select * if you don't need all columns, but I'm being a bit lazy and using %rowtype for this example.

Hope that helps

like image 195
tbone Avatar answered Oct 10 '22 01:10

tbone


Just a quick question: I guess the following non-parameterized query will also run for 1.5 minutes?

select * from Purchases
where (1700656396 IS NULL OR purchase_id    = 1700656396)
and   ('some-name' IS NULL OR purchase_name  = 'some-name')
and   (12       IS NULL OR purchase_price = 12)

If yes, the problem is not the bind variables but the lack of indexes.

EDIT The problem is, Oracle cannot decide to use the index when generating the plan for the parametrized query

like image 22
bpgergo Avatar answered Oct 10 '22 01:10

bpgergo


Taking a different approach to tbone's answer, I realized that I can dynamically construct the query in code, and still use bind variables (and thus gain flexibility with indexes, and still be 100% protected from SQL injection).

In my code, I can do something like this:

string sql = "select * from Purchases where 1 = 1";
if(purchase_id != null)    sql += " and (purchase_id = :purchase_id)";
if(purchase_name != null)  sql += " and (purchase_name = :purchase_name)";
if(purchase_price != null) sql += " and (purchase_price = :purchase_price)";

I tested this and it solves my issue.

like image 44
Jay Sullivan Avatar answered Oct 10 '22 01:10

Jay Sullivan