The answer provided by Gordon Linoff is definitely good and workable and straight-forward, but it does not address the performance issues in cases like yours.
Namely, the table you are searching against could be very large and have various indexes (say, in your case, an index on NAME
and another index on CITY
). In that case, a simple, static SQL approach may use one index or another and perform poorly in cases where the criteria for the index it is using is not supplied. (I know Oracle 11g introduced adaptive cursor sharing -- I have yet to see it be really helpful though. I'd like to believe it could make my answer outdated and it should make my answer outdated. But I just haven't seen it really work very well yet. I'd welcome comments about it).
Anyway, if you don't have 11g and/or do not want to rely to heavily on adaptive cursor sharing, I think the current best practice for writing such search queries to to use a REF CURSOR
. Like this:
-- Create a table to query from
CREATE TABLE matt1 ( id number, name varchar2(30), city varchar2(30) );
CREATE OR REPLACE PACKAGE matt_query_pkg AS
FUNCTION get_results ( p_id NUMBER, p_name VARCHAR2, p_city VARCHAR2 ) RETURN SYS_REFCURSOR;
END matt_query_pkg;
CREATE OR REPLACE PACKAGE BODY matt_query_pkg AS
FUNCTION get_results ( p_id NUMBER, p_name VARCHAR2, p_city VARCHAR2 )RETURN SYS_REFCURSOR IS
l_rc SYS_REFCURSOR;
l_sql VARCHAR2(32000);
BEGIN
l_sql := 'SELECT id, name, city FROM matt1 WHERE 1=1';
if p_id IS NULL THEN
l_sql := l_sql || ' AND (1=1 OR :b_id IS NULL)';
else
l_sql := l_sql || ' AND (id = :b_id)';
end if;
if p_name IS NULL THEN
l_sql := l_sql || ' AND (1=1 OR :b_name IS NULL)';
else
l_sql := l_sql || ' AND (name = :b_name)';
end if;
if p_city IS NULL THEN
l_sql := l_sql || ' AND (1=1 OR :b_city IS NULL)';
else
l_sql := l_sql || ' AND (id = :b_city)';
end if;
dbms_output.put_line('Executing:');
dbms_output.put_line(l_sql);
OPEN l_rc FOR l_sql USING p_id, p_name, p_city;
RETURN l_rc;
END get_results;
END matt_query_pkg;
In an example case where only the ID
and NAME
criteria is given, it will generate a SQL like this:
SELECT id, name, city
FROM matt1
WHERE 1=1
AND (id = :b_id)
AND (name = :b_name)
AND (1=1 OR :b_city IS NULL)
Oracle's optimizer will factor out the (1=1 OR :b_city IS NULL)
clause (because it knows it is always true
), leaving a SQL that it can then optimize specifically for the criteria given.
NOTE: the point of putting in the (1=1 OR :b_city IS NULL)
clause at all is to keep the number of bind variables constant, so you can always run the search using:
OPEN l_rc FOR l_sql USING p_id, p_name, p_city;
If you didn't put in that (1=1 OR...)
clause, you'd have to have a different OPEN..FOR
statement for every possible combination of null/not null input parameters.
Here is some code to test it, for reference:
-- Test it
insert into matt1 values (1, 'Fred', 'New York');
insert into matt1 values (2, 'Fred', 'Philadelphia');
insert into matt1 values (3, 'John', 'Philadelphia');
insert into matt1 values (4, 'Mark', 'Philadelphia');
insert into matt1 values (5, 'Mark', 'Chicago');
commit;
declare
l_rc SYS_REFCURSOR;
l_id NUMBER;
l_name VARCHAR2(30);
l_city VARCHAR2(30);
begin
l_rc := matt_query_pkg.get_results (NULL, 'Fred', NULL);
loop
fetch l_rc INTO l_id, l_name, l_city;
exit when l_rc%NOTFOUND;
dbms_output.put_line ('Found: ' || l_id || ', ' || l_name || ', ' || l_city);
end loop;
end;