Re-using bind variables in Oracle PL/SQL - oracle

I have a hefty SQL statement with unions where code keeps getting re-used. I was hoping to find out if there is a way to re-use a single bind variable without repeating the variable to for "USING" multiple times.
The code below returns "not all variables bound" until I change the "USING" line to "USING VAR1,VAR2,VAR1;"
I was hoping to avoid that as I'm referring to :1 in both instances - any ideas?
declare
var1 number :=1;
var2 number :=2;
begin
execute immediate '
select * from user_objects
where
rownum = :1
OR rownum = :2
OR rownum = :1 '
using var1,var2;
end;
/
EDIT: For additional info, I am using dynamic SQL as I also generate a bundle of where conditions.
I'm not great with SQL arrays (I am using a cursor in my code but I think that will overcomplicate the issue) but the pseudocode is:
v_where varchar2(100) :='';
FOR i in ('CAT','HAT','MAT') LOOP
v_where := v_where || ' OR OBJECT_NAME LIKE ''%' || i.string ||'%''
END;
v_where := ltrim(v_where, ' OR');
And then modifying the SQL above to something like :
execute immediate '
select * from user_objects
where
rownum = :1
OR rownum = :2
OR rownum = :1 AND ('||V_WHERE||')'
using var1,var2;

There are some options you might consider, although they may require changes, either to how you execute your SQL statement or to your SQL statement itself.
Use DBMS_SQL instead of EXECUTE IMMEDIATE -- DBMS_SQL (see http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_sql.htm) is harder to use than EXECUTE IMMEDIATE, but gives you more control over the process -- including the ability (through DBMS_SQL.BIND_VARIABLE and DBMS_SQL.BIND_ARRAY) to bind by name instead of by position.
Use EXECUTE IMMEDIATE with a WITH clause -- You might be able restructure your query to use WITH clause that gathers your bind variables in subquery at the beginning, and then joins to the subquery (instead of referencing the bind variables directly) whenever it needs them. It might look something like this
with your_parameters as
(select :1 as p1, :2 as p2 from dual)
select *
from your_table, your_parameters
where your_table.some_column1 = your_parameters.p1
and your_table.some_column2 <= your_parameters.p1
and your_table.some_column3 = your_parameters.p2
This could affect the performance of your query, but it might be an acceptable compromise.
Don't use dynamic SQL -- Of course, if you don't need dynamic SQL, you don't need to use EXECUTE IMMEDIATE, so the "bind only by position" limitiation does not apply. Are you sure you really need to use dynamic SQL?
EDIT: If you're using dynamic SQL because you have a variable number of OR conditions like you posted in your edit, you might be able to avoid using dynamic SQL by doing one of the following:
If the OR criteria come from a table (or query) -- Join to that table (or query) instead of using a list of OR criteria. For example, if CAT, HAT, and MAT are listed in a column named YOUR_CRITERIA in a table named YOUR_CRITERIA_TABLE you might add YOUR_CRITERIA_TABLE to the FROM clause and replace the OBJECT_NAME LIKE '%CAT% OR OBJECT_NAME LIKE '%MAT% OR OBJECT_NAME LIKE '%HAT% OR OBJECT_NAME LIKE '%MAT% in the WHERE clause with something like OBJECT_NAME LIKE '%' || YOUR_CRITERIA_TABLE.YOUR_CRITERIA || '%'.
Otherwise, you might put the criteria in a global temporary table -- If your criteria don't come from a table (or query), you could (once, at design time, not at run time) create a global temporary table to hold them, and then at run time, insert the criteria into the global temporary table and then join to it as described in item 1.
Or, you might put the criteria in an nested table -- This is like item 2, except uses a nested table (one created using CREATE TYPE...IS TABLE OF) instead of a global temporary table. You could create or own nested table type, or use a built-in one like SYS.ODCIVARCHAR2LIST. In PL/SQL, you would populate an variable of this type, and then use it like a "real" table like in item 1.
An example of item 3 might look something like:
DECLARE
tblCriteria SYS.ODCIVARCHAR2LIST;
BEGIN
tblCriteria := SYS.ODCIVARCHAR2LIST();
-- In "real" code you might populate the nested table in a loop.
-- This example populates it explicitly so that it will compile. For the
-- purpose of the example, we could have populated the nested table in
-- a single statement:
-- tblCriteria := SYS.ODCIVARCHAR2LIST('CAT', 'HAT', 'MAT');
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'CAT';
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'HAT';
tblCriteria.EXTEND(1);
tblCriteria(tblCriteria.LAST) := 'MAT';
FOR rec IN
(
SELECT
USER_OBJECTS.*
FROM
USER_OBJECTS,
TABLE(tblCriteria) YOUR_NESTED_TABLE
WHERE
USER_OBJECTS.OBJECT_NAME LIKE '%' || YOUR_NESTED_TABLE.COLUMN_VALUE || '%'
)
LOOP
-- Do something. For example, print out the object name.
DBMS_OUTPUT.PUT_LINE(rec.OBJECT_NAME);
END LOOP;
END;

No, unfortunately, the bind variables for EXECUTE IMMEDIATE must be provided in the same order they appear in the statement, and the bind variable names are ignored. So you'll just have to have :1, :2 and :3 in your statement.

Related

Using variables within the WHERE clause in Oracle SQL Developer

I'm new to Oracle SQL Developer. I have a VFP background and I'm having a difficult time trying to use variables as I have done quite a few other applications. Define the variable, determine what the variable is (programmatically), then use the variable. I'm good with the first 2 but I cannot figure out how to use the variable. Create a procedure, write the select statement to a var string, output to a cursor? I've seen many examples but nothing I can use or I'm just not understanding the different concept. Here is the code I've started with.
declare
v_mydate DATE;
begin
select max(date_added) into v_mydate from TEST_TB;
DBMS_OUTPUT.put_line(TO_CHAR(v_mydate)); -- checking to see if the variable has been assigned. It has
create table test_banks as
SELECT DISTINCT
FIELD1,
FIELD2,
, DATE_APPEND
FROM TEST_TB
WHERE DATE_ADDED=V_MYDATE
end;
Field1 and Field2 will have many distinct combinations. I want them all that have the max(date_added) value.
DDL is not allowed here. DDL is the data definition language. CREATE TABLE is a DDL statement.
Try execute immediate, which is really inefficient.
declare
v_mydate DATE;
begin
select max(date_added) into v_mydate from TEST_TB;
DBMS_OUTPUT.put_line(TO_CHAR(v_mydate)); -- checking to see if the variable has been assigned. It has
EXECUTE IMMEDIATE
'create table test_banks as
SELECT DISTINCT
FIELD1,
FIELD2,
, DATE_APPEND
FROM TEST_TB
WHERE DATE_ADDED in (select max(date_added) from TEST_TB)';
end;
You need not use PLSQL to do this task,You can just create a view like the below
CREATE OR REPLACE VIEW VW_TEST_BANKS
AS SELECT DISTINCT field1, field2, date_append
FROM TEST_TB where date_added=(SELECT TRUNC(MAX(date_added)) FROM TEST_TB)
or if you want to test the use of variables in WHERE clause you can try the below
declare
v_mydate DATE;
begin
select max(date_added) into v_mydate from TEST_TB;
DBMS_OUTPUT.put_line(TO_CHAR(v_mydate)); -- checking to see if the variable has been assigned. It has
FOR REC in (SELECT DISTINCT FIELD1, FIELD2,, DATE_APPEND
FROM TEST_TB
WHERE DATE_ADDED = V_MYDATE) LOOP
DBMS_OUTPUT.put_line('field1 ' || rec.field1);
DBMS_OUTPUT.put_line('field2 ' || rec.field2);
DBMS_OUTPUT.put_line('date_append ' || rec.date_append);
END LOOP;
end;

PL/SQL - How to use an array in an IN Clause

I'm trying to use an array of input values to my procedure in an IN Clause as part of the where clause of a cursor. I know that this has been asked before, but I haven't seen how to make my syntax compile correctly.
In the package specification, the type is
TYPE t_brth_dt IS TABLE OF sourceTable.stdt_brth_dt%TYPE INDEX BY PLS_INTEGER;
sourceTable.std_brth_dt is a date column in the table.
Simplified version of my cursor is in the package body is -
cursor DataCursor_Sort( p_brth_dt in t_brth_dt) is
SELECT *
FROM sourceTable
WHERE a.brth_dt IN (select column_value
from table(p_brth_dt))
When I try to compile this, I'm getting the following errors.
[1]:(Error): PLS-00382: expression is of wrong type
[2]:(Error): PL/SQL: ORA-22905: cannot access rows from a non-nested table item
I know this looks similar to other questions, but I don't understand what the syntax error is.
In order to use collection defined as a nested table or an associative array in the from clause of a query you either should, as #Alex Poole correctly pointed out, create a schema level (SQL) type or use one, that is available to you trough ODCIConst package - odcidatelist as you intend to use a list of dates. For example, your cursor definition might look like this:
cursor DataCursor_Sort(p_brth_dt in sys.odcidatelist) is
select *
from sourceTable
where a.brth_dt IN (select column_value
from table(p_brth_dt))
OR
cursor DataCursor_Sort(p_brth_dt in sys.odcidatelist) is
select s.*
from sourceTable s
join table(p_brth_dt) t
on (s.brth_dt = t.column_value)
Note: You should take into consideration the time part of a date when performing a date comparison. If you want to compare date part only it probably would be useful to get rid of time part by using trunc() function.
It is possible to use a PL/SQL-defined nested table type (as opposed to a SQL-defined nested table type) indirectly in an IN clause of a SELECT statement in a PL/SQL package. You must use a PIPELINED function as an intermediary. It felt kind of clever to write, but I don't believe in its fundamental usefulness.
CREATE OR REPLACE PACKAGE so18989249 IS
TYPE date_plsql_nested_table_type IS TABLE OF DATE;
dates date_plsql_nested_table_type;
FUNCTION dates_pipelined RETURN date_plsql_nested_table_type PIPELINED;
PROCEDURE use_plsql_nested_table_type;
END so18989249;
/
CREATE OR REPLACE PACKAGE BODY so18989249 IS
FUNCTION dates_pipelined RETURN date_plsql_nested_table_type
PIPELINED IS
BEGIN
IF (dates.count > 0)
THEN
FOR i IN dates.first .. dates.last
LOOP
IF (dates.exists(i))
THEN
PIPE ROW(dates(i));
END IF;
END LOOP;
END IF;
END;
PROCEDURE use_plsql_nested_table_type IS
BEGIN
dates := NEW date_plsql_nested_table_type();
-- tweak these values as you see fit to produce the dbms_output results you want
dates.extend(5);
dates(1) := DATE '2013-12-25';
dates(2) := DATE '2013-01-01';
dates(3) := DATE '2013-07-01';
dates(4) := DATE '2013-09-03';
dates(5) := DATE '2008-11-18';
FOR i IN (SELECT o.owner,
o.object_name,
o.object_type,
to_char(o.last_ddl_time, 'YYYY-MM-DD') AS last_ddl
FROM all_objects o
WHERE trunc(o.last_ddl_time) IN
(SELECT column_value FROM TABLE(dates_pipelined))
--uses pipeline function which uses pl/sql-defined nested table
)
LOOP
dbms_output.put_line('"' || i.owner || '"."' || i.object_name || '" ("' || i.object_type || ') on ' || i.last_ddl);
END LOOP;
END;
END so18989249;
/
begin so18989249.use_plsql_nested_table_type; end;
/
The type has to be created at SQL level, not in a package. An SQL query doesn't know how to use any types defined in PL/SQL. So you'd have to do:
CREATE OR REPLACE TYPE t_brth_dt IS TABLE OF date;
/
... and remove the type from your package specification. (Or give them different names, at least, and they won't be interchangeable in use). Because it's at SQL level, you also can't use sourceTable.stdt_brth_dt%TYPE in the declaration, unfortunately.

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.

Execute immediate inside FORALL statement

I have a procedure in which 100 tables have to be updated one by one. All tables have the same column to be updated. For improving the performance I am trying to use Execute Immediate with FORALL but I am getting a lot of compilation errors.
Is it syntactically possible to update 100 different tables inside a FORALL statement using Execute immediate.
My code looks something like this.
Declare
TYPE u IS TABLE OF VARCHAR2(240) INDEX BY BINARY_INTEGER;
Table_List u;
FOR somecursor IN (SELECT variable1, variable2 FROM SomeTable)
LOOP
BEGIN
Table_List(1) := 'table1';
Table_List(2) := 'table2';
......
......
table_list(100):= 'table100';
FORALL i IN Table_List.FIRST .. Table_List.LAST
EXECUTE IMMEDIATE 'UPDATE :1 SET column = :3 WHERE column = :2'
USING Table_List(i), somecursor.variable1, somecursor.variable2 ;
end loop;
I hope people can understand what I am trying to do through this code. If something is big time wrong please suggest me what exactly is the syntax and if it can be done in some other efficient way also.
Thanks a lot for all the help which comes my way.
(1) No, you can't use a bind variable for the table name.
(2) When you're using EXECUTE IMMEDIATE, this implies Dynamic SQL - but FORALL requires that only one statement to be executed. As soon as you specify a different table, you're talking about a different statement (regardless of whether the tables' structures happen to be equivalent or not).
You're going to have to do this in an ordinary FOR loop.
Just a guess, but I don't think you can use a bind variable as a table name. Have you tried:
EXECUTE IMMEDIATE 'UPDATE ' || Table_List(i) || ' SET column = :2 WHERE column = :3' ...

Oracle 8i dynamic SQL error on subselects in pl/sql blocks

I wrote an Oracle function (for 8i) to fetch rows affected by a DML statement, emulating the behavior of RETURNING * from PostgreSQL. A typical function call looks like:
SELECT tablename_dml('UPDATE tablename SET foo = ''bar''') FROM dual;
The function is created automatically for each table and uses Dynamic SQL to execute a query passed as an argument. Moreover, a statement that executes the query dynamically is also wrapped in a BEGIN .. END block:
EXECUTE IMMEDIATE 'BEGIN'||query||' RETURNING col1, col2 BULK COLLECT INTO :1, :2;END;' USING OUT col1_t, col2_t;
The reason behind this perculiar construction is that it seems to be the only way to get values from the DML statement that affects multiple rows. Both col1_t and col2_t are declared as collections of the types corresponding to the table columns.
Finally, to the problem. When the query passed contains a subselect, execution of the function produces a syntax error. Below is a simpe example to illustrate this:
CREATE TABLE xy(id number, name varchar2(80));
CREATE OR REPLACE FUNCTION xy_fn(query VARCHAR2) RETURN NUMBER IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE 'BEGIN '||query||'; END;';
ROLLBACK;
RETURN 5;
END;
SELECT xy_fn('update xy set id = id + (SELECT min(id) FROM xy)') FROM DUAL;
The last statement produces the following error: (the SELECT that is mentioned there is the SELECT min(id))
ORA-06550: line 1, column 32: PLS-00103: Encountered the symbol
"SELECT" when expecting one of the following: ( - + mod not null
others avg count current exists max min prior sql stddev sum
variance execute forall time timestamp interval date
This problem occurs on 8i (8.1.6), but not 10g.
If BEGIN .. END blocks are removed - the problem disappears.
If a subselect in a query is replaced with something else, i.e. a constant, the problem disappears.
Unfortunately, I'm stuck with 8i and removing BEGIN .. END is not an option (see the explanation above).
Is there a specific Oracle 8i limitation in play here? Is it possible to overcome it with dynamic SQL?
Not sure why you need to do all this work. Oracle 8i supported RETURNING INTO with bulk collection. Find out more
So you should just be able to execute this statement in non-dynamic SQL. Something like this:
UPDATE tablename
SET foo = 'bar'
returning col1, col2 bulk collect into col1_t, col2_t;
Stripped of all the irrelevancies, I think your question is simple.
This update statement runs in SQL:
update xy set id = id + (SELECT min(id) FROM xy);
And this anonymous block also runs:
begin
update xy set id = id + 100;
end;
But combining the two doesn't work:
begin
update xy set id = id + (SELECT min(id) FROM xy);
end;
Probably you have run into a limitation of older Oracle. Prior to 9i, the SQL engine and the PL/SQL SQL engine were always out of sync. So latest features supported in SQL often weren't supported in PL/SQL. It seems like you have one of those.
Since 9i Oracle have striven to keep the two engines in sync, so it is much rarer to find things which work in SQL but not in PL/SQL.
Given the nature of your task, upgrading your version of Oracle is out. So all I can suggest is that you have two procedures, one which supports the sub query syntax (by avoiding the need for such subqueries. Something like this:
CREATE OR REPLACE FUNCTION xy_sqfn
(main_query VARCHAR2
, sub_query VARCHAR2 )
RETURN NUMBER
IS
n pls_integer;
BEGIN
execute immediate sub_query into n;
EXECUTE IMMEDIATE 'BEGIN '||main_query||'; END;'
using n;
RETURN 5;
END;
call it like this
result := xy_sqfn ('update xy set id = id + :1'
, 'SELECT min(id) FROM xy');
Now this approach won't work for correlated sub-queries. So it you have any of them, you'll need to do something different again.
Incidentally, using the AUTONOMOUS TRANSACTION pragma to fudge executing DML in a SELECT statement is quite horrible. Why not just run the functions in PL/SQL? Or use procedures? I suppose you'll say it doesn't matter because you're just writing some shonky code to support a data migration. Which is fair enough, but for the benefit of future seekers: don't do this! It's very bad practice!

Resources