2010-03-20 39 views
28

Si tengo una tabla como la siguiente:Oracle: ¿cómo "agrupar por" en un rango?

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

puedo "grupo por" para obtener un recuento de cada edad.

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

¿Qué consulta puedo usar para agrupar por rangos de edad?

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

Estoy en 10gR2, pero también me interesaría cualquier enfoque específico de 11g.

Respuesta

50
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 
+0

Esta debería ser la primera y única respuesta a esta pregunta. Sin embargo, podría usar un poco más de formato. – jva

+2

No, las sentencias CASE usan evaluación de circo corto – Einstein

+0

¿Cómo podría la evaluación de circo corto causar un problema en esta consulta? Como los casos están ordenados y usan <=, el grupo correcto siempre se selecciona. ¿No es así? – Adrian

23

Probar:

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

uso inteligente de piso/división! – mpen

+1

Este enfoque es mejor cuando tenemos un patrón definido y los grupos se pueden calcular a través de una expresión. No es necesario mencionar explícitamente los grupos en la consulta y, por lo tanto, podrá proporcionar nuevos grupos sin modificar la consulta .... –

+1

Esto no funciona, resulta en ** error ORA-00979: no es una expresión GROUP BY * * porque 'ceil (age/10)' falta en la expresión GROUP BY. Pero la dirección de este enfoque es mejor que @NitinMidha, así que estoy votando esta respuesta. – Wintermute

1

añadir una tabla AGE_RANGE y un campo age_range_id a su mesa y grupo por ese lugar.

// excusa el DDL, sino que debe obtener la idea

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

// excusa el nuevo LMD pero debe obtener la idea

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; 

Puede refinar esta idea si se quiere - agregue from_age to_age columnas en la tabla age_range, etc., pero se lo dejo a usted.

esperanza de que esto ayude :)

+0

A juzgar por las otras respuestas, el rendimiento y la flexibilidad no son criterios importantes. Los planes de explicación para todas las consultas dinámicas enumeradas serían terribles y tendrías que modificar el código si cambia tu rango de edad. Cada uno por su cuenta, supongo: P –

+0

1 escaneo completo siempre será más rápido que 2 escaneos completos. Además, las personas que solicitan estadísticas de rango de edad probablemente hayan tenido los mismos rangos durante los últimos 20 años y no tengan intenciones de cambiar eso. – jva

+1

Estoy bastante seguro de que la columna física superará a una derivada/calculada. De hecho, es probable que sea un candidato ideal para un índice de mapa de bits. Aún prefiero usar una tabla de búsqueda que para codificar valores en mis aplicaciones. Agregar un nuevo rango de edad dice 14-16 años y estoy insertando una nueva fila frente a una solicitud de cambio, pasando tiempo codificando y probando los cambios y lanzando en prod. –

3

Aquí es una solución que crea una tabla de "gama" en un sub-consulta y luego lo utiliza para dividir los datos de la tabla principal:

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

Si se utiliza Oracle 9i +, que puede poder utilizar el 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 

La advertencia para NTILE es que solo puede especificar el número de las particiones, no los puntos de corte en sí. Por lo tanto, debe especificar un número que sea apropiado. IE: con 100 filas, NTILE(4) asignará 25 filas a cada uno de los cuatro cubos/particiones. No puede anidar las funciones analíticas, por lo que deberá superponerlas mediante subconsultas/factorización de subconsulta para obtener la granularidad deseada. De lo contrario, use:

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 
2

Tuve que agrupar los datos por la cantidad de transacciones que aparecieron en una hora. Hice esto por extracción de la hora de la marca de tiempo:

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 
; 

salida de Dar:

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

Como se puede ver esto da una manera fácil agradable de agrupar el número de registros por hora.

1

Tenía que obtener un recuento de muestras por día. Inspirado por @Clarkey, utilicé TO_CHAR para extraer la fecha de muestra de la marca de tiempo en un formato de fecha ISO-8601 y la utilicé en las cláusulas GROUP BY y ORDER BY. (Inspirado más, también lo publico aquí en caso de que sea útil para otros).)

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 
/
7

Lo que se busca, es decir, básicamente, los datos para un histogram.

Tendría la edad (o rango de edad) en el eje xy la cuenta n (o frecuencia) en el eje y.

En la forma más simple, se podría simplemente contar el número de cada valor edad distinta como si ya descritos:

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

Cuando hay demasiados diferentes valores para el eje x sin embargo, uno puede querer crear grupos (o clústeres o cubos). En su caso, usted agrupa en un rango constante de 10.

Podemos evitar escribir una línea WHEN ... THEN para cada rango - podría haber cientos si no fuera por la edad. En cambio, el enfoque de @MatthewFlaschen es preferible por las razones mencionadas por @NitinMidha.

Ahora vamos a construir el SQL ...

En primer lugar, tenemos que dividir las edades dentro del rango-grupos de 10, así:

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

Esto puede lograrse mediante la div Iding la columna de la edad de 10 y luego calcular el suelo del resultado:

FLOOR(age/10) 

"FLOOR devuelve el mayor entero igual o menor que n" http://docs.oracle.com/cd/E11882_01/server.112/e26088/functions067.htm#SQLRF00643

Luego tomamos el SQL original y sustituir edad con esa expresión:

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

Esto está bien, pero no podemos ver la gama, sin embargo. En cambio, solo vemos los valores de piso calculados que son 0, 1, 2 ... n.

Para obtener el actual límite inferior, necesitamos multiplicar con 10 de nuevo por lo que tenemos 0, 10, 20 ... n:

FLOOR(age/10) * 10 

también necesitamos que el límite superior de cada intervalo que es inferior obligado + 10 - 1 o

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

por último, se concatena tanto en una cadena como esta:

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

Esto crea '0-9', '10-19', '20-29' etc.

Ahora nuestro SQL tiene este aspecto:

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

Finalmente, se aplica un orden y alias de columna bonito:

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) 

Sin embargo, en escenarios más complejos, estos rangos podrían no ser agrupados en constante trozos de tamaño 10, pero necesitan clustering dinámico. Oracle tiene funciones de histograma más avanzadas incluidas, consulte http://docs.oracle.com/cd/E16655_01/server.121/e15858/tgsql_histo.htm#TGSQL366

Créditos a @MatthewFlaschen por su enfoque; Solo expliqué los detalles.

0

Mi enfoque:

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 
  • que tienen flexibilidad para definir los rangos
  • no repetir los rangos en algunos y de grupo cláusulas
  • pero alguien por favor dígame, cómo realizar un pedido ellos por magnitud!