I have a function(funct_A) that returns a varchar in quotes
Select funct_A from dual returns 'A1234';
I want to use the result from funct_A in an IN clause
This returns 100 --> select count(E.tickets) from event E where E.ticket_number in ('A1234').
This returns 0 --> select count(E.tickets) from event E where E.ticket_number in (funct_A).
How is this accomplished?
How is this accomplished.
You probably wouldn't. If your function always returns a single value, you'd just use an equality condition rather than an IN. Assuming your actual goal, though, is for the function to return multiple values, you likely want a pipelined table function instead
create or replace type ticket_tbl is table of varchar2(10);
create or replace function funcA
return ticket_tbl
pipelined
is
begin
pipe row ('A1234');
pipe row ('B5678' );
end;
/
select *
from event e
where e.ticket_number in (select column_value
from table( funcA ) );
Related
I have a stored procedure taking an PL/SQL associative array-valued input, like the following:
TYPE IntegerArray is table of Number index by binary_integer;
PROCEDURE GetItems(itemIds in IntegerArray, results out sys_refcursor)
IS BEGIN
Open Results for
select id, name, price from Item where id in (select * from table(itemIds));
END;
It's called from C# and the input item ids are in a specific order, which should be retained in the output.
I can write C# code which will order the resultant object array List<(int id,string name,int price)> data based on the input List<int> ids since there is a 1:1 mapping input - output, but what I cannot tell is if I need to, or Oracle will automatically return rows in an order based on the input. Is this guaranteed? Does Oracle even have a concept of ordering in the input?
If you have a nested table collection type defined in SQL (you do not, see below):
CREATE TYPE IntegerList is table of Number;
Then you can use it in SQL statements.
When you do:
SELECT * FROM TABLE(instance_of_integer_list)
Then empirical evidence suggests that it will return the values from the collection in order.
(Note: this is not guaranteed as result sets are non-deterministically ordered when they do not have an ORDER BY clause and there may be cases, such as perhaps large arrays on a parallel database, where the empirical evidence for small arrays on a single database instance does not hold and the order may not be maintained.)
However:
SELECT id, name, price
FROM Item
WHERE id IN (SELECT * FROM TABLE(instance_of_integer_list));
Does not guarantee any particular order for the outer query.
If you want to order the outer query based on the collection's order then you will need to use a JOIN and an ORDER BY clause:
SELECT i.id, i.name, i.price
FROM Item i
INNER JOIN (
SELECT COLUMN_VALUE AS item,
ROWNUM AS position
FROM TABLE(instance_of_integer_list)
) t
ON i.id = t.item
ORDER BY t.position;
You do NOT have a nested-table collection type (and C# does not support passing nested-table collection types, it only supports PL/SQL associative array types).
You should need to convert your PL/SQL associative array collection to an SQL nested-table collection to be able to use it in SQL:
CREATE PACKAGE your_package IS
TYPE IntegerArray IS TABLE OF NUMBER INDEX BY binary_integer;
FUNCTION map_ids(
Ids IN IntegerArray
) RETURN IntegerList PIPELINED;
PROCEDURE GetItems(
ItemIds IN IntegerArray,
results OUT SYS_REFCURSOR
);
END;
/
CREATE PACKAGE BODY your_package IS
FUNCTION map_ids(
Ids IN IntegerArray
) RETURN IntegerList PIPELINED
IS
v_idx BINARY_INTEGER;
BEGIN
IF ids IS NULL THEN
RETURN;
END IF;
v_idx := ids.FIRST;
WHILE v_idx IS NOT NULL LOOP
PIPE ROW(ids(v_idx));
v_idx := ids.NEXT(v_idx);
END LOOP;
END;
PROCEDURE GetItems(
ItemIds IN IntegerArray,
results OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN Results FOR
SELECT i.id, i.name, i.price
FROM Item i
INNER JOIN (
SELECT COLUMN_VALUE AS item,
ROWNUM AS position
FROM TABLE(map_ids(ItemIds))
) t
ON i.id = t.item
ORDER BY t.position;
END;
END;
/
Then if the order that the associative array sorts its keys corresponds to the order that you want to display the data then it should work (note: this is untested as I do not have your data, tables or C# application to call the package).
I have a function which is not working as expected. How can I modify to get the desired output?
CREATE OR REPLACE FUNCTION f_get_all_programs_test(
pidm in number,aidy in varchar2
) RETURN VARCHAR2
IS
TERM NUMBER;
result VARCHAR2(300);
CURSOR C
IS
SELECT stvterm_code FROM stvterm
WHERE stvterm_fa_proc_yr = aidy;
BEGIN
Open C;
loop
fetch C into TERM;
exit when C%NOTFOUND;
SELECT LISTAGG(rzkutil.f_get_program (pidm,TERM, aidy), ',') WITHIN GROUP (ORDER BY stvterm.stvterm_code)
INTO result
FROM stvterm stvterm
WHERE stvterm.stvterm_fa_proc_yr = aidy;
end loop;
RETURN result;
END;
Cursor select Query returns multiple rows. For each row, the function rzkutil.f_get_program must run and separate them by comma. The existing code is not working as expected. instead the output is repeating multiple times.
Example:
select rzkutil.f_get_program(12098136,'',2122) from dual -- AAS-PG2-AOTO (result)
select f_get_all_programs_test(12098136,'2122') from dual --AAS-PG2-AOTO,AAS-PG2-AOTO,AAS-PG2-AOTO,AAS-PG2-AOTO,AAS-PG2-AOTO,AAS-PG2-AOTO (result, instead it should return AAS-PG2-AOTO)
As values you're aggregating come from the cursor, it means that its (cursor's) query returns duplicates. To avoid them, use the DISTINCT keyword:
CURSOR C
IS
SELECT DISTINCT stvterm_code
FROM stvterm
WHERE stvterm_fa_proc_yr = aidy;
Though, I believe that you don't need such a complicated (and potentially slow, because of loop processing) code. I don't have any test data to try it, but - see if this helps:
CREATE OR REPLACE FUNCTION f_get_all_programs_test (pidm IN NUMBER,
aidy IN VARCHAR2)
RETURN VARCHAR2
IS
result VARCHAR2 (300);
BEGIN
SELECT LISTAGG (term, ',') WITHIN GROUP (ORDER BY stvterm_code)
INTO result
FROM (SELECT DISTINCT
rzkutil.f_get_program (pidm, stvterm_code, aidy) AS term,
stvterm_code
FROM stvterm
WHERE stvterm_fa_proc_yr = aidy);
RETURN result;
END;
Is it possible to select the parameters for calling a procedure from the select statement?
EXECUTE PROCEDURE_NAME(para1,para2,para3,para4);
commit;
Is it possible to select para1,para2,para3,para4 from a select query?
EXECUTE PROCEDURE_NAME((SELECT PARA1,PARA2,PARA3,PARA4 FROM TABLEA))
COMMIT;
I do not have access to modify the procedure.
As a slight variation on what #vc74 suggested, you could just replace your EXECUTE command (which, assuming this is SQL*Plus or SQL Developer anyway, is just a wrapper for an anonymous block anyway) with an explicit anonymous block:
begin
for r in (SELECT PARA1,PARA2,PARA3,PARA4 FROM TABLEA) loop
PROCEDURE_NAME(r.PARA1,r.PARA2,r.PARA3,r.PARA4);
end loop;
end;
/
(I've left the bits from your original call uppercase and the new bits lower case mostly to distinguish them.)
Using a loop just means you don't need to declare local variables and select into those. It would also allow you to process multiple rows from the table, though I see form a comment you only expect one row. However, the flip side of that is it won't complain if there are no rows, or if there is more than one row, as the variable approach would do.
You could also use a record type to avoid declaring all the parameters separately:
declare
l_row tablea%rowtype;
begin
SELECT * into l_row FROM TABLEA;
PROCEDURE_NAME(l_row.PARA1,l_row.PARA2,l_row.PARA3,l_row.PARA4);
end;
/
This now does expect exactly one row to be found in the table.
You can call the functions in sql. So if you are able to create a function in your schema then you can do the following:
create a function function_name in your schema that calls the procedure procedure_name and returns some dummy result
use this function in sql query: select function_name(para1,para2,para3,para4) from tablea
example of function:
create or replace function function_name(
p1 varchar2,
p2 varchra2,
p3 varchar2,
p4 varchar2
) return number
is
begin
procedure_name(p1,p2,p3,p4); -- here you execute the procedure
return null;
end;
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.
I have a PL/SQL collection of following type
type p_typ_str_tab is table of varchar2(4000) index by pls_integer;
I would like to aggregate the values into a single string with a simple inline function like LISTAGG without writing any custom functions or for loops. All examples of LISTAGG don't show how to use PL/SQL collections. I'm using Oracle 11g R2. Is this possible?
To be able to use LISTAGG function with a collection, the collection must be declared as nested table not as an associative array and must be created as sql type (schema object) because it's not possible to use pl/sql type in a select statement. To that end you might do the following:
--- create a nested table type
SQL> create or replace type t_tb_type is table of number;
2 /
Type created
--- and use it as follows
SQL> select listagg(column_value, ',') within group(order by column_value) res
2 from table(t_tb_type(1,2,3)) -- or call the function that returns data of
3 / -- t_tb_type type
RES
-------
1,2,3
Otherwise the loop is your only choice.
LISTAGG is an analytic SQL function, and those don't operate against PL/SQL collections, but cursors/rowsets.
So, in short, no, it's not possible.
That said, iterating over a PL/SQL table to build a concatenated string is trivial:
l_new_string := null;
for i in str_tab.first .. str_tab.last
loop
if str_tab(i) is not null then
l_new_string := str_tab(i) || ', ';
end if;
end loop;
-- trim off the trailing comma and space
l_new_string := substr(l_new_string, 1, length(l_new_string) - 2);