PL/SQL 创建包含结果集连接的过程

PL/SQL Creating a procedure that contains result set joins

我想在 PL/SQL 中创建一个包含 5 个步骤的程序。第 1 步和第 2 步首先执行 return 和 ID。在第 3 步中,我们有一个 SELECT 语句,它的条件是 returned ID。然后我想获取该 SELECT 语句的所有结果,并在另一个 SELECT 语句的 JOIN 中使用它们,并在第三个 SELECT 语句中再次使用这些结果JOIN。据我所知,我不能在 JOIN 语句中使用 CURSOR。我的一些同事建议我将结果保存在 CURSOR 中,然后使用循环遍历每一行并将该数据用于下一个 SELECT。然而,由于我要进行 2 次选择,这将创建一个巨大的内部循环分支,而这正是我要避免的。

另一个建议是使用临时表来存储数据。但是这个过程可能会被很多用户同时执行,table的数据会相互冲突。现在我正在查看 LOCAL Temporary tables,它应该根据会话过滤数据,但我不确定我是否想为我的程序创建虚拟 tables,因为我想避免在模式中留下垃圾(此过程适用于应用程序的自定义部分)。有这样做的标准方法吗?有什么想法吗?

样本:

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;

注意:这不起作用。 Oracle 不支持游标之间的连接。 注 2:这可以在单个查询中完成,但为了论证(以及在我的实际用例中),我想在一个过程中分解这些步骤。示例代码描述了我要实现的目标,如果游标之间的连接有效。但他们没有,我正在寻找替代方案。

您可以通过多种方式进行此类分解,但与单个 SQL 语句相比,所有这些方式都有显着的性能损失。
可维护性的提升也值得商榷,视具体情况而定。
要查看所有可能性,请查看 documentation.

以下是一些基于简单逻辑的可能变体:

  1. 根据给定的Id计算Oracle用户名前缀;
  2. 获取名称以此前缀开头的所有用户;
  3. 查找步骤 2 中用户拥有的所有 table;
  4. 计算找到的总数 table。

1.流水线

准备函数要使用的类型:

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
/

通过用户 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;
/

搜索用户功能:

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;

函数搜索 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;

代码中的用法:

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。简单 table 函数

此解决方案使用与上述流水线函数变体相同的类型。

搜索用户功能:

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

函数搜索 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;
/

代码中的用法:

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。光标 - xml - 光标

这是一种特殊情况,可以在没有用户定义类型的情况下实现,但性能损失很大,涉及不必要的类型转换并且可维护性低。

搜索用户功能:

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;

函数搜索 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;

代码中的用法:

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;

当然,所有变体都可以混合使用,但 SQL 至少对于简单的情况看起来更好:

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。所有代码仅用于演示目的:没有优化等。

我最终使用了一个函数(尽管也可以使用一个过程)以及 tables。学习和注意事项:

  1. PL/SQL 函数只能return 预先在schema 中声明的类型,并且是明确的。您不能创建 return 类似于 MY_TABLE%ROWTYPE 的函数,即使看起来类型信息可用,但它不接受 table。你必须创建一个自定义类型 MY_TABLE%ROWTYPE 是你想要 return 它。
  2. Oracle 对待声明类型的 table 与 %ROWTYPE 的 table 不同。一开始这让我很困惑,但从我收集到的信息来看,这就是它的工作原理。

    DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
    

声明了 MY_TABLE 行类型的集合。为了添加到这一点,我们必须使用查询 MY_TABLE 的 SQL 语句中的 BULK COLLECT INTO。生成的 集合 不能在 JOIN 语句中使用,不可查询并且不能被函数 return 编辑。

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) 如果填充可以在使用 TABLE(my_custmo_tab) 查询FROM 语句。作为预先在模式中声明的 table,可以从函数中 returned。但是它不能使用 BULK COLLECT INTO 填充,因为它不是集合。我们必须改为使用正常的 SELECT INTO 语句。但是,如果我们想用现有的 table 中的数据填充它,它有 2 个数字列,我们不能简单地执行 SELECT * INTO my_custom_tab FROM DOUBLE_NUMBER_TABLE,因为 my_custom_tab 尚未初始化并且不包含足够的行接收数据。如果我们不知道查询 return 有多少行,我们就无法对其进行初始化。填充 table 的技巧是使用 CAST 命令并将我们的 select 结果集转换为 MY_CUSTOM_TABLE 然后添加它。

SELECT CAST(MULTISET(SELECT COL_A, COL_B FROM DOUBLE_NUMBER_TABLE) AS MY_CUSTOM_TABLE) INTO my_custom_tab FROM DUAL

现在我们可以通过使用 TABLE() 函数轻松地在查询等中使用 my_custom_tab。

SELECT * FROM TABLE(my_custom_tab)

有效。