Oracle:如何在一个范围内进行“group by”聚合?

36

如果我有一个这样的表:

pkey   age
----   ---
   1     8
   2     5
   3    12
   4    12
   5    22
我可以使用“group by”来按年龄分组并得到每个年龄的计数。
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特有的方法感兴趣。

10个回答

66
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

这应该是这个问题的第一个并且唯一的答案。不过可以加点格式。 - jva
2
不,CASE语句使用短路评估。 - Einstein
短路求值如何在此查询中引起问题?因为情况被排序并使用<=,所以总是选择正确的组。不是吗? - Adrian
1
Adrian,你是正确的,这是回复之前已被删除的评论。 - Einstein
1
有没有一种方法可以包含没有行的范围。比如说,如果20岁以上的人没有人,查询返回(20+, 0)这一行吗? - dcarneiro

29

尝试:

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);

4
巧妙运用地板除法! - mpen
1
当我们有一个明确定义的模式并且可以通过表达式计算出组时,这种方法更好。它不需要在查询中明确提到组,因此将能够提供新的组而无需修改查询... - Nitin Midha
2
这个不起作用,会导致 错误ORA-00979: 不是GROUP BY表达式,因为 ceil(age/10) 在GROUP BY表达式中缺失。但是这种方法的方向更好,正如@NitinMidha所写的那样,所以我投票支持这个答案。 - Wintermute

14
你需要的是基本上用于直方图数据。
你会在x轴上有年龄(或年龄范围),在y轴上有计数n(或频率)。
简单来说,你可以像你已经描述的那样,只需计算每个不同年龄值的数量。
SELECT age, count(*)
FROM tbl
GROUP BY age
当x轴上存在太多不同的值时,我们可能希望创建分组(或聚类或桶)。在您的情况下,您按常量范围为10进行分组。
我们可以避免为每个范围编写一个WHEN ... THEN行-如果不是年龄,则可能有数百个。相反,@MatthewFlaschen的方法因@NitinMidha提到的原因而更可取。
现在让我们构建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

我们还需要每个范围的上限,即下限加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提供的方法; 我只解释了细节。


3
这里有一个解决方案,它在子查询中创建了一个“范围”表,然后使用该表将主表的数据进行分区:
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;

2

我需要按照每小时出现的交易次数对数据进行分组。我通过从时间戳中提取小时来实现这一点:

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

正如您所看到的,这提供了一种方便易行的方法来按小时分组记录数量。


1
请在您的表中添加一个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;

如果您愿意,可以完善这个想法 - 在年龄范围表中添加 from_age 和 to_age 列等等 - 但我会把这个留给您。

希望这有所帮助 :)


根据其他回复,性能和灵活性似乎不是重要的标准。列出的所有动态查询的解释计划都会很糟糕,如果年龄范围发生变化,您将不得不修改代码。每个人有自己的想法 :P - Jon Black
一次完整的扫描总是比两次完整的扫描更快。而且,询问年龄范围统计数据的人可能在过去20多年里一直有相同的范围,并且没有改变的意图。 - jva
1
我相信物理列会比派生/计算列表现更好。实际上,它可能是位图索引的理想候选。我仍然更喜欢使用查找表而不是将值硬编码到我的应用程序中。例如,添加一个新的年龄范围,比如14-16岁,我只需插入一行新记录,而不是提出变更请求、花费时间编写和测试更改并发布到生产环境。 - Jon Black

1
如果使用Oracle 9i+,您可能可以使用NTILE分析函数
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的注意事项是,您只能指定分区的数量,而不能指定断点本身。因此,您需要指定一个适当的数字。例如:对于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

1
你可以尝试以下解决方案:
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 

1

我需要按天计算样本数量。受@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
/

0

我的方法:

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  
  • 我在定义范围方面有灵活性
  • 我不会在选择和分组子句中重复范围
  • 但请问有人能告诉我如何按大小顺序排序吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接