-1

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.

PentaKon
  • 3,359
  • 3
  • 32
  • 64
  • Please, provide more details, e.g. any sample code. Now it's not clear why all this steps can't be done in single SQL query. – ThinkJet Mar 28 '16 at 15:14
  • I don't WANT them to be done in a single SQL query. They CAN be done but I want to break them apart for ease of use and maintainability. – PentaKon Mar 28 '16 at 15:20

2 Answers2

1

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:

  1. 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.
  2. 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.

PentaKon
  • 3,359
  • 3
  • 32
  • 64
-1

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:

  1. calculate Oracle user name prefix based on given Id;
  2. get all users whose name starts with this prefix;
  3. find all tables owned by users from step 2;
  4. 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.

ThinkJet
  • 6,495
  • 22
  • 31
  • I'm confused. The example given does not match the question's use case at all and there is no joining of data. My problem, as stated above, is how I can perform a JOIN operation of selected data inside a stored procedure. I'm not looking for solutions to the general problem but the specific one catering to the JOINING of custom data collections. – PentaKon Mar 29 '16 at 07:32
  • All answer are about custom data collections in PL/SQL. Please look at [description of collections in Oracle docs](https://docs.oracle.com/cd/E11882_01/appdev.112/e25519/composites.htm#LNPLS005). You may join it easy if convert to a table. There are no reason to use cursors here. – ThinkJet Mar 29 '16 at 08:29
  • Yes I think I've found the solution. However I don't want to declare extra types and table types statically through create, and since table types declared inside a pl/sql statement can't be used in queries I'm gonna have to go with basic SQL. – PentaKon Mar 29 '16 at 09:18