【问题标题】:Oracle SQL Hierarchical Query: Flatten Hierarchy and Perform AggregationOracle SQL 分层查询:扁平化层次结构并执行聚合
【发布时间】:2012-08-31 18:51:38
【问题描述】:

我正在尝试提高我已经编写的概念证明的性能,但没有运气。我认为这种方法可能存在缺陷,但我正在努力寻找另一种解决方案。我已经涵盖了我能找到的所有 Ask Tom 文章和论坛帖子。

我们正在运行 Oracle 10g R2。

我们有按层次结构排列的项目。数量是在关系上定义的。层次结构中有两种类型的对象:作为逻辑分组的程序集和表示实际项目的项目。因此,如果我们代表一个完整的工具集,我们将有一个代表整个工具集的根,以及一个代表实际工具的叶子。所以:

工具组 -> 螺丝刀 -> 平头螺丝刀 -> 小平头螺丝刀

程序集可以在层次结构中重复使用,项目也可以。

我需要展平层次结构,以便项目的每个实例都有一行和数量。任何关系的数量都可以 >= 1。要获得项目的数量,我们需要从根到叶的所有关系中获取数量的乘积。

我的解决方案有效,但不能很好地扩展。针对实际数据运行大约需要 8 分钟才能生成 6000 多行,我们的层次结构可以生成 50k 多行。理想情况下,这将在 10 秒或更短的时间内完成,但我知道那是……乐观的 ;)

我的解决方案和简化数据集如下。任何反馈将不胜感激!

CREATE TABLE ITEMHIER
(
  PARENT          VARCHAR2(30 BYTE),
  CHILD           VARCHAR2(30 BYTE),
  QUANTITY        NUMBER(15,2),
  ISLEAF          NUMBER
);

INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY005','ITEM001',2,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY005','ITEM002',1,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY005','ITEM003',5,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY006','ITEM002',10,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY006','ITEM004',3,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY007','ITEM005',12,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY007','ITEM006',1,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY008','ITEM006',2,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY008','ITEM005',5,1);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY002','ASSY005',2,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY002','ASSY007',1,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY003','ASSY006',3,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY003','ASSY008',2,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY004','ASSY007',1,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY004','ASSY005',3,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY004','ASSY006',2,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY001','ASSY002',1,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY001','ASSY003',2,0);
INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY001','ASSY004',1,0);

COMMIT;
/

CREATE OR REPLACE FUNCTION GETQTY(P_NAVPATH   IN VARCHAR2,
                                  P_STARTWITH IN VARCHAR2) RETURN INTEGER AS

R_QTY  INTEGER;

BEGIN

    SELECT EXP(SUM(LN(QUANTITY)))
    INTO R_QTY
    FROM (
           SELECT QUANTITY, SYS_CONNECT_BY_PATH(CHILD,'/') NAV_PATH
           FROM ITEMHIER
           START WITH PARENT = P_STARTWITH
           CONNECT BY PRIOR  CHILD = PARENT
         )
    WHERE INSTR(P_NAVPATH, NAV_PATH) = 1; 

    RETURN R_QTY;
END;
/

SELECT 'ASSY001' || SYS_CONNECT_BY_PATH(CHILD,'/') NAV_PATH,
      GETQTY(SYS_CONNECT_BY_PATH(CHILD,'/'), 'ASSY001') QTY,
      CHILD
FROM ITEMHIER
WHERE ISLEAF = 1
START WITH PARENT = 'ASSY001'
CONNECT BY PRIOR CHILD = PARENT;

----编辑

使用WITH 子句,我能够将处理时间缩短大约 1/2,这是一个很大的收获!还有其他想法吗?

with
h as (
    select sys_connect_by_path(child,'/') navpath,
          child,
          quantity qty,
          isleaf
    from itemhier
    start with parent = 'ASSY001'
    connect by prior child = parent
)
select h1.navpath,
       h1.child,
       (SELECT exp(sum(ln(h2.qty)))
        FROM h h2
        WHERE instr(h1.navpath, h2.navpath) = 1) qty
from h h1
where isleaf = 1

编辑 2

jonearles 建议使用 sys_connect_by_path 构建算术表达式,然后使用 PL/SQL 对其进行评估似乎是可行的方法。运行我最大的数据集,我能够在 55 秒内生成 77k 行输出。

我也尝试使用并行性,但正如他所说,几乎没有性能提升。

【问题讨论】:

  • 索引PARENT 可能会有很大帮助。您的示例数据非常有用,并且比大多数人发布的要好得多,但是对于诊断性能问题,拥有更多数据会有所帮助。例如,我使用这样的脚本来尝试创建更大的数据集:begin for i in 1 .. 100000 loop INSERT INTO ITEMHIER (PARENT, CHILD, QUANTITY, ISLEAF) VALUES ('ASSY005'||lpad(i, 6, '0'),'ITEM001'||lpad(i, 6, '0'),2,1); ...。但我不确定这是否真的能代表您的数据。
  • 谢谢...我尽量做到彻底,答案与问题一样好:) 该表已被很好地索引,并且 parent 是索引之一。我非常有信心在索引部门没有太多收获。

标签: sql oracle aggregate-functions hierarchical-data hierarchical


【解决方案1】:

Podiluska 的建议很好。如果您有 Oracle 11g R2,则可以使用公用表表达式。新语法的递归性质将允许您放弃 sys_connect_by_pathinstr 的组合,这将严重影响您的性能。

试试这个:

select
  child,
  sum(total_quantity) total_quantity
from (
  with h (parent, child, isleaf, quantity, total_quantity) as (
    select 
      parent,
      child,
      isleaf,
      quantity,
      quantity total_quantity
    from
      itemhier
    where
      parent = 'ASSY001' 
    union all
    select
      ih.parent,
      ih.child,
      ih.isleaf,
      ih.quantity,
      ih.quantity * h.total_quantity total_quantity
    from
      itemhier ih
    join 
      h on h.child = ih.parent
  )
  select * from h
  where isleaf = 1
)
group by child;

这是 sqlfiddle:http://sqlfiddle.com/#!4/9840f/6

【讨论】:

  • 我错过了关于您在 Oracle 10g R2 上运行的部分。但是这里有一些很棒的建议:stackoverflow.com/a/4786672/537166
  • 这样效率更高,但我需要层次结构分支的数量,而不是层次结构中的总数量。这就是我使用 instr 函数的原因,我知道这很昂贵,但我想不出另一种方法来将聚合缩小到单个分支。
  • 不确定我是否关注过你。该解决方案递归地遍历层次结构的分支并计算分支的乘积。如果叶节点项仅与层次结构的一个分支相关联,则您将仅获得该分支的产品。使用您提供的样本数据,项目位于多个分支上,因此它汇总了项目所属的任何潜在分支的总数。但正如我上面提到的,它只适用于 Oracle 11g R2 或更高版本。
  • 如果您查看第一篇文章中查询的输出,它会为该项目所在的每个分支生成一行,并且数量列仅反映该分支中的数量。
  • 啊,好的。如果您运行内部查询(以“with h”开头并以“where isleaf = 1”结尾),您将获得每个分支对应的一行。
【解决方案2】:

您可以使用SYS_CONNECT_BY_PATH 生成表达式,即分支中所有数量的乘积。然后用一个函数动态执行那个表达式,得到最终的数量。

这不是一个理想的解决方案。 SQL 和 PL/SQL 之间的上下文切换需要一些时间。而且您需要担心 SQL 注入。但至少可以避免两次查询同一张表。

(正如 Dan A. 和 podiluska 所建议的那样,递归 CTE 很可能是最好的解决方案。根据我的经验,即使两种语法做同样的事情并使用相似的访问路径,递归 CTE 也可能显着比connect by 更快。但您需要等到升级到 11gR2。)

CREATE OR REPLACE FUNCTION EVALUATE_EXPRESSION(P_EXPRESSION IN VARCHAR2) RETURN NUMBER AS
    R_QTY  INTEGER;
BEGIN
    EXECUTE IMMEDIATE 'SELECT '||P_EXPRESSION||' FROM DUAL' INTO R_QTY;
    RETURN R_QTY;
END;
/


SELECT 'ASSY001' || SYS_CONNECT_BY_PATH(CHILD,'/') NAV_PATH,
      GETQTY(SYS_CONNECT_BY_PATH(CHILD,'/'), 'ASSY001') QTY,
      SUBSTR(SYS_CONNECT_BY_PATH(QUANTITY,'*'), 2) QTY_EXPRESSION,
      EVALUATE_EXPRESSION(SUBSTR(SYS_CONNECT_BY_PATH(QUANTITY,'*'), 2)) QTY2,
      CHILD
FROM ITEMHIER
WHERE ISLEAF = 1
START WITH PARENT = 'ASSY001'
CONNECT BY PRIOR CHILD = PARENT;

另外,您提到该表具有索引。但是查询是否使用索引?可以发一下解释计划吗?

最后,对于这么慢的查询,您可能需要研究并行性。不幸的是,我在使用并行性和connect by 时从来没有运气。

【讨论】:

  • 我认为这就是答案,它要快得多。我们正计划进行 11g 升级,但仍处于初期阶段。升级完成后我会重新访问,但在此之前我当然可以使用它。
【解决方案3】:

您应该查看WITH 语句和公用表表达式/子查询分解,这将允许您在单个 SQL 语句中遍历您的层次结构。它可能也会更快。

例如:

查找“assy002”的所有叶子

with cte as
(
    select * from #ITEMHIER
    union all
    select i.PARENT, cte.CHILD, cte.QUANTITY, cte.ISLEAF
    from #ITEMHIER i
        inner join cte on i.CHILD = cte.PARENT
)
    select CHILD,QUANTITY, isleaf from cte
    where PARENT='assy002'
    and isleaf=1;

【讨论】:

  • 使用 START WITH 不会得到相同的结果吗? SELECT QUANTITY, CHILD FROM ITEMHIER WHERE ISLEAF = 1 START WITH PARENT = 'ASSY002' CONNECT BY PRIOR CHILD = PARENT我的挑战是获得数量的乘积,以沿着层次结构向下走一条路径。
  • 不确定。我的 oracle 语法充其量是粗制滥造的 :)
  • 感谢使用 with 子句的建议,我将尝试使用它,看看是否可以让它只执行一个分层查询。这应该可以节省一些时间......
猜你喜欢
  • 2021-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-17
  • 2015-10-15
  • 2020-03-14
相关资源
最近更新 更多