【问题标题】:4:**Count/sum rows in multiple related tables4:**Count/sum 多个相关表中的行
【发布时间】:2014-05-14 18:33:58
【问题描述】:

我有一个复杂的选择 - 简化后 - 看起来像这样:

select m.ID,
  (select sum(AMOUNT) from A where M_ID = m.ID) sumA,
  (select sum(AMOUNT) from B where M_ID = m.ID) sumB,
  .....
from M;

表 A,B,... 有一个外键 M_ID 指向表 M。 问题是这个选择非常慢。我想用表连接重写它,但我不知道怎么做,因为

select m.ID
  sum(a.AMOUNT),
  sum(b.AMOUNT),
  .....
from M
join A on a.M_ID = m.ID
join B on b.M_ID = m.ID
....
group by m.ID;

给出不正确(更高)的总和结果,因为 A 或 B 中的每一行都可以计算多次。

有没有办法如何使用例如最佳方式编写该选择?分析函数还是其他方法?

编辑: 原始(未简化)选择的解释计划如下所示:

|   0 | SELECT STATEMENT              |                           |
|   1 |  SORT AGGREGATE               |                           |
|*  2 |   FILTER                      |                           |
|*  3 |    TABLE ACCESS BY INDEX ROWID| WORKITEM                  |
|*  4 |     INDEX SKIP SCAN           | WORKITEM_U01              |
|*  5 |    FILTER                     |                           |
|*  6 |     TABLE ACCESS FULL         | RPRODUCT_INVENTORY_MASTER |
.....
|  31 |  SORT AGGREGATE               |                           |
|* 32 |   FILTER                      |                           |
|* 33 |    TABLE ACCESS BY INDEX ROWID| WORKITEM                  |
|* 34 |     INDEX SKIP SCAN           | WORKITEM_U01              |
|* 35 |    FILTER                     |                           |
|* 36 |     TABLE ACCESS FULL         | RPRODUCT_INVENTORY_MASTER |
|  37 |  SORT GROUP BY                |                           |
|  38 |   TABLE ACCESS FULL           | RPRODUCT                  |

这就是我想要优化它的原因。此外,AWR 报告显示此选择有 50000 次获取/执行。

编辑2,3: 整个选择看起来像这样:

SELECT rprd.ID,
  rprd.NAME,
  (select sum(AMOUNT) from WORKITEM
    where ACTION='REMOVE'
      and trunc(CREATED_DATE) = to_date(:1,'DDMMYYYY')
      and PAYEE_ID in
          (select rim.RPRODUCT_ID from RPRODUCT_INVENTORY_MASTER rim
            where  rprd.ID = rim.RPRODUCT_ID
              and rim.INVENTORY_DATE = to_date(:2,'DDMMYYYY')),
  .....
  (select sum(AMOUNT) from WORKITEM
    where ACTION='COLLECT'
      and trunc(CREATED_DATE) < to_date(:11,'DDMMYYYY')
      and PAYEE_ID in
          (select rim.RPRODUCT_ID from RPRODUCT_INVENTORY_MASTER rim
            where  rprd.ID = rim.RPRODUCT_ID
              and rim.INVENTORY_DATE < to_date(:12,'DDMMYYYY'))
FROM RPRODUCT rprd 
GROUP BY rprd.ID, rprd.NAME 
ORDER BY rprd.ID
;

不是我写的 :-),我要重新写了。请注意,比较运算符、ACTION 值和用于比较 INVENTORY_DATE 的日期存在差异。

编辑4: 我尝试像这样重写查询(并且执行计划看起来更好),但遇到了上述“行多重性”问题:

with RPRODUCT_INVENTORY_MASTER# as (
    select RPRODUCT_ID, min(INVENTORY_DATE) INVENTORY_DATE
      from RPRODUCT_INVENTORY_MASTER
      group by RPRODUCT_ID),
  WORKITEM# as (
    select AMOUNT, PAYEE_ID, ACTION, trunc(CREATED_DATE) CREATED_DATE
      from WORKITEM
      where ACTION in ('REMOVE','ADD','COLLECT')
  )
select rprd.ID,
  rprd.NAME,
--  sum(wip2.AMOUNT), -- this is singular because of '=' in inventory_date comparison
  sum(abs(wip4.AMOUNT)),
  .....
  sum(wip12.AMOUNT)
from RPRODUCT rprd
left join RPRODUCT_INVENTORY_MASTER# rim4 on rim4.RPRODUCT_ID = rprd.ID 
  and rim4.INVENTORY_DATE <= to_date(:4 ,'DDMMYYYY')
left join WORKITEM# wip4 on wip4.PAYEE_ID = rim4.RPRODUCT_ID
  and wip4.ACTION='REMOVE'
  and wip4.CREATED_DATE = to_date(:3 ,'DDMMYYYY')
.....
left join RPRODUCT_INVENTORY_MASTER# rim12 on rim12.RPRODUCT_ID = rprd.ID 
  and rim12.INVENTORY_DATE < to_date(:12 ,'DDMMYYYY')
left join WORKITEM# wip12 on wip12.PAYEE_ID = rim12.RPRODUCT_ID
  and wip12.ACTION='COLLECT'
  and wip12.CREATED_DATE < to_date(:11 ,'DDMMYYYY')
group by rprd.ID, rprd.NAME 
order by rprd.ID
;

RPRODUCT_INVENTORY_MASTER# 总是为每个 rprd.ID 提供最多一行。对于每个 RPRODUCT_ID = rprd.ID,WORKITEM# 可以有任意数量的行。

【问题讨论】:

  • 在重写查询之前,也许您应该确保您拥有的查询已经过优化。你有查询计划吗?你检查索引了吗?这些桌子有多大?如果您拥有正确的索引和所有索引,我不太确定其他查询的性能会比这个更好。
  • 我在我原来的问题中添加了对您的(部分)问题的回答。我不希望索引对这种类型的查询有任何用处,我希望哈希连接。
  • 好的。该信息很有用,因为它向我们展示了查询比您最初建议的要复杂得多。但由于这是一个优化问题,我认为它不一定与查询的形式(JOINs vs subSELECTs)有关。只需对大表进行 1 或 2 次未索引表访问即可破坏查询性能。我建议您发布整个查询和整个解释计划。您可能只是遇到了一些复杂的 WHERE 或 subSELECTs 的问题。
  • 总的来说,你是对的。但是表 RPRODUCT (M) 目前大约有 100 行,跨越 5 个块,RPRODUCT_INVENTORY_MASTER 大约有 550 行和 5 个块,WORKITEM 有 4500 行和 88 个块。仅此一项并不能导致 50000 次获取/执行,这一定是由于执行计划不好。
  • 至于 EDIT4:我必须承认,我更喜欢原始查询。到目前为止,我知道优化器在连接上比在子查询上做得更好,但是我发现将所有这些东西连接在一起并最终将它们全部压缩以获得所需的结果并不是很容易理解。在您的情况下,您必须这样划分:四个表外部连接 a、b、c 和 d:nvl(sum(a.AMOUNT), 0) / best(count(distinct b.id), 1) / best (count(distinct c.id), 1) / 最大(count(distinct d.id), 1)。

标签: sql oracle11g


【解决方案1】:

是的,这是一个典型的问题。我喜欢你原来的查询,因为它很清楚。但是,如果在性能问题上运行,则必须考虑其他选项。

这是一种选择。当 A 和 B 相乘时,您可以简单地将总和除以相关计数。好吧,诚然,这看起来有点奇怪。

select m.ID
  sum(a.AMOUNT) / count(distinct b.id),
  sum(b.AMOUNT) / count(distinct a.id),
  .....
from M
join A on a.M_ID = m.ID
join B on b.M_ID = m.ID
....
group by m.ID;

我更喜欢的另一种选择是建立组,以免每个 m.id 首先有多个 A 和 B:

select m.ID
  a_agg.SUM_AMOUNT,
  b_agg.SUM_AMOUNT,
  .....
from M
join (select M_ID, sum(AMOUNT) as SUM_AMOUNT from A group by M_ID) a_agg
  on a_agg.M_ID = m.ID
join (select M_ID, sum(AMOUNT) as SUM_AMOUNT from B group by M_ID) b_agg
  on b_agg.M_ID = m.ID

编辑:如果 M_ID 可能没有任何 A 或任何 B,则必须在两个查询中将连接替换为 LEFT JOIN。然后在第一个查询中选择:

nvl(sum(a.AMOUNT), 0) / greatest(count(distinct b.id), 1),
nvl(sum(b.AMOUNT), 0) / greatest(count(distinct a.id), 1),

在第二个查询中:

nvl(a_agg.SUM_AMOUNT, 0),
nvl(b_agg.SUM_AMOUNT, 0),

编辑:这是您修改的查询。诀窍是加入不同的轮辋。

SELECT 
  rprd.ID,
  rprd.NAME,
  nvl(same_date.SUM_AMOUNT, 0),
  .....
  nvl(earlier_date.SUM_AMOUNT, 0)
FROM RPRODUCT rprd 
LEFT JOIN
(
  select rim.RPRODUCT_ID, sum(w.AMOUNT) as SUM_AMOUNT
  from
  (
    select distinct RPRODUCT_ID 
    from RPRODUCT_INVENTORY_MASTER
    where INVENTORY_DATE = to_date(:2,'DDMMYYYY')
  ) rim
  left join WORKITEM w 
    on w.PAYEE_ID = rim.RPRODUCT_ID
    and w.ACTION = 'REMOVE'
    and trunc(w.CREATED_DATE) = to_date(:1,'DDMMYYYY')
) same_date on same_date.RPRODUCT_ID = rprd.ID
LEFT JOIN
(
  select rim.RPRODUCT_ID, sum(w.AMOUNT) as SUM_AMOUNT
  from
  (
    select distinct RPRODUCT_ID 
    from RPRODUCT_INVENTORY_MASTER
    where INVENTORY_DATE < to_date(:12,'DDMMYYYY')
  ) rim
  left join WORKITEM w 
    on w.PAYEE_ID = rim.RPRODUCT_ID
    and w.ACTION = 'REMOVE'
    and trunc(w.CREATED_DATE) < to_date(:11,'DDMMYYYY')
) earlier_date on earlier_date.RPRODUCT_ID = rprd.ID
GROUP BY rprd.ID, rprd.NAME 
ORDER BY rprd.ID
;

【讨论】:

  • 也许我理解错了,但是 A 和 B 不会相乘,它们会相加。我看不出你的数学应该如何处理第一个。至于第二个,我想它的性能与 OP 完全相同,因为您只是将子选择移动到连接。
  • @paqogomez:乘以我的意思是,如果您为 m_id 获得三个 A 记录和四个 B 记录,那么您将获得 m_id 的 3 x 4 = 12 记录,这就是总和的原因也倍增。您将得到 sum = 4 x n,而不是表 A 的 sum = n。您必须除以 B 计数,即 4 才能得到实际想要的总和。至于第二个查询:这些扫描不是为每个 M 扫描 A 和 B,而是预先完成一次。诚然,Oracle 的优化器可能会注意到并为这两个查询制定相同的计划。
  • 我现在明白了,我没有注意到您将 b 与 a 分开,反之亦然。聪明点。
  • 谢谢 Thorsten,我更喜欢第一个选项,尽管我必须处理除以 0。无法应用第二个选项,子选择太复杂了 - 请参阅我原始帖子中的 Edit2 .此外,在第二个选项中,我会强制执行一个特定的执行计划,我更愿意将执行计划的决定留给优化器。
  • 我明白了。至于第一个问题:你是对的;如果 m_id 可能没有任何 A 或 B,那么您将需要外连接并将计数为零视为 1。关于第二个查询:嗯,我们正在讨论优化。尽管编写了一个很好的查询,但您仍面临性能问题,因此我们必须寻找重新编写查询的方法。说“我想将每个 M 与其 A 总和和 B 总和组合”对我来说听起来并不比“我想组合所有 M、A 和 B 以找到每个 M 的 A 和 B 总和”更糟糕。
【解决方案2】:

这应该可以工作

select m.ID,
a.aamount,
b.bamount
from M
inner join 
(
select M_ID,sum(AMOUNT) as aamount
from A group by M_ID
) a
on a.M_ID = m.ID
inner join 
(
select M_ID,sum(AMOUNT) as bamount
from B group by M_ID
) b
on b.M_ID = m.ID;

【讨论】:

  • 谢谢,但我不能这样做,子选择对于这种情况来说太复杂了。请参阅我原来的问题中的 Edit2。
【解决方案3】:

无论 A、B、C、... 表中的 m_id 行数如何,这都应该有效:

select
  M.id,
  sum(decode(u.src, 'A', u.sumx, 0)) sum_a,
  sum(decode(u.src, 'B', u.sumx, 0)) sum_b,
  sum(decode(u.src, 'C', u.sumx, 0)) sum_c,
  ...
from M,
  (select 'A' src, m_id, sum(amount) sumx from A group by m_id
  union all
  select 'B', m_id, sum(amount) from B group by m_id
  union all
  select 'C', m_id, sum(amount) from C group by m_id
  ...
  ) u
where
  M.id=u.m_id
group by
  M.id;

【讨论】:

  • 嗯,是的,但实际上这个选择会导致与原始选择几乎相同的执行计划,不是吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多