【问题标题】:Is there an aggregate function that could return first non-null value within a group?是否有一个聚合函数可以返回组内的第一个非空值?
【发布时间】:2011-12-09 00:10:04
【问题描述】:

我正在使用 Oracle XE 10g。

请您仔细阅读我的问题。我有一个奇怪的用例,但请耐心等待。

假设我有以下记录:

Table person
Name  YearOfBirth
a     null
a     2001
a     2002
b     1990
b     null
c     null
c     2001
c     2009

基本上,如果我执行以下查询:

select
  p.Name, max(p.YearOfBirth)
from
  person p
group by
  p.Name

这将为我提供具有不同名称的记录,并且每个不同的名称将与其组中 YearOfBirth 的最大值配对。在给定示例中,Name='a' 的组中,YearOfBirth 的最大值为 2002。

如果 max() 是返回给定组中列的最大值的聚合函数,是否有返回组中的第一个值不为空的函数? 我想要的不是最大值,而是你能找到的第一个值,只要它不为空即可。

请不要问我为什么不能简单地使用 min() 或 max()。

显然我不能像某些人建议的那样在这里使用 rownum,因为这样做会限制我可以获得的组数。

【问题讨论】:

  • 如何定义“第一”?除非您的表是 IOT(索引组织表)或您正在处理从带有“ORDER BY”的 SELECT 返回的行,否则表中的行没有定义的顺序。
  • 请先定义。表中的数据是无序的,返回结果的顺序可能随时改变。仅当可以根据数据进行定义时,首先的概念才有意义。
  • @George3:即使在 IOT 中,也没有定义的顺序,并且有可能返回不按主键顺序排列的结果,尤其是在快速全扫描主键索引的情况下被执行。见:asktom.oracle.com/pls/apex/…
  • @Shannon Severance - 在 IOT 中没有为 retrieval 定义的顺序,仅按主键定义的逻辑存储顺序排列。
  • @Shannon 是的,我知道没有检索“第一”行的“规范”是没有意义的,或者没有明确定义“第一”是没有意义的”。但这就是重点,解决方案本身应该没有获得第一个的基础。这正是“规范”。我知道这没有意义,但到底是什么,这是一个很长的故事。以前从未有过这种用例。

标签: sql database oracle plsql oracle10g


【解决方案1】:

我可能误解了为什么 ROW NUMBER 不适合您。我没有 Oracle,但我在 SQL Server 中对此进行了测试,我相信它提供了您要求的结果:

WITH soTable AS
(
   SELECT 'a' AS Name, null AS YearOfBirth
   UNION ALL SELECT 'a', 2001
   UNION ALL SELECT 'a', 2002
   UNION ALL SELECT 'b', 1990
   UNION ALL SELECT 'b', null
   UNION ALL SELECT 'b', 1994
   UNION ALL SELECT 'b', 1981
   UNION ALL SELECT 'c', null
   UNION ALL SELECT 'c', 2009
   UNION ALL SELECT 'c', 2001
)
, soTableNoNulls AS
(
   SELECT so.Name, so.YearOfBirth, ROW_NUMBER() OVER (PARTITION BY so.Name ORDER BY so.Name ASC) AS RowNumber
   FROM soTable AS so
   WHERE so.YearOfBirth IS NOT NULL
)
SELECT nn.Name, nn.YearOfBirth
FROM soTableNoNulls AS nn
WHERE nn.RowNumber = 1

【讨论】:

  • 我在这里假设存在主键驱动顺序,因此“第一”记录将是一致的。
  • 您似乎没有使用 soTableNoNulls 中的 RowNumber 列。如果不需要,最好删除。我认为您可以将其减少到一个 CTE 而不是两个。 (不计算 CTE 和测试数据。)(CTE = 公共表表达式,在 Oracle 中通常称为子查询因子。)
  • 谢谢,注意到我发布答案后为时已晚。现在已删除。
  • 太棒了!我不知道“分区依据”是如何工作的,但你让它工作了。谢谢亚当。顺便说一句,没有主键。有可能获得多个具有相同名称和相同 YearOfBirths 的记录。会不会有问题?
  • Brent Ozar 写了一篇关于 PARTITION BY 如何在 ROW_NUMBER 中工作的好帖子(他的帖子也包含其他聚合函数的信息)brentozar.com/archive/2011/07/leaving-windows-open
【解决方案2】:

如果“第一”是指出生年份最低的记录,则可以执行以下操作:

WITH s1 AS
(
   SELECT 'a' AS name, NULL AS birth_year FROM dual
   UNION ALL SELECT 'a', 2001 FROM dual
   UNION ALL SELECT 'a', 2002 FROM dual
   UNION ALL SELECT 'b', 1990 FROM dual
   UNION ALL SELECT 'b', null FROM dual
   UNION ALL SELECT 'b', 1994 FROM dual
   UNION ALL SELECT 'b', 1981 FROM dual
   UNION ALL SELECT 'c', null FROM dual
   UNION ALL SELECT 'c', 2009 FROM dual
   UNION ALL SELECT 'c', 2001 FROM dual
)
SELECT name, birth_year FROM (
    SELECT name, birth_year
         , FIRST_VALUE(birth_year IGNORE NULLS) OVER ( PARTITION BY name ORDER BY birth_year ) AS first_birth_year
      FROM s1
) WHERE birth_year = first_birth_year

使用FIRST_VALUE() 优于ROW_NUMBER() 的优点是前者将在出现平局时返回多行。例如,如果您的数据中有另一个 2001 年出生的 a,那么生成的数据将如下所示:

NAME  BIRTH_YEAR
a     2001
a     2001
b     1981
c     2001

ROW_NUMBER() 解决方案将仅返回上述行之一。但是,这也可以通过使用RANK() 来解决。

如果有其他定义“第一”的方式(例如,输入日期列),只需在FIRST_VALUE()ORDER BY 子句中使用它。

【讨论】:

  • 仅供参考,以供任何寻找 T-SQL 等效项的人受益,此解决方案也适用于 SQL Server - 即使其 FIRST_VALUE 缺少 IGNORE NULLS 子句。如果其他值都为空,您可以简单地按列 DESC 排序。这有助于避免在数据透视查询中出现很多尴尬的连接,因为我现在正在第一手发现(感谢您的解决方案)
【解决方案3】:

这是解决方案:

CREATE OR REPLACE FUNCTION first_agg ( anyelement, anyelement )
RETURNS anyelement AS
$$
    SELECT $1;
$$
LANGUAGE SQL
IMMUTABLE
;

然后:

CREATE AGGREGATE first (
        sfunc    = first_agg,
        basetype = anyelement,
        stype    = anyelement
);

测试一下:

select first((case when a = 1 then null else a end) ORDER BY a NULLS FIRST) from generate_series(1, 100) a; -- => "2"

【讨论】:

    猜你喜欢
    • 2017-09-02
    • 2015-09-13
    • 1970-01-01
    • 2017-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多