Creating SQL-Injection proof dynamic where-clause from collection in PL/SQL - oracle

I need to execute a query where the where-clause is generated based on user input. The input consists of 0 or more pairs of varchar2s.
For example:
[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]
This would translate into:
where (attrib = 'firstname' and value = 'John')
and (attrib = 'lastname' and value = 'Smith')
and (attrib = 'street' and value = 'somestreetname')
This is not the actual data structure as there are several tables but for this example lets keep it simple and say the values are in 1 table. Also I know the parentheses are not necessary in this case but I put them there to make things clear.
What I do now is loop over them and concatinate them to the SQL string. I made a stored proc to generate this where-clause which might also not be very secure since I just concat to the original query.
Something like the following, where I try to get the ID's of the nodes that correspond with the requested parameters:
l_query := select DISTINCT n.id from node n, attribute_values av
where av.node_id = n.id ' || getWhereClause(p_params)
open l_rc
for l_query;
fetch l_rc bulk collect into l_Ids;
close l_rc;
But this is not secure so I'm looking for a way that can guaranty security and prevent SQL-Injection attacks from happening.
Does anyone have any idea on how this is done in a secure way? I would like to use bindings but I don't see how I can do this when you dont know the number of parameters.
DB: v12.1.0.2 (i think)

It's still a bit unclear and generalised, but assuming you have a schema-level collection type, something like:
create type t_attr_value_pair as object (attrib varchar2(30), value varchar2(30))
/
create type t_attr_value_pairs as table of t_attr_value_pair
/
then you can use the attribute/value pairs in the collection for the bind:
declare
l_query varchar2(4000);
l_rc sys_refcursor;
type t_ids is table of number;
l_ids t_ids;
l_attr_value_pairs t_attr_value_pairs;
-- this is as shown in the question; sounds like it isn't exactly how you have it
p_params varchar2(4000) := q'^[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]^';
begin
-- whatever mechanism you want to get the value pairs into a collection;
-- this is just a quick hack to translate your example string
select t_attr_value_pair(rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, (2 * level) - 1, null, 1),
'[('''), ''''),
rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, 2 * level, null, 1),
''''), ''')]'))
bulk collect into l_attr_value_pairs
from dual
connect by level <= regexp_count(p_params, ',') / 2 + 1;
l_query := 'select DISTINCT id from attribute_values
where (attrib, value) in ((select attrib, value from table(:a)))';
open l_rc for l_query using l_attr_value_pairs;
fetch l_rc bulk collect into l_ids;
close l_rc;
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
although it doesn't need to be dynamic with this approach:
...
begin
-- whatever mechamism you want to get the value pairs into a collection
...
select DISTINCT id
bulk collect into l_ids
from attribute_values
where (attrib, value) in ((select attrib, value from table(l_attr_value_pairs)));
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
or with a join to the table collection expression:
select DISTINCT av.id
bulk collect into l_ids
from table(l_attr_value_pairs) t
join attribute_values av on av.attrib = t.attrib and av.value = t.value;
Other collection types will need different approaches.
Alternatively, you could still build up your where clause with one condition per attribute/value pair, while still making them bind variables - but you would need two levels of dynamic SQL, similar to this.

Related

PL/SQL Creating a procedure that contains result set joins

I want to create a procedure in PL/SQL that has 5 steps. Step 1 and 2 execute first and return an ID. In step 3, we have a SELECT statement that has a condition with that returned ID. I want then to take all of the results of that SELECT statement and use them in a JOIN in another SELECT statement and use THOSE results in a 3rd SELECT statement again using JOIN. From what I've seen, I can't use CURSOR in JOIN statements. Some of my co-workers have suggested that I save the results in a CURSOR and then use a loop to iterate through each row and use that data for the next SELECT. However since I'm going to do 2 selects this will create a huge fork of inside loops and that's exactly what I'm trying to avoid.
Another suggestion was to use Temprary Tables to store the data. However this procedure could be executed at the same time by many users and the table's data would conflict with each other. Right now I'm looking at LOCAL Temporary tables that supposedly filter the data according the the session but I'm not really sure I want to create dummy tables for my procedures since I want to avoid leaving trash in the schema (this procedure is for a custom part of the application). Is there a standard way of doing this? Any ideas?
Sample:
DECLARE
USERID INT := 1000000;
TEXT1 VARCHAR(100);
TEXT_INDEX INT;
CURSOR NODES IS SELECT * FROM NODE_TABLE WHERE DESCRIPTION LIKE TEXT || '%';
CURSOR USERS IS SELECT * FROM USERGROUPS JOIN NODES ON NODES.ID = USERGROUPS.ID;
BEGIN
SELECT TEXT INTO TEXT1 FROM TABLE_1 WHERE ID = USERID;
TEXT_INDEX = INSTR(TEXT, '-');
TEXT = SUBSTR(TEXT, 0, TEXT_INDEX);
OPEN NODES;
OPEN USERS;
END;
NOTE: This does NOT work. Oracle doesn't support joins between cursors.
NOTE2: This CAN be done in a single query but for the sake of argument (and in my real use case) I want to break those steps down in a procedure. The sample code is a depiction of what I'm trying to achieve IF joins between cursors worked. But they don't and I'm looking for an alternative.
I ended up using a function (although a procedure could be used as well) along with tables. Things I've learned and one should pay attention to:
PL/SQL functions can only return types that have been declared in the schema in advance and are clear. You can't create a function that returns something like MY_TABLE%ROWTYPE, even though it seems the type information is available it is not acceptable. You have to instead create a custom type of MY_TABLE%ROWTYPE is you want to return it.
Oracle treats tables of declared types differently from tables of %ROWTYPE. This confused the hell out of me at first but from what I've gathered this is how it works.
DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
Declares a collection of types of MY_TABLE row. In order to add to this we must use BULK COLLECT INTO from an SQL statement that queries MY_TABLE. The resulting collection CANNOT be used in JOIN statements is not queryable and CANNOT be returned by a function.
DECLARE
CREATE TYPE MY_CUSTOM_TYPE AS OBJECT (COL_A NUMBER, COL_B NUMBER);
CREATE TYPE MY_CUSTOM_TABLE AS TABLE OF MY_CUSTOM_TYPE;
my_custom_tab MY_CUSTOM_TABLE;
This create my_custom_tab which is a table (not a collection) and if populated can be queried at using TABLE(my_custmo_tab) in the FROM statement. As a table which is declared in advance in the schema this CAN be returned from a function. However it CANNOT be populated using BULK COLLECT INTO since it is not a collection. We must instead use the normal SELECT INTO statement. However, if we want to populate it with data from an existing table that has 2 number columns we cannot simply do SELECT * INTO my_custom_tab FROM DOUBLE_NUMBER_TABLE since my_custom_tab hasn't been initialized and doesn't contain enough rows to receive the data. And if we don't know how many rows a query returns we can't initialize it. The trick into populating the table is to use the CAST command and cast our select result set as a MY_CUSTOM_TABLE and THEN add it.
SELECT CAST(MULTISET(SELECT COL_A, COL_B FROM DOUBLE_NUMBER_TABLE) AS MY_CUSTOM_TABLE) INTO my_custom_tab FROM DUAL
Now we can easily use my_custom_tab in queries etc through the use of the TABLE() function.
SELECT * FROM TABLE(my_custom_tab)
is valid.
You can do such decomposition in many ways, but all of them have a significant performance penalty in comaration with single SQL statement.
Maintainability improvement are also questionable and depends on specific situation.
To review all possibilities please look through documentation.
Below is some possible variants based on simple logic:
calculate Oracle user name prefix based on given Id;
get all users whose name starts with this prefix;
find all tables owned by users from step 2;
count a total number of found tables.
1. pipelined
Prepare types to be used by functions:
create or replace type TUserRow as object (
username varchar2(30),
user_id number,
created date
)
/
create or replace type TTableRow as object (
owner varchar2(30),
table_name varchar2(30),
status varchar2(8),
logging varchar2(3)
-- some other useful fields here
)
/
create or replace type TUserList as table of TUserRow
/
create or replace type TTableList as table of TTableRow
/
Simple function to find prefix by user id:
create or replace function GetUserPrefix(piUserId in number) return varchar2
is
vUserPrefix varchar2(30);
begin
select substr(username,1,3) into vUserPrefix
from all_users
where user_id = piUserId;
return vUserPrefix;
end;
/
Function searching for users:
create or replace function GetUsersPipe(
piNameStart in varchar2
)
return TUserList pipelined
as
vUserList TUserList;
begin
for cUsers in (
select *
from
all_users
where
username like piNameStart||'%'
)
loop
pipe row( TUserRow(cUsers.username, cUsers.user_id, cUsers.created) ) ;
end loop;
return;
end;
Function searching for tables:
create or replace function GetUserTablesPipe(
piUserNameStart in varchar2
)
return TTableList pipelined
as
vTableList TTableList;
begin
for cTables in (
select *
from
all_tables tab_list,
table(GetUsersPipe(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
)
loop
pipe row ( TTableRow(cTables.owner, cTables.table_name, cTables.status, cTables.logging) );
end loop;
return;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTablesPipe(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
2. Simple table functions
This solution use same types as a variant with pipelined functions above.
Function searching for users:
create or replace function GetUsers(piNameStart in varchar2) return TUserList
as
vUserList TUserList;
begin
select TUserRow(username, user_id, created)
bulk collect into vUserList
from
all_users
where
username like piNameStart||'%'
;
return vUserList;
end;
/
Function searching for tables:
create or replace function GetUserTables(piUserNameStart in varchar2) return TTableList
as
vTableList TTableList;
begin
select TTableRow(owner, table_name, status, logging)
bulk collect into vTableList
from
all_tables tab_list,
table(GetUsers(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
;
return vTableList;
end;
/
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTables(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
3. cursor - xml - cursor
It's is a specific case, which may be implemented without user-defined types but have a big performance penalty, involves unneeded type conversion and have a low maintainability.
Function searching for users:
create or replace function GetUsersRef(
piNameStart in varchar2
)
return sys_refcursor
as
cUserList sys_refcursor;
begin
open cUserList for
select * from all_users
where username like piNameStart||'%'
;
return cUserList;
end;
Function searching for tables:
create or replace function GetUserTablesRef(
piUserNameStart in varchar2
)
return sys_refcursor
as
cTableList sys_refcursor;
begin
open cTableList for
select
tab_list.*
from
(
XMLTable('/ROWSET/ROW'
passing xmltype(GetUsersRef(piUserNameStart))
columns
username varchar2(30) path '/ROW/USERNAME'
)
) user_list,
all_tables tab_list
where
tab_list.owner = user_list.username
;
return cTableList;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from
XMLTable('/ROWSET/ROW'
passing xmltype(GetUserTablesRef(GetUserPrefix(vUserId)))
columns
table_name varchar2(30) path '/ROW/TABLE_NAME'
)
;
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
Of course, all variants may be mixed, but SQL looks better at least for simple cases:
declare
vUserId number := 5;
vUserPrefix varchar2(100);
vTableCount number;
begin
-- Construct prefix from Id
select max(substr(user_list.username,1,3))
into vUserPrefix
from
all_users user_list
where
user_list.user_id = vUserId
;
-- Count number of tables owned by users with name started with vUserPrefix string
select
count(1) into vTableCount
from
all_users user_list,
all_tables table_list
where
user_list.username like vUserPrefix||'%'
and
table_list.owner = user_list.username
;
dbms_output.put_line('Users with name started with "'||vUserPrefix||'" owns '||vTableCount||' tables');
end;
P.S. All code only for demonstration purposes: no optimizations and so on.

Oracle: Pure PL/SQL data extraction and anonymization using temporary tables, read-only permissions

I am trying to create a PL/SQL script that extracts a root "object" together with all children and other relevant information from an oracle production database. The purpose is to create a set of test-data to recreate issues that are encountered in production. Due to data protection laws the data needs to be anonymized when extracted - object names, certain types of id's, and monetary amounts need to be replaced.
I was trying to create one or more temporary translation tables, which would contain both the original values and anonymized versions. Then I would join the real data with the translation tables and output the anonymized values wherever required.
DECLARE
rootId integer := 123456;
TYPE anonTableRow IS RECORD
(
id NUMBER,
fieldC NUMBER,
anonymizedFieldC NUMBER
);
TYPE anonTable IS TABLE OF anonTableRow;
anonObject anonTable;
BEGIN
FOR cursor_row IN
(
select
id,
fieldC,
1234 -- Here I would create anonymized values based on rowNum or something similar
from
prodTable
where id = rootId
)
LOOP
i := i + 1;
anonObject(i) := cursor_row;
END LOOP;
FOR cursor_row IN
(
select
prod_table.id,
prod_table.fieldB,
temp_table.anonymizedFieldC fieldC,
prod_table.fieldD
from
prod_table
inner join table(temp_table) on prod_table.id = temp_table.id
where prod_table.id = 123456789
)
LOOP
dbms_output.put_line('INSERT INTO prod_table VALUES (' || cursor_row.id || ', ' || cursor_row.fieldB || ', ' || cursor_row.fieldC || ', , ' || cursor_row.fieldD);
END LOOP;
END;
/
However I ran into several problems with this approach - it seems to be near impossible to join oracle PL/SQL tables with real database tables. My access to the production database is severely restricted, so I cannot create global temporary tables, declare types outside PL/SQL or anything of that sort.
My attempt to declare my own PL/SQL types failed with the problems mentioned in this question - the solution does not work for me because of the limited permissions.
Is there a pure PL/SQL way that does not require fancy permissions to achieve something like the above?
Please Note: The above code example is simplified a lot and would not really require a separate translation table - in reality I need access to the original and translated values in several different queries, so I would prefer not having to "recalculate" translations everywhere.
If your data is properly normalized, then I guess this should only be necessary for internal IDs (not sure why you need to translate them though).
The following code should work for you, keeping the mappings in Associative Arrays:
DECLARE
TYPE t_number_mapping IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
mapping_field_c t_number_mapping;
BEGIN
-- Prepare mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
mapping_field_c(cur.field_c) := mapping_field_c.COUNT; -- first entry mapped to 1
END LOOP;
-- Use mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
-- You can use the mapping when generating the `INSERT` statement
dbms_output.put_line( cur.field_c || ' mapped to ' || mapping_field_c(cur.field_c) );
END LOOP;
END;
Output:
101 mapped to 1
102 mapped to 2
If this isn't a permanent piece of production code, how about "borrowing" an existing collection type - e.g. one define in SYS that you can access.
Using this script from your schema you can generate a SQL Plus script to describe all SYS-owned types:
select 'desc ' || type_name from all_types
where typecode = 'COLLECTION'
and owner = 'SYS';
Running the resulting script will show you the structure of all the ones you can access. This one looks potentially suitable for example:
SQL> desc KU$_PARAMVALUES1010
KU$_PARAMVALUES1010 TABLE OF SYS.KU$_PARAMVALUE1010
Name Null? Type
----------------------------------------- -------- ----------------------------
PARAM_NAME VARCHAR2(30)
PARAM_OP VARCHAR2(30)
PARAM_TYPE VARCHAR2(30)
PARAM_LENGTH NUMBER
PARAM_VALUE_N NUMBER
PARAM_VALUE_T VARCHAR2(4000)
Of course, you can't guarantee that type will still exist or be the same or be accessible to you after a database upgrade, hence my caveat at the start.
More generic way to achieve this goal.
In my example i'm using xquery flwor expressions and dbms_xmlstore. Knowledge about xquery is mandatory.
create table mask_user_objects as select * from user_objects where rownum <0;
declare
v_s_table varchar2(30) := 'USER_OBJECTS'; --uppercase!!!
v_d_table varchar2(30) := 'MASK_USER_OBJECTS'; --uppercase!!!
v_mask_columns xmltype := xmltype('<COLS><OBJECT_NAME>XXXX</OBJECT_NAME>
<DATA_OBJECT_ID>-1</DATA_OBJECT_ID>
<OBJECT_TYPE/>
</COLS>'); --uppercase!!!
insCtx DBMS_XMLSTORE.ctxType;
r NUMBER;
v_source_table xmltype;
v_cursor sys_refcursor;
begin
open v_cursor for 'select * from '||v_s_table||' where rownum <100 ';
v_source_table := xmltype(v_cursor);
close v_cursor;
-- Load source table into xmltype.
insCtx := DBMS_XMLSTORE.newContext(v_d_table); -- Get saved context
for rec in (
select tt.column_value from xmltable('
let $col := $anomyze/COLS
for $i in $doc/ROWSET/ROW
let $row := $i
return <ROWSET>
<ROW>
{
for $x in $row/*
return if(
exists($col/*[name() = $x/name()] )
) then element{$x/name()}{ $col/*[name() = $x/name()]/text() }
else element{$x/name()}{$x/text()}
}
</ROW>
</ROWSET>
'
passing v_source_table as "doc"
, v_mask_columns as "anomyze"
) tt) loop
null;
r := DBMS_XMLSTORE.insertXML(insCtx, rec.column_value);
end loop;
DBMS_XMLSTORE.closeContext(insCtx);
end;

ROWID in casted pl-sql collection

Is there any rowid like facility in a pl-sql collection? In my case, while I am using this collection in an sql query, I also need the sequence number as they are put in. I know modification is data struecture is a way, but I want to use the index of the collection. so what I am looking for is something like this:
TYPE t_List IS TABLE OF VARCHAR2(200);
and
declare
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
......
FOR Rec IN (SELECT Column_Value v
,ROWID r
FROM TABLE(CAST(v_data t_list)))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
The loop is not expected to be executed in sequence, but I want something built-in like ROWID that is like the index of the collection.
Only schema-level types can be used in SQl statement, even within a PL/SQL block. As you seem to suggest you already know, you can create your own object type that includes the 'sequence' ID:
CREATE TYPE t_object AS OBJECT (
id NUMBER,
data VARCHAR2(200)
)
/
And a collection of that type:
CREATE TYPE t_List IS TABLE OF t_object;
/
And then populate the ID as you build the list:
DECLARE
l_List t_List := t_List(t_object(1, 'data 1')
,t_object(2, 'data_2')
,t_object(3, 'data3'));
BEGIN
FOR Rec IN (SELECT id, data
FROM TABLE(l_list))
LOOP
Dbms_Output.Put_Line('at ' || Rec.id || ':' || Rec.data);
-- .... and other codes here
END LOOP;
END;
/
Without an object type you can use the ROWNUM pseudocolumn:
CREATE TYPE t_List IS TABLE OF VARCHAR2(200);
/
DECLARE
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
BEGIN
FOR Rec IN (SELECT Column_Value v
,ROWNUM r
FROM TABLE(v_data))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
END;
/
anonymous block completed
at 1:data 1
at 2:data_2
at 3:data3
As far as I'm aware that isn't guaranteed to preserve the original creation sequence. I think it almost certainly will at the moment, but perhaps isn't something you should rely on as always being true. (There is no order without an order by, but here you don't have anything you can order by without destroying your initial order...).
If you query a subset of the table - I'm not sure what you mean by "the loop is not expected to be executed in sequence" - you'd need to generate the ROWNUM in a subquery before filtering or it won't be consistent. You'd also need to generate the ROWNUM in a subquery if you're joining this to other, real, tables - I imagine you are, otherwise you could use a PL/SQL collection.
If indexing is important, use Associative Arrays(Also known as Index-by tables)
Refer his.

Oracle LISTAGG() for querying use

So I'm trying to make use of the LISTAGG() function to simply build a comma delimited list to use within an underlying query. The list generation works fine and I just applied an output for debug purposes where I can see my list as it should be:
VALUES:
'AB','AZ','BC','CA','CT','DC','FL','FO','GA','IL','KS','MA','MB','ME','MN','MS','MT','NB','NC','NL','NOVA
SCOTIA','NS','NT','NU','NY','ON','ONTARIO','OR','PE','QC','QUEBEC','QUÉBEC','SASKATCHEWAN','SK','TX','VT','WA','YT'
When I try to pass this list variable to my query however just to see if anything will come back, nothing comes back, but if I copy / past the provinces / states list from above (as is) instead of using "v_Province" in my where clause, I get a result back. What am I doing wrong?
DECLARE
v_PROVINCE varchar2(500);
v_results varchar2(1000);
BEGIn
dbms_output.enable(1000000);
Select '''' || LISTAGG(STATE, ''',''') WITHIN GROUP (ORDER BY STATE) || '''' PROV
INTO v_PROVINCE
from (Select distinct STATE from ADDRDATA where STATE IS NOT NULL);
DBMS_OUTPUT.PUT_LINE('VALUES: ' || v_PROVINCE);
Select CITY
INTO v_results
from VWPERSONPRIMARYADDRESS
where state in (v_Province)
AND ROWNUM <= 1;
DBMS_OUTPUT.PUT_LINE(v_results);
END;
/
Firstly, it is almost always more efficient to do everything in a single statement if at all possible.
Your second query doesn't work as you are returning everything into a single string. This is not a comma delimited list as required by an IN statement.
There is a little trick to get round this though. Assuming you are using the string for something between the two SELECT statements you can play around with regexp_substr() to turn your string into something usable.
Something like this would work;
select city
from vwpersonprimaryaddress
where state in (
select regexp_substr(v_province,'[^'',]+', 1, level)
from dual
connect by regexp_substr(v_province, '[^'',]+', 1, level) is not null
)
The variable v_province would have to be changed to be quoted twice, for instance '''AB'',''AZ'',''BC''' in order for this to work.
Here's a working example
What you're trying to do won't work, because the IN operator treats the comma-separated list as a single value. In theory, you could gather the values into a single string, then parse the string into individual values so your next query could interpret it. However, that would be a really bad idea.
A better idea would be to use an array to pass your list of values from the first query to the second:
create type nt_varchar_50 as table of varchar2(10)
/
DECLARE
v_PROVINCE nt_varchar_50;
v_results varchar2(1000);
cursor cur_provinces is
Select distinct STATE from ADDRDATA where STATE IS NOT NULL;
i pls_integer;
BEGIN
dbms_output.enable(1000000);
open cur_provinces;
fetch cur_provinces bulk collect into v_PROVINCE;
close cur_provinces;
DBMS_OUTPUT.PUT('VALUES: ');
for i in v_PROVINCE.first .. v_province.last loop
if i <> 1 then
DBMS_OUTPUT.PUT(', ');
end if;
DBMS_OUTPUT.PUT(v_PROVINCE(i));
end loop;
DBMS_OUTPUT.PUT_LINE();
Select CITY
INTO v_results
from VWPERSONPRIMARYADDRESS
where state in (select * from table(v_Province))
AND ROWNUM <= 1;
DBMS_OUTPUT.PUT_LINE(v_results);
END;
/
Of course, even this is vastly less efficient than use a single SQL statement in the first place. Really, you should only use this kind of technique if you need to do some sort of processing between the two queries that does not lend itself to SQL or, possibly, if you need to use the first result set multiple times.

Oracle PL\SQL Null Input Parameter WHERE condition

As of now I am using IF ELSE to handle this condition
IF INPUT_PARAM IS NOT NULL
SELECT ... FROM SOMETABLE WHERE COLUMN = INPUT_PARAM
ELSE
SELECT ... FROM SOMETABLE
Is there any better way to do this in a single query without IF ELSE loops. As the query gets complex there will be more input parameters like this and the amount of IF ELSE required would be too much.
One method would be to use a variant of
WHERE column = nvl(var, column)
There are two pitfalls here however:
if the column is nullable, this clause will filter null values whereas in your question you would not filter the null values in the second case. You could modify this clause to take nulls into account but it turns ugly:
WHERE nvl(column, impossible_value) = nvl(var, impossible_value)
Of course if somehow the impossible_value is ever inserted you will run into some other kind of (fun) problems.
The optimizer doesn't understand correctly this type of clause. It will sometimes produce a plan with a UNION ALL but if there are more than a couple of nvl, you will get full scan even if perfectly valid indexes are present.
This is why when there are lots of parameters (several search fields in a big form for example), I like to use dynamic SQL:
DECLARE
l_query VARCHAR2(32767) := 'SELECT ... JOIN ... WHERE 1 = 1';
BEGIN
IF param1 IS NOT NULL THEN
l_query := l_query || ' AND column1 = :p1';
ELSE
l_query := l_query || ' AND :p1 IS NULL';
END IF;
/* repeat for each parameter */
...
/* open the cursor dynamically */
OPEN your_ref_cursor FOR l_query USING param1 /*,param2...*/;
END;
You can also use EXECUTE IMMEDIATE l_query INTO l_result USING param1;
This should work
SELECT ... FROM SOMETABLE WHERE COLUMN = NVL( INPUT_PARAM, COLUMN )

Resources