【问题标题】:PL/SQL Creating a procedure that contains result set joinsPL/SQL 创建包含结果集连接的过程
【发布时间】:2016-07-15 20:05:28
【问题描述】:

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

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

示例:

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:这可以在单个查询中完成,但为了论证(以及在我的实际用例中),我想在一个过程中分解这些步骤。示例代码描述了我正在尝试实现的 IF 游标之间的连接工作。但他们没有,我正在寻找替代方案。

【问题讨论】:

  • 请提供更多详细信息,例如任何示例代码。现在还不清楚为什么所有这些步骤都不能在单个 SQL 查询中完成。
  • 我不希望它们在单个 SQL 查询中完成。它们可以完成,但我想将它们分开以便于使用和可维护性。

标签: oracle join stored-procedures cursor temp-tables


【解决方案1】:

我最终使用了一个函数(尽管也可以使用一个过程)以及 表格。我学到的和应该注意的事情:

  1. PL/SQL 函数只能返回已经预先在模式中声明且明确的类型。您不能创建返回类似MY_TABLE%ROWTYPE 的函数,即使类型信息似乎可用,但这是不可接受的。如果您想返回它,您必须创建一个自定义类型 MY_TABLE%ROWTYPE
  2. Oracle 将声明类型的表与%ROWTYPE 的表区别对待。起初这让我很困惑,但从我收集到的信息来看,这就是它的工作原理。

    DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
    

声明 MY_TABLE 行类型的集合。为了增加这一点,我们必须使用查询 MY_TABLE 的 SQL 语句中的BULK COLLECT INTO。生成的 collection CANNOT be used in JOIN statements 不可查询且不能由函数返回。

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;

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

有效。

【讨论】:

    【解决方案2】:

    您可以通过多种方式进行这种分解,但与单个 SQL 语句相比,所有这些方式都会显着降低性能。
    可维护性改进也值得商榷,取决于具体情况。
    要查看所有可能性,请查看documentation

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

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

    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;
    

    表格搜索功能:

    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。简单的表格函数

    此解决方案使用与具有上述流水线功能的变体相同的类型。

    用户搜索功能:

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

    表格搜索功能:

    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;
    

    表格搜索功能:

    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;
    

    附注所有代码仅用于演示目的:没有优化等等。

    【讨论】:

    • 我很困惑。给出的示例与问题的用例完全不匹配,并且没有数据连接。如上所述,我的问题是如何在存储过程中执行所选数据的 JOIN 操作。我不是在寻找解决一般问题的解决方案,而是寻找适合加入自定义数据集合的特定解决方案。
    • 所有答案都是关于 PL/SQL 中的自定义数据集合。请看description of collections in Oracle docs。如果转换为表格,您可以轻松加入。这里没有理由使用游标。
    • 是的,我想我已经找到了解决方案。但是我不想通过 create 静态声明额外的类型和表类型,并且由于在 pl/sql 语句中声明的表类型不能在查询中使用,我将不得不使用基本 SQL。
    猜你喜欢
    • 1970-01-01
    • 2023-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-21
    • 1970-01-01
    相关资源
    最近更新 更多