如何执行 SQL 游标转换

Posted

技术标签:

【中文标题】如何执行 SQL 游标转换【英文标题】:How to perform SQL cursor transformation 【发布时间】:2019-09-23 14:50:39 【问题描述】:

在我的 Oracle 存储过程中,有一个基于选择的查询,不是那么复杂,只是几个连接表,带有额外的普通过滤器和单一排序依据。

查询包含一个用户(按名称排序),每个用户有一些属性和可能的​​几行。

但是我需要添加一些计算出来的额外列。

    将 1 设置为组中第一行中具有相同“名称”的“标志”列(其他行为 0)。

    累积列“权利”,它确实加入了相同的字段“权利”前行值以及其他列和当前行值(在我的情况下为“组”),也应该为每个用户重新设置

例如:

john;        admin;         1;            admin
john;        poweruser;     0;            admin | poweruser
ken;         guest;         1;            guest
ted;         developer;     1;            developer
ted;         user;           0;           developer | user
ted;         techwriter;     0;           developer | user | techwriter

我需要执行一些格式化并作为存储过程的结果返回。 不知道如何设置所需的值并从存储过程中返回。

我应该将初始查询声明为游标,循环遍历并设置(可能吗?对于非数据库字段?)以及如何从存储过程中返回此游标?

【问题讨论】:

以及如何在组内进行排序?为什么 admin 是第一条记录,而 poweruser 是 john 的第二条记录?有排序标准吗? 你确定你真的想要每个用户多行吗?似乎您可能真的想listagg()所有“权利”,为每个“名称”提供一行(并使标志变得不必要)? 除了“名称”之外没有排序。不幸的是,我无法删除标志或/和聚合一组行,输出格式由消费者系统预定义,我们无权访问 简单问题:为什么 admin 的标志是 1 而 poweruser 的标志是 0? 另见this问题 【参考方案1】:

您还可以将其他一些分析函数与 LISTAGG() 一起使用来解决问题。由于您不提供测试数据,因此我将使用 EMP 表进行演示:

select deptno, ename,
  case rn when 1 then 1 else 0 end flags,
  case cnt when rn
    then enames
    else substr(enames, 1, instr(enames, ' | ', 1, rn) - 1)
  end enames
from (
  select deptno, ename,
    row_number() over(partition by deptno order by ename) rn,
    count(*) over(partition by deptno) cnt,
    listagg(ename, ' | ') within group(order by ename) over(partition by deptno) enames
  from emp
) a;

DEPTNO   ENAME    FLAGS  ENAMES                                           
    10   CLARK        1  CLARK                                             
    10   KING         0  CLARK | KING                                      
    10   MILLER       0  CLARK | KING | MILLER                             
    20   ADAMS        1  ADAMS                                             
    20   FORD         0  ADAMS | FORD                                      
    20   JONES        0  ADAMS | FORD | JONES                              
    20   SCOTT        0  ADAMS | FORD | JONES | SCOTT                      
    20   SMITH        0  ADAMS | FORD | JONES | SCOTT | SMITH              
    30   ALLEN        1  ALLEN                                             
    30   BLAKE        0  ALLEN | BLAKE                                     
    30   JAMES        0  ALLEN | BLAKE | JAMES                             
    30   MARTIN       0  ALLEN | BLAKE | JAMES | MARTIN                    
    30   TURNER       0  ALLEN | BLAKE | JAMES | MARTIN | TURNER           
    30   WARD         0  ALLEN | BLAKE | JAMES | MARTIN | TURNER | WARD

最好的问候,斯图阿什顿

【讨论】:

【参考方案2】:

我知道您已经找到了适合您的答案。我会为后代添加这个。

SQL MODEL 子句可以很好地解决这个问题。这是一个使用DBA_ROLE_PRIVS 表的示例,该表存在于每个Oracle 数据库中,并且与您帖子中的数据具有相似的结构和概念。显然,您应该将 DBA_ROLE_PRIVS 替换为您的表名。

select grantee,
       granted_role,
       DECODE(rn,1,1,0) flag,
       role_list
FROM dba_role_privs
MODEL 
  PARTITION BY (grantee)
  DIMENSION BY (ROW_NUMBER() OVER ( PARTITION BY grantee ORDER BY granted_role) AS rn)
  MEASURES (CAST(NULL AS VARCHAR2(4000)) as role_list, granted_role)
  RULES UPSERT
    ( role_list[1] = granted_role[1],
      role_list[rn>1] = role_list[cv(rn)-1] || ',' || granted_role[cv(rn)]);

我不会发布我的数据库中的示例结果(出于安全原因),但我认为它们符合您的要求。

【讨论】:

不错。 (我真的需要掌握示范条款... *8-)。 db<>fiddle demo 与我的答案相同的假数据,只是为了表明它符合预期。【参考方案3】:

不幸的是,listagg() 的解析版本不允许使用 window 子句,这会使这变得相当简单。

您可以通过递归子查询分解来模拟这一点,但是由于您没有真正的排序标准(除了名称之外),您需要添加一些东西来代替它,例如行号或排名函数;也可以在 CTE 中:

with cte (name, right, rn) as (
  select name,
    right,
    row_number() over (partition by name order by null)
  from your_data
),
rcte (name, right, rn, flag, rights) as (
  select name, right, rn, 1, right
  from cte
  where rn = 1
  union all
  select c.name, c.right, c.rn, 0, r.rights || ' | ' || c.right
  from rcte r
  join cte c on c.name = r.name and c.rn = r.rn + 1
)
select name, right, flag, rights
from rcte
order by name, rn;

NAME RIGHT            FLAG RIGHTS                        
---- ---------- ---------- ------------------------------
john admin               1 admin                         
john poweruser           0 admin | poweruser             
ken  guest               1 guest                         
ted  developer           1 developer                     
ted  user                0 developer | user              
ted  techwriter          0 developer | user | techwriter 

这里your_data 是您的查询现在正在执行的操作;整个事情可能会进入 CTE 内部,并在最后加上 rn 计算,但如果没有看到您现有的查询,它并不完全清楚。希望这能适应您的数据。

我的rn 计算是按null 排序的,这不是确定性的 - 它在一次查询执行中为您提供一个固定值,但如果再次运行,您可能会得到一个不同的值。由于权利的顺序似乎并不重要,因此您也可以按照这些顺序进行排序,以给出确定性的结果;这可能会也可能不会改变输出(因为上面是不确定的,它可能会碰巧匹配这个;有时......):

cte (name, right, rn) as (
  select name,
    right,
    row_number() over (partition by name order by right)
  from your_data
),
rcte (name, right, rn, rights) as (
  select name, right, rn, right
  from cte
  where rn = 1
  union all
  select c.name, c.right, c.rn, r.rights || ' | ' || c.right
  from rcte r
  join cte c on c.name = r.name and c.rn = r.rn + 1
)
select name, right, case when rn = 1 then 1 else 0 end as flag, rights
from rcte
order by name, rn;

NAME RIGHT            FLAG RIGHTS                        
---- ---------- ---------- ------------------------------
john admin               1 admin                         
john poweruser           0 admin | poweruser             
ken  guest               1 guest                         
ted  developer           1 developer                     
ted  techwriter          0 developer | techwriter        
ted  user                0 developer | techwriter | user 

实际上,您可能还有其他一些可以使用的标准,例如您要加入的其中一个表中的标志或序列;或者您可以通过 case 表达式根据正确的值应用自己的值。

【讨论】:

【参考方案4】:

以下是一种可能如何转换由存储过程返回的游标

你必须做三个步骤:

1) 为原始游标定义一个行和表TYPEs

2) 定义一个表函数返回原始光标

3) 定义在查询表函数(第 2 点)并将其与其他源连接时打开新游标的新过程

示例

原程序

create procedure P1(cur OUT SYS_REFCURSOR) IS
begin
  open cur for
  select id, col from V1;
end;
/

1) 定义类型

create or replace type t_row is object
(id int,
 col VARCHAR2(5));
/

create or replace type t_table is table of t_row;
/

2) 定义返回原始光标的表函数

create or replace  function F1 return t_table PIPELINED as
  cv sys_refcursor;
  v_row  v1%rowtype; 
begin
  P1(cv);
  loop
    FETCH cv  INTO v_row;
    EXIT WHEN cv%NOTFOUND;
    PIPE ROW(t_row(v_row.id, v_row.col));
  end loop;
  close cv;
  return;
end;
/

请注意,现在您可以使用 SQL 访问原始 游标

select * from table(F1);

        ID COL  
---------- -----
         1 A    
         2 B

3) 定义执行转换的新过程

请注意,我忽略了您的详细信息,只是通过添加新的虚拟列来模拟转换

create procedure P2(cur OUT SYS_REFCURSOR) IS
begin
  open cur for
  select id, col, 'new' col2
  from table(F1);
end;
/

【讨论】:

以上是关于如何执行 SQL 游标转换的主要内容,如果未能解决你的问题,请参考以下文章

PL/SQL编程_游标

如何在 Oracle PL/SQL 过程的开始部分之后声明游标

如何使用Oracle的游标?

PL/SQL练习游标cursor :oracle 在执行sql语句时,为sql语句所分配的一个私有的内存区域

如何强制优化器重用 sql 游标

Python MySQL连接器在游标循环中执行第二条sql语句?