Listagg function with PLSQL collection - oracle

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);

Related

ORACLE - Selecting Parameters for calling Procedure from Table

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;

Bulk collect sql

i made a data_object by myself :
CREATE OR REPLACE TYPE my_object AS OBJECT(
number_type NUMBER,
varchar_type VARCHAR2(20)
)
and then i create type
CREATE OR REPLACE TYPE my_nt IS TABLE OF my_object;
And I want with nested table and this object make a procedure, that will be return number of employyes of some departments. I ve got Two tables: employees and department a this is my code:
DECLARE
enum_dname my_nt := my_nt();
PROCEDURE print_l IS
BEGIN
DBMS_OUTPUT.put_line('---------------------------------------------------------');
FOR i IN 1..enum_dname.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE(enum_dname(i));
END LOOP;
END;
BEGIN
SELECT COUNT(emp_id) as number_of, department_name
BULK COLLECT INTO enum_dname
FROM employees e, department d
WHERE e.department_id = d.department_id
GROUP BY department_name;
print_l;
END;
And it show me errors : PLS - 00306: Wrong numbers of argument in call type: PUT_LINE
and PL\SQL : ORA - 00947:not enough values
THANK YOU!
You have two errors. As #SudiptaMondal pointed out (and as here) you can't pass an object to put_line(), you have to pass a single string value - or something which evaluates to a string, which can be concatenated or implicit converted or whatever. So here you might do:
DBMS_OUTPUT.PUT_LINE(enum_dname(i).varchar_type || ': ' || enum_dname(i).number_type);
or however you want to format that output. Using dbms_output for anything except debugging generally not a good idea as you have no control over whether someone using your code has output enabled. But this may be enough for this exercise.
The second problem, causing the ORA-00947, is because your query is trying to bulk collect two scalar variables into a collection of objects. You need to include the object constructor:
SELECT my_object(COUNT(emp_id), department_name)
BULK COLLECT INTO enum_dname
...

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.

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.

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.

Resources