【问题标题】:Oracle: how to "group by" over a range?Oracle:如何在一个范围内“分组”?
【发布时间】:2011-01-29 19:13:42
【问题描述】:

如果我有这样的表:

pkey   age
----   ---
   1     8
   2     5
   3    12
   4    12
   5    22

我可以“分组”来计算每个年龄。

select age,count(*) n from tbl group by age;
age  n
---  -
  5  1
  8  1
 12  2
 22  1

我可以使用什么查询来按年龄范围分组?

  age  n
-----  -
 1-10  2
11-20  2
20+    1

我使用的是 10gR2,但我也对任何特定于 11g 的方法感兴趣。

【问题讨论】:

    标签: sql oracle plsql oracle10g


    【解决方案1】:
    SELECT CASE 
             WHEN age <= 10 THEN '1-10' 
             WHEN age <= 20 THEN '11-20' 
             ELSE '21+' 
           END AS age, 
           COUNT(*) AS n
    FROM age
    GROUP BY CASE 
               WHEN age <= 10 THEN '1-10' 
               WHEN age <= 20 THEN '11-20' 
               ELSE '21+' 
             END
    

    【讨论】:

    • 这应该是这个问题的第一个也是唯一一个答案。不过可以使用更多格式。
    • 不,CASE 语句使用短路评估
    • 短路评估会如何导致此查询出现问题?因为案例是有序的并使用
    • 阿德里安,你说得对,这是对先前已被删除的评论的回复。
    • 有没有办法包含一个没有行的范围。示例:如果没有人超过 20,则查询返回一行 (20+, 0)?
    【解决方案2】:

    试试:

    select to_char(floor(age/10) * 10) || '-' 
    || to_char(ceil(age/10) * 10 - 1)) as age, 
    count(*) as n from tbl group by floor(age/10);
    

    【讨论】:

    • 地板/除法的巧妙用法!
    • 当我们有定义的模式并且可以通过表达式计算组时,这种方法会更好。它不需要在查询中明确提及组,因此可以在不修改查询的情况下提供新组 ....
    • 这不起作用,它会导致 错误 ORA-00979: not a GROUP BY 表达式,因为 GROUP BY 表达式中缺少 ceil(age/10)。但是正如@NitinMidha 所写,这种方法的方向更好,所以我投票赞成这个答案。
    【解决方案3】:

    您要查找的基本上是histogram 的数据。

    x 轴为年龄(或年龄范围),y 轴为计数 n(或频率)。

    在最简单的形式中,可以像您已经描述的那样简单地计算每个不同年龄值的数量:

    SELECT age, count(*)
    FROM tbl
    GROUP BY age
    

    但是,当 x 轴有太多不同的值时,可能需要创建组(或集群或存储桶)。在您的情况下,您按 10 的恒定范围分组。

    我们可以避免为每个范围编写 WHEN ... THEN 行 - 如果不是关于年龄,可能会有数百个。相反,由于@NitinMidha 提到的原因,@MatthewFlaschen 的方法更可取。

    现在让我们构建 SQL...

    首先,我们需要将年龄分成 10 个范围组,如下所示:

    • 0-9
    • 10-19
    • 20 - 29

    这可以通过将年龄列除以10然后计算结果的FLOOR来实现:

    FLOOR(age/10)
    

    "FLOOR 返回等于或小于 n 的最大整数" http://docs.oracle.com/cd/E11882_01/server.112/e26088/functions067.htm#SQLRF00643

    然后我们使用原始 SQL 并将 age 替换为该表达式:

    SELECT FLOOR(age/10), count(*)
    FROM tbl
    GROUP BY FLOOR(age/10)
    

    这没关系,但我们还看不到范围。相反,我们只看到计算出的底值0, 1, 2 ... n

    要获得实际的下限,我们需要再次将其乘以 10,从而得到0, 10, 20 ... n

    FLOOR(age/10) * 10
    

    我们还需要每个范围的上限 bound + 10 - 1

    FLOOR(age/10) * 10 + 10 - 1
    

    最后,我们将两者连接成这样的字符串:

    TO_CHAR(FLOOR(age/10) * 10) || '-' || TO_CHAR(FLOOR(age/10) * 10 + 10 - 1)
    

    这会创建'0-9', '10-19', '20-29' 等。

    现在我们的 SQL 看起来像这样:

    SELECT 
    TO_CHAR(FLOOR(age/10) * 10) || ' - ' || TO_CHAR(FLOOR(age/10) * 10 + 10 - 1),
    COUNT(*)
    FROM tbl
    GROUP BY FLOOR(age/10)
    

    最后,应用一个顺序和漂亮的列别名:

    SELECT 
    TO_CHAR(FLOOR(age/10) * 10) || ' - ' || TO_CHAR(FLOOR(age/10) * 10 + 10 - 1) AS range,
    COUNT(*) AS frequency
    FROM tbl
    GROUP BY FLOOR(age/10)
    ORDER BY FLOOR(age/10)
    

    但是,在更复杂的场景中,这些范围可能不会被分组为大小为 10 的恒定块,但需要动态聚类。 Oracle 包含更高级的直方图函数,请参阅http://docs.oracle.com/cd/E16655_01/server.121/e15858/tgsql_histo.htm#TGSQL366

    感谢@MatthewFlaschen 的方法;我只解释了细节。

    【讨论】:

      【解决方案4】:

      这是一个解决方案,它在子查询中创建一个“范围”表,然后使用它来对主表中的数据进行分区:

      SELECT DISTINCT descr
        , COUNT(*) OVER (PARTITION BY descr) n
      FROM age_table INNER JOIN (
        select '1-10' descr, 1 rng_start, 10 rng_stop from dual
        union (
        select '11-20', 11, 20 from dual
        ) union (
        select '20+', 21, null from dual
      )) ON age BETWEEN nvl(rng_start, age) AND nvl(rng_stop, age)
      ORDER BY descr;
      

      【讨论】:

        【解决方案5】:

        我必须按照一小时内出现的交易数量对数据进行分组。我通过从时间戳中提取小时来做到这一点:

        select extract(hour from transaction_time) as hour
              ,count(*)
        from   table
        where  transaction_date='01-jan-2000'
        group by
               extract(hour from transaction_time)
        order by
               extract(hour from transaction_time) asc
        ;
        

        给出输出:

        HOUR COUNT(*)
        ---- --------
           1     9199 
           2     9167 
           3     9997 
           4     7218
        

        如您所见,这提供了一种非常简单的方法来对每小时的记录数进行分组。

        【讨论】:

          【解决方案6】:

          将一个 age_range 表和一个 age_range_id 字段添加到您的表中,然后按此分组。

          // 原谅 DDL 但你应该明白了

          create table age_range(
          age_range_id tinyint unsigned not null primary key,
          name varchar(255) not null);
          
          insert into age_range values 
          (1, '18-24'),(2, '25-34'),(3, '35-44'),(4, '45-54'),(5, '55-64');
          

          // 再次原谅 DML,但你应该明白了

          select
           count(*) as counter, p.age_range_id, ar.name
          from
            person p
          inner join age_range ar on p.age_range_id = ar.age_range_id
          group by
            p.age_range_id, ar.name order by counter desc;
          

          如果您愿意,您可以改进这个想法 - 在 age_range 表中添加 from_age to_age 列等 - 但我会把它留给您。

          希望这会有所帮助:)

          【讨论】:

          • 从其他回答来看,性能和灵活性并不是重要的标准。列出的所有动态查询的解释计划将是可怕的,如果您的年龄范围发生变化,您将不得不修改代码。我猜每个人都有自己的:P
          • 1 次完整扫描总是比 2 次完整扫描快。此外,询问年龄范围统计数据的人可能在过去 20 多年中拥有相同的范围,并且无意改变这一点。
          • 我很确定物理列将执行派生/计算的列。事实上,它可能是位图索引的理想候选者。我仍然更喜欢使用查找表,而不是将值硬编码到我的应用程序中。添加一个新的年龄范围,比如 14-16 岁,我插入一个新行与提出更改请求,花时间编码和测试更改并发布到产品中。
          【解决方案7】:

          如果使用 Oracle 9i+,您可能可以使用NTILE analytic function

          WITH tiles AS (
            SELECT t.age,
                   NTILE(3) OVER (ORDER BY t.age) AS tile
              FROM TABLE t)
            SELECT MIN(t.age) AS min_age,
                   MAX(t.age) AS max_age,
                   COUNT(t.tile) As n
              FROM tiles t
          GROUP BY t.tile
          

          对 NTILE 的警告是您只能指定分区数,而不是断点本身。所以你需要指定一个合适的数字。 IE:如果有 100 行,NTILE(4) 将为四个存储桶/分区中的每一个分配 25 行。您不能嵌套分析函数,因此您必须使用子查询/子查询分解将它们分层以获得所需的粒度。否则,使用:

            SELECT CASE t.age
                     WHEN BETWEEN 1 AND 10 THEN '1-10' 
                     WHEN BETWEEN 11 AND 20 THEN '11-20' 
                     ELSE '21+' 
                   END AS age, 
                   COUNT(*) AS n
              FROM TABLE t
          GROUP BY CASE t.age
                     WHEN BETWEEN 1 AND 10 THEN '1-10' 
                     WHEN BETWEEN 11 AND 20 THEN '11-20' 
                     ELSE '21+' 
                   END
          

          【讨论】:

            【解决方案8】:

            我必须按天计算样本数量。受@Clarkey 启发,我使用 TO_CHAR 将样本日期从时间戳提取为 ISO-8601 日期格式,并在 GROUP BY 和 ORDER BY 子句中使用。 (进一步的启发,我也把它贴在这里,以防它对其他人有用。)

            SELECT 
              TO_CHAR(X.TS_TIMESTAMP, 'YYYY-MM-DD') AS TS_DAY, 
              COUNT(*) 
            FROM   
              TABLE X
            GROUP BY
              TO_CHAR(X.TS_TIMESTAMP, 'YYYY-MM-DD')
            ORDER BY
              TO_CHAR(X.TS_TIMESTAMP, 'YYYY-MM-DD') ASC
            /
            

            【讨论】:

              【解决方案9】:

              您可以尝试以下解决方案吗:

              SELECT count (1), '1-10'  where age between 1 and 10
              union all 
              SELECT count (1), '11-20'  where age between 11 and 20
              union all
              select count (1), '21+' where age >20
              from age 
              

              【讨论】:

                【解决方案10】:

                我的做法:

                select range, count(1) from (
                select case 
                  when age < 5 then '0-4' 
                  when age < 10 then '5-9' 
                  when age < 15 then '10-14' 
                  when age < 20 then '15-20' 
                  when age < 30 then '21-30' 
                  when age < 40 then '31-40' 
                  when age < 50 then '41-50' 
                  else                '51+' 
                end 
                as range from
                (select round(extract(day from feedback_update_time - feedback_time), 1) as age
                from txn_history
                ) ) group by range  
                
                • 我可以灵活地定义范围
                • 我不会重复 select 和 group 子句中的范围
                • 但是请有人告诉我,如何按大小排序!

                【讨论】:

                  猜你喜欢
                  • 2011-08-05
                  • 2010-09-18
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多