How to use variables in an Oracle PL/SQL where clause - oracle

I can't seem to get variables to work in an Oracle PL/SQL where clause. I come from a Microsoft SQL Server background and there it was easy. For example, what would be all steps needed to do something similar to the following?
declare #var int set #var = 1
select * from SomeTable where SomeField = #var
This doesn't seem like it should be hard in PL/SQL, but evidently it is. :-/ I hear I need to use cursors and the like in PL/SQL?
Any help would be greatly appreciated. Thanks.

What do you want to do with the data that the SELECT returns? If you just want to see it you don't need PL/SQL at all, just do this in SQL Plus:
variable var number
exec :var := 1
select * from SomeTable where SomeField = :var;
Or in a tool like SQL Developer or Toad, just do this:
select * from SomeTable where SomeField = :var;
and it will prompt you to enter the value for :var.

The following code declares a variable var to use in the WHERE clause, and a variable result to put the result in then executes it inside a PL/SQL block.
DECLARE
var INT := 1;
result INT;
BEGIN
SELECT 123
INTO result
FROM DUAL
WHERE var = 1;
DBMS_OUTPUT.put_line (var);
DBMS_OUTPUT.put_line (result);
END;
The DBMS_OUTPUT.PUT_LINE calls make it produce this DBMS output:
1
123

declare
type t_rec is record
(
col1 number,
col2 myTable.col2%type
);
v_rec t_rec;
type t_tab is table of v_rec%type index by binary_integer;
v_tab t_tab;
begin
select col1, col2
bulk collect into v_tab
from myTable
where col3 = 'BLAH';
-- do something great with v_tab...
end;
Also know that if you try to select into (or bulk collect into) a variable and no rows are returned, you'll get a no_data_found exception, so you may want to handle that situation.
See more here on pl/sql collections. Above uses an associative array, but there are nested tables and varrays as well. Again, see the link.
Hope that helps.

I use it like this
select * from sec_mainmenu where serno = '&MENU_ID';
When you run it, pl/sql will prompt for MENU_ID value.

Related

How to use Collection as a table for UPDATE with an IN clause

I need this piece of code in a stored procedure so I can pass an array of ids and update the related records. I am wondering whether I have to use the loop rather than use an IN clause in the sp.
SET SERVEROUTPUT ON
DECLARE
P_IDS PKGINFO.t_ids; --type: table of NUMBER index by pls_integer;
P_RESULT NUMBER;
BEGIN
p_IDS(1) := 12345;
--this works fine:
for i in ( select * from table(p_ids))
loop
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID = i.column_value;
end loop;
--this works fine too:
SELECT COUNT(*) INTO p_RESULT FROM TABLE1
WHERE P_ID IN (SELECT * FROM TABLE (p_ids));
--but this does not work, why????? how to make it work?
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID IN (SELECT * FROM TABLE (p_ids));
END;
--==================PKGINFO.t_ids==================
CREATE OR REPLACE package dbname.PKGINFO as
-- package created to perform Associative array calls
type t_ids is table of NUMBER index by pls_integer;
end PKGINFO;
/
I expected the UPDATE can use IN clause, but it gives me an INVALID TYPE error.
Until fairly recently Oracle didn't allow PL/SQL types in SQL statements, including in a table collection expression. You seem to be using a version where support has been added for select but not (yet) for update. If you had one available then you could use a schema-level type. Also have a look at member of.
With the type you have now you could use FORALL, which would be more efficient than a loop with individual updates::
FORALL i IN p_ids.first..p_ids.last
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID = p_ids(i);

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.

Select count from collection

Is there a way to capture the count in a collection table for a certain condition.
For example :
SELECT COUNT(*)
BULK COLLECT INTO v_cnt
FROM tt_product
WHERE region = 'NA';
The above query throws error saying:
Error(649,13): PL/SQL: ORA-00942: table or view does not exist
Any insights would be highly appreciated .
Thanks
Firstly, remove BULK COLLECT, you are getting a single value...
Secondly, if I understand you right, and you are talking about the collection of e.g. type
CREATE OR REPLACE T AS TABLE OF ...
then wrap the name with the table() keyword
SELECT COUNT(*)
INTO v_cnt
FROM table(tt_product)
WHERE region = 'NA';
Dump your results into a #TEMP table first then do your count from there.
Something like:
SELECT * BULK COLLECT INTO #TEMP
FROM tt_product
Where region = 'NA'
SELECT COUNT(*) FROM #TEMP
This is untested so you might have to play with it a little to get it to work.
My example is for SQL Server, you'll have to find the equivalent for ORACLE as I'm not as familiar with it.
declare
b l_number := new l_number(1,2,3,4); -- type L_number is table of number ;
v1 number;
v2 number;
begin
select count(*) into v1 from table(b);
select CARDINALITY(b) into v2 from dual;
dbms_output.put_line(v1);
dbms_output.put_line(v2);
end;

What is the simplest way to define a local variable in Oracle?

In the SQL Server, I can define local variables like this.
declare #id number := 1000
select * from tbl_A where id = #id;
select * from tbl_B where id = #id;
It is very convenient.
I tried to do same thing in PL/SQL but it doesn't work.
DECLARE id number;
select 1000 into id from dual;
Do you know how to do something similar? The simplest method is my objective.
If you want to define a local variable in PL/SQL, you need a complete PL/SQL block
DECLARE
id NUMBER;
BEGIN
SELECT 1000
INTO id
FROM dual;
END;
or just
DECLARE
id NUMBER := 1000;
BEGIN
<<do something that uses the local variable>>
END;
If you want to declare a variable in SQL*Plus
SQL> variable id number
SQL> begin
select 1000 into :id from dual;
end;
/
SQL> print id
ID
----------
1000
SQL> SELECT * FROM tbl_a WHERE id = :id
An alternative to DECLARE Block is to use a WITH Clause:
WITH my_params AS (
SELECT 123 AS min_id FROM DUAL
)
SELECT *
FROM some_table
WHERE id > (SELECT min_id FROM my_params)
It is more portable as many vendors support the WITH clause and you can change seamless from parameter to dynamic value. For example:
WITH my_params AS (
SELECT min(id) AS min_id FROM some_id_table
)
SELECT *
FROM some_table
WHERE id > (SELECT min_id FROM my_params)
Solution for Oracle SQL
DEF x = foo
SELECT '&x' FROM dual;
The result will be : foo
NB: The variable will keep the value even after execution. To clear variable run UNDEFINE x.
General syntax to declare variable in PL/SQL is
var_nm datatype [NOT NULL := var_value ];
var_nn is the name of the variable.
datatype is a valid PL/SQL datatype.
NOT NULL is an optional specification on the variable which this variable cannot be assigned null value.
var_value or DEFAULT value is also an optional specification, where you can initialize a variable with some specific value.
Each variable declaration is a separate statement and must be terminated by a semicolon.
We can assign value to variables in one of the following two ways -
direct assignment (Eg. var_nm:= var_value;)
Using select from (Eg. SELECT col_nm INTO var_nm FROM tbl_nm [WHERE clause];)
In you case as Justin Cave has already mentioned it can be
DECLARE
id number;
BEGIN
SELECT 1000 into id from dual;
dbms_output.put_line('id : '|| id );
END;
/
OR
DECLARE
id number := 1000;
BEGIN
dbms_output.put_line('id : '|| id );
END;
/
NOTE: '/' i.e Back slash after END keyword indicates to execute the above PL/SQL Block.
(Just stumbled across this thread.) Beginning with SQL*Plus 12.2, you can declare and assign a value at the same time:
SQL> var id number = 1000
SQL> select * from tbl_A where id = :id;
Oracle calls it input binding.
If you must do it in PL/SQL, the answer was given by others.

How to return a resultset / cursor from a Oracle PL/SQL anonymous block that executes Dynamic SQL?

I have this table:
ALLITEMS
---------------
ItemId | Areas
---------------
1 | EAST
2 | EAST
3 | SOUTH
4 | WEST
The DDL:
drop table allitems;
Create Table Allitems(ItemId Int,areas Varchar2(20));
Insert Into Allitems(Itemid,Areas) Values(1,'east');
Insert Into Allitems(ItemId,areas) Values(2,'east');
insert into allitems(ItemId,areas) values(3,'south');
insert into allitems(ItemId,areas) values(4,'east');
In MSSQL, to get a cursor from a dynamic SQL I can do:
DECLARE #v_sqlStatement VARCHAR(2000);
SET #v_Sqlstatement = 'SELECT * FROM ALLITEMS';
EXEC (#v_sqlStatement); --returns a resultset/cursor, just like calling SELECT
In Oracle, I need to use a PL/SQL Block:
SET AUTOPRINT ON;
DECLARE
V_Sqlstatement Varchar2(2000);
outputData SYS_REFCURSOR;
BEGIN
V_Sqlstatement := 'SELECT * FROM ALLITEMS';
OPEN outputData for v_Sqlstatement;
End;
--result is : anonymous block completed
**But all I get is
anonymous block completed".
How do I get it to return the cursor?
(I know that if I do AUTOPRINT, it will print out the information in the REFCURSOR (it's not printing in the code above, but thats another problem))
I will be calling this Dynamic SQL from code (ODBC,C++), and I need it to return a cursor. How?
You can write a PL/SQL function to return that cursor (or you could put that function in a package if you have more code related to this):
CREATE OR REPLACE FUNCTION get_allitems
RETURN SYS_REFCURSOR
AS
my_cursor SYS_REFCURSOR;
BEGIN
OPEN my_cursor FOR SELECT * FROM allitems;
RETURN my_cursor;
END get_allitems;
This will return the cursor.
Make sure not to put your SELECT-String into quotes in PL/SQL when possible. Putting it in strings means that it can not be checked at compile time, and that it has to be parsed whenever you use it.
If you really need to use dynamic SQL you can put your query in single quotes:
OPEN my_cursor FOR 'SELECT * FROM allitems';
This string has to be parsed whenever the function is called, which will usually be slower and hides errors in your query until runtime.
Make sure to use bind-variables where possible to avoid hard parses:
OPEN my_cursor FOR 'SELECT * FROM allitems WHERE id = :id' USING my_id;
in SQL*Plus you could also use a REFCURSOR variable:
SQL> VARIABLE x REFCURSOR
SQL> DECLARE
2 V_Sqlstatement Varchar2(2000);
3 BEGIN
4 V_Sqlstatement := 'SELECT * FROM DUAL';
5 OPEN :x for v_Sqlstatement;
6 End;
7 /
ProcÚdure PL/SQL terminÚe avec succÞs.
SQL> print x;
D
-
X
You should be able to declare a cursor to be a bind variable (called parameters in other DBMS')
like Vincent wrote, you can do something like this:
begin
open :yourCursor
for 'SELECT "'|| :someField ||'" from yourTable where x = :y'
using :someFilterValue;
end;
You'd have to bind 3 vars to that script. An input string for "someField", a value for "someFilterValue" and an cursor for "yourCursor" which has to be declared as output var.
Unfortunately, I have no idea how you'd do that from C++. (One could say fortunately for me, though. ;-) )
Depending on which access library you use, it might be a royal pain or straight forward.
This setting needs to be set:
SET SERVEROUTPUT ON

Resources