How to Declare, Populate and Use a Variable using Oracle PL/SQL - oracle

I swear this has been asked so many times previously, yet I cannot seem to apply other examples to my use case:
First things first, this query will be executed as part of an Informatica SQL Source Qualifier, and in some circumstances, be passed-through from an SQL Server OpenQuery statement, so please be mindful of this, and
that SQL Plus will not be used be used; Oracle SQL Developer is used only for code development.
My history is primarily SQL Server & Teradata, but as the title suggests, I now have a requirement where I need to declare, populate and use a variable in Oracle, all within the same procedure. Not SP, so no In/Out declarations
In SQL Server, this code will work as expected (line numbers added for clarity):
1. Declare #MaxDate Int
2.
3. With f_data (cal_period) As (Select 201904 As cal_period)
4.
5. Select #MaxDate = Max(cal_period) From f_data
6.
7. Select
8. Case
9. When (#MaxDate%100) < 12 Then #MaxDate+1
10. Else (#MaxDate+100) - ((#MaxDate%100)-1)
11. End As dt
Line 1: These are YYYYMM date periods defined as int
Line 3: I am using an inline view (CTE) here for illustration, and to
make it easier for you to copy and paste, but in reality, this is
actually a physical control table, so would not normally be visible in the
script.
Line 5: Populates the parameter (SQL Server prefixes parameters with
the at symbol) with the single-value resultset
Line 7-11: Is simply the logic to progress the period by one, the
percentage-mark in SQL Server is the Modulus function, Oracle is
written as Mod(#MaxDate,100)
For those unfamiliar with SQL Server, it does not need a reference table such as Dual ("Sys.Dual") in order to execute the query, such that for Oracle a "From Dual" statement is necessary on the missing Line 12
My requirement is essentially a carbon-copy of the above T-SQL, so I need to declare a one-time use variable, to populate that variable with the results of an SQL query, and then to use this variable in a transformation - the result of which is captured to an Informatica and SSIS variable for later use.
So far, I have tried declaring a variable, this seemed to work (by which I mean it didn't return an error):
Declare MaxPeriod Int;
Begin
Select 201904 Into MaxPeriod From Dual;
End;
And populating from an SQL statement is also showing as successfully completed:
Declare MaxPeriod Int;
Begin
Select Max(MaxPeriodVal) Into MaxPeriod From CtrlTable;
End;
Although I can't seem to get beyond this to actually test the variable as put.line statements fail, as do simple Case checks:
Declare MaxPeriod Int;
Begin
Select 201904 Into MaxPeriod From Dual;
End;
Select
Case
When 201904 = MaxPeriod Then 'Match'
Else 'No Match'
End As dteChk
From Dual;
I have attempted to prefix the MaxPeriod in the check with a colon, and, to have prefixed,suffixed/both with an ampersand eg :MaxPeriod; &MaxPeriod; MaxPeriod&; &MaxPeriod&
All of which failed.

The basic issue is a variable scope problem. You're declaring MaxPeriod within the context of a PL/SQL anonymous block, so it will disappear (fall out of scope) when the block ends on line 4.
You could put your entire query inside the PL/SQL block, but there's not an easy way to return an entire result set from a PL/SQL block, so I don't think you want that.
I don't know how your Oracle driver handles native queries, but this might work:
var MaxPeriod number; -- bind variable declared as global scope for this script
Begin -- one of several ways to assign values to bind variables
:MaxPeriod := 201904;
End;
/
Select
Case
When 201904 = :MaxPeriod Then 'Match'
Else 'No Match'
End As dteChk
From Dual;
If the var syntax doesn't work for you to declare a SQL bind variable, then you may have to look into some other way of passing a bind variable for the query string. You could probably pass a null value (for a number datatype, anyway) and then overwrite it in the SQL script.
Alternately, in your original example code, I think I'd use a CTE or an inline view instead of a variable anyway.
With f_data As (Select 201904 As cal_period from dual)
Select
Case
When Mod(MaxDate,100) < 12 Then MaxDate+1
Else (MaxDate+100) - (Mod(MaxDate,100)-1)
End As dt
from (Select Max(cal_period) as MaxDate From f_data) mp

You can use substitution variable using define in sql*plus as following.
Define MaxPeriod := 201904
Select
Case
When &MaxPeriod = MaxPeriod Then 'Match'
Else 'No Match'
End As dteChk
From Dual;
Cheers!!

Related

ORA-00900: invalid SQL statement for Oracle Procedure

I am trying to execute my below procedure but kept getting error (ORA-00900: invalid SQL statement)
CREATE OR REPLACE PROCEDURE RESETUSERSESSION (run IN VARCHAR2)
IS
cursor usersessiondetail_cur IS
SELECT usd.CLIENTID,usd.OPERID,usd.REGISTER,usd.MACHINE_ID,usd.SESSIONNUMBER
FROM cashiering_dev.CSH_USER usr, cashiering_dev.CSH_USERSESSIONDETAIL usd
WHERE usr.clientid = usd.clientid
AND usr.operid = usd.operid
AND usr.register = usd.register
AND usr.machine_id = usd.machine_id
AND usr.sessionnumber = usd.sessionnumber
AND usr.Machine_ID = 'basrytest'
AND usd.LOGOFFDATETIME IS NULL;
BEGIN
OPEN usersessiondetail_cur;
FOR vItems in usersessiondetail_cur
LOOP
EXECUTE IMMEDIATE 'UPDATE csh_UserSessionDetail
SET ClientID =vItems.CLIENTID
WHERE ClientID =vItems.CLIENTID
AND OperID =vItems.OPERID
AND Register =vItems.REGISTER
AND Machine_ID =vItems.MACHINE_ID
AND SessionNumber =vItems.SESSIONNUMBER';
END LOOP;
CLOSE usersessiondetail_cur;
END;
Your SQL is invalid because the cursor projection names are not in scope when the dynamic SQL string is executed. You need to use placeholders like this:
FOR vItems in usersessiondetail_cur
LOOP
EXECUTE IMMEDIATE 'UPDATE csh_UserSessionDetail
SET ClientID = :p1
WHERE ClientID = :p2
AND OperID = :p3
AND Register = :p4
AND Machine_ID = :p5
AND SessionNumber = :p6'
using vItems.CLIENTID
, vItems.CLIENTID
, vItems.OPERID
, Items.REGISTER
, vItems.MACHINE_ID
, vItems.SESSIONNUMBER;
END LOOP;
Your dynamic code is not an anonymous PL/SQL block or a CALL statement so parameters are passed by position not name, which means you must pass vItems.CLIENTID twice. Find out more.
Other observations
First and foremost, there is absolutely no need to implement dynamic execution for this SQL.
The OPEN and CLOSE cursor statements are not used with a FOR cursor loop.
You do not need an explicit cursor declaration for this query.
The row-by-agonising row UPDATE with a cursor loop is bad practice and needlessly inefficient compared to set-based UPDATE statement.
Your procedure doesn't use the run parameter ...
... but the cursor does have a hardcoded string for MACHINE_ID.
Lastly, the UPDATE statement doesn't actually change the state of the table, because it sets CLIENT_ID = CLIENT_ID, so the whole procedure is pointless.
Apart from that, everything is fine.
I assume you're writing this as a test for understanding how to use dynamic SQL rather than as an implementation of business logic. But even if it is a test it is better to write a proper piece of code which does something. Especially when you're sharing the code with others on StackOverflow. Posting code with so many issues is distracting because potential respondents don't know which to tackle.
A much simpler approach with just FOR loop. We dont require to Open Close cursor in this case as this is internally handled by Oracle. Also I do not understand the need of updating Client ID again. If we are selecting the Client ID in where Clause there is no significance of Updating. Anyhow Enjoy :)
CREATE OR REPLACE
PROCEDURE RESETUSERSESSION(
run IN VARCHAR2)
AS
BEGIN
FOR vItems IN
(SELECT usd.CLIENTID,
usd.OPERID,
usd.REGISTER,
usd.MACHINE_ID,
usd.SESSIONNUMBER
FROM cashiering_dev.CSH_USER usr,
cashiering_dev.CSH_USERSESSIONDETAIL usd
WHERE usr.clientid = usd.clientid
AND usr.operid = usd.operid
AND usr.register = usd.register
AND usr.machine_id = usd.machine_id
AND usr.sessionnumber = usd.sessionnumber
AND usr.machine_id = 'basrytest'
AND usd.LOGOFFDATETIME IS NULL
)
LOOP
UPDATE csh_UserSessionDetail
SET ClientID =vItems.CLIENTID
WHERE ClientID =vItems.CLIENTID
AND OperID =vItems.OPERID
AND Register =vItems.REGISTER
AND Machine_ID =vItems.MACHINE_ID
AND SessionNumber =vItems.SESSIONNUMBER;
END LOOP;
END;
/

Error when a named parameter used multiple times in JDBC PL/SQL block

I get an error when use named parameter to call PL/SQL block, when all named parameters are used only once, then my code works fine, but when I dupplicate the SQL marked with "// the SQL". then all named parameters (starts with colon, :q) are used twice, now I get a SQL Exception, it says: The number of parameter names does not match the number of registered praremeters.
It seems that JDBC driver or DB think there is 2 parameters, but only 1 parameters are registered? why we cannot use a named parameter multiple times? is JDBC driver don't required to support this case?
How I get an alternative (exeption rewriting PL/SQL block to stored procedure)?
My Oracle JDBC Driver is latest version 11.2.0.3.0.
Because my project have many PL/SQL block, I try my best to avoid rewrite SQL to a stored procedure, running raw PL/SQL block with named parameter (just treat it as a procedure) is preferred. I tested convert the PL/SQL block to a stored procedure, then only 1 parameters exported, but I don't want to rewrite all PL/SQL blocks, it takes more efforts.
thanks for any hints.
My Java code:
CallableStatement stmt = ...;
stmt.registerOutParameter("q", Types.VARCHAR);
stmt.execute();
String v1 = stmt.getString("q");
SQL as below:
BEGIN
select DUMMY into :q from dual where dummy = 'X';
select DUMMY into :q from dual where dummy = 'X';
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
I just found that "Execute Immediate" can be used for my case, when the dynamic SQL is a PL/SQL block (quoted with begin and end), then the named parameters with a name can be referenced by all occurrences. i.e. SQL attached below, in this block, parameter 'q' is used only once,
but now I have another 2 questions,
Q1: I don't know if the parameter 'q' is IN, OUT or both IN and OUT. If I give wrong IN/OUT mode, error got, how we test if the parameter is IN/OUT or both of them? I want to scan the SQL for ':q :=' and 'into :q', it seems that it is not good method.
Q2: Why I can't fetch result of parameter 'q' when it is assigned IN OUT mode? only if it is OUT, I can get its value. when it is both IN OUT, I get NULL.
begin
execute immediate 'begin select dummy into :q from dual where :q is not null; end;'
using in out :q;
end;
Oh, I get a workaround for NULL when parameter is IN OUT mode, I just treat it is a bug of Oracle JDBC driver, I split IN/OUT role of the named parameter 'q' into 2 parts, first is IN, second is OUT, using a variable to keep its value returned by 'using in out :q' clause, and then assign variable to 2nd role, like below-attached, in JDBC we treat it both IN OUT, only use exact IN,OUT or IN OUT in USING clause after scanning ' :q := ' and ' into :q '.
declare
p varchar2(100);
q varchar2(100);
begin
p := ?;
q := ?;
execute immediate 'begin if :p is null then :p := ''X''; else :p := ''Y''; :q := ''Z''; end if; end;' using in out p, out q;
? := p;
? := q;
end;
You can't use one bind parameter multiple times in SQL statement. You must provide a value for each occurrence of parameter. This is because Oracle ignores bind parameter name and only a colon symbol is taken into account.
Oracle docs

IF-ELSE issue in Select Statement in Oracle

I have following query in SQL Server which I am trying to convert to Oracle 11g.
IF '[Param.1]' = 'S' OR '[Param.1]' = 'T' THEN
select * from ULQUEUE
END IF
But when I write the same query in Oracle, it gives error stating Invalid SQL Statement. So how do I incorporate IF-ELSE in Select Statement in Oracle?
You are converting a T-SQL statement. The equivalent in Oracle is an anonymous PL/SQL block.
However, PL/SQL is a bit more demanding than T-SQL. It requires selecting rows into variables. If the query will return more than one row we need to define a collection variable or use a cursor.
Depending on your requirements you may enbd up with something like this:
begin
if ( &&param1 = 'S' or &&param1 = 'T' ) then
for lrec in ( select * from ulqueue ) loop
do_something;
end loop;
end if;
end;
/
I agree this looks like more work than T-SQL, but PL/SQL is a proper programming language with a lot more functionality. Find out more.

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!

Simple Oracle variable SQL Assignment

Despite having spent an hour researching I can't seem to figure out how to correctly define a variable and then use it in your SQL.
This is what I have so far produced:
DECLARE startDate DATE := to_date('03/11/2011', 'dd/mm/yyyy');
of which I get the reply:
ORA-06550: line 1, column 63: PLS-00103: Encountered the symbol
"end-of-file" when expecting one of the following:
begin function package pragma procedure subtype type use form current
cursor
Details: DECLARE startDate DATE := to_date('03/11/2011',
'dd/mm/yyyy'); Error at line 1 ORA-06550: line 1, column 63:
PLS-00103: Encountered the symbol "end-of-file" when expecting one of
the following:
begin function package pragma procedure subtype type use form current
cursor
I'd love to find out how to do such a simple task!
Your variable declaration is correct.
The DECLARE keyword is used to define variables scoped in a PL/SQL block (whose body is delimited by BEGIN and END;). How do you want to use this variable?
The following PL/SQL works fine for me:
DECLARE
startDate DATE := to_date('03/11/2011', 'dd/mm/yyyy');
reccount INTEGER;
BEGIN
SELECT count(*) INTO reccount
FROM my_table tab
WHERE tab.somedate < startDate;
dbms_output.put_line(reccount);
END;
You can also use the DEFINE statement to use simple string substitution variables. They are suitable for a client like SQL/PLUS or TOAD.
DEFINE start_date = "to_date('03/11/2011', 'dd/mm/yyyy')"
SELECT COUNT(*) from my_table tab where tab.some_date < &start_date;
To accomplish what you're attempting in Toad, you don't need to declare the variable at all. Simply include your variable prefaced with a colon and Toad will prompt you for the variable's value when you execute the query. For example:
select * from all_tables where owner = :this_is_a_variable;
If this doesn't work initially, right-click anywhere in the editor and make sure "Prompt for Substitution Variables" is checked.
If you really want to do it similarly to the way SQL Server handles variables (or you want to be able to do the same thing in SQL*Plus), you can write it as follows:
var this_is_a_variable varchar2(30);
exec :this_is_a_variable := 'YOUR_SCHEMA_NAME';
print this_is_a_variable;
select * from all_tables where owner = :this_is_a_variable;
However, to make this work in Toad, you'll need to run it through "Execute as script", rather than the typical "Execute statement" command.
Take in mind that Oracle's PL/SQL is not SQL.
PL/SQL is a procedural language. SQL is not procedural, but you can define "variables" the user can enter via the "&var" syntax (see http://www.orafaq.com/node/515).
This is an old post, but in case anyone stumbles on this (as I just did), you can handle this with a CTE:
with params as (
select date '2011-11-03' as startdate
from dual
)
select . . .
from params cross join
. . .
Almost the same syntax works in SQL Server (minus the date-specific stuff and from dual).
Solution
DEF startDate = to_date('03/11/2011', 'dd/mm/yyyy');
Select &startDate from dual;

Resources