sql >> Base de Datos >  >> RDS >> Oracle

Consulta SQL para contraer valores duplicados por rango de fechas

Voy a desarrollar mi solución de forma incremental, descomponiendo cada transformación en una vista. Esto ayuda a explicar lo que se está haciendo y ayuda a depurar y probar. Básicamente, aplica el principio de descomposición funcional a las consultas de la base de datos.

También lo haré sin usar extensiones de Oracle, con SQL que debería ejecutarse en cualquier RBDMS moderno. Así que no hay mantenimiento, cambio, partición, solo subconsultas y bys de grupo. (Infórmeme en los comentarios si no funciona en su RDBMS).

Primero, la tabla, que como no soy creativo, llamaré valor_mes. Dado que la identificación no es en realidad una identificación única, la llamaré "eid". Las otras columnas son "mes", "año" y "valor":

create table month_value( 
   eid int not null, m int, y int,  v int );

Después de insertar los datos, para dos eid, tengo:

> select * from month_value;
+-----+------+------+------+
| eid | m    | y    | v    |
+-----+------+------+------+
| 100 |    1 | 2008 |   80 |
| 100 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |   80 |
| 200 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |   80 |
+-----+------+------+------+
8 rows in set (0.00 sec)

A continuación, tenemos una entidad, el mes, que se representa como dos variables. Eso realmente debería ser una columna (ya sea una fecha o una fecha y hora, o tal vez incluso una clave externa para una tabla de fechas), así que lo haremos una columna. Lo haremos como una transformación lineal, de modo que se clasifique igual que (y, m), y de tal manera que para cualquier tupla (y,m) haya un único valor, y todos los valores sean consecutivos:

> create view cm_abs_month as 
select *, y * 12 + m as am from month_value;

Eso nos da:

> select * from cm_abs_month;
+-----+------+------+------+-------+
| eid | m    | y    | v    | am    |
+-----+------+------+------+-------+
| 100 |    1 | 2008 |   80 | 24097 |
| 100 |    2 | 2008 |   80 | 24098 |
| 100 |    3 | 2008 |   90 | 24099 |
| 100 |    4 | 2008 |   80 | 24100 |
| 200 |    1 | 2008 |   80 | 24097 |
| 200 |    2 | 2008 |   80 | 24098 |
| 200 |    3 | 2008 |   90 | 24099 |
| 200 |    4 | 2008 |   80 | 24100 |
+-----+------+------+------+-------+
8 rows in set (0.00 sec)

Ahora usaremos una autounión en una subconsulta correlacionada para encontrar, para cada fila, el primer mes sucesor en el que cambia el valor. Basaremos esta vista en la vista anterior que creamos:

> create view cm_last_am as 
   select a.*, 
    ( select min(b.am) from cm_abs_month b 
      where b.eid = a.eid and b.am > a.am and b.v <> a.v) 
   as last_am 
   from cm_abs_month a;

> select * from cm_last_am;
+-----+------+------+------+-------+---------+
| eid | m    | y    | v    | am    | last_am |
+-----+------+------+------+-------+---------+
| 100 |    1 | 2008 |   80 | 24097 |   24099 |
| 100 |    2 | 2008 |   80 | 24098 |   24099 |
| 100 |    3 | 2008 |   90 | 24099 |   24100 |
| 100 |    4 | 2008 |   80 | 24100 |    NULL |
| 200 |    1 | 2008 |   80 | 24097 |   24099 |
| 200 |    2 | 2008 |   80 | 24098 |   24099 |
| 200 |    3 | 2008 |   90 | 24099 |   24100 |
| 200 |    4 | 2008 |   80 | 24100 |    NULL |
+-----+------+------+------+-------+---------+
8 rows in set (0.01 sec)

last_am es ahora el "mes absoluto" del primer mes (el más antiguo) (después del mes de la fila actual) en el que cambia el valor, v. Es nulo donde no hay un mes posterior, para ese eid, en la tabla.

Dado que last_am es el mismo para todos los meses previos al cambio en v (que ocurre en last_am), podemos agrupar en last_am y v (y eid, por supuesto), y en cualquier grupo, el min(am) es el absoluto mes del primero mes consecutivo que tuvo ese valor:

> create view cm_result_data as 
  select eid, min(am) as am , last_am, v 
  from cm_last_am group by eid, last_am, v;

> select * from cm_result_data;
+-----+-------+---------+------+
| eid | am    | last_am | v    |
+-----+-------+---------+------+
| 100 | 24100 |    NULL |   80 |
| 100 | 24097 |   24099 |   80 |
| 100 | 24099 |   24100 |   90 |
| 200 | 24100 |    NULL |   80 |
| 200 | 24097 |   24099 |   80 |
| 200 | 24099 |   24100 |   90 |
+-----+-------+---------+------+
6 rows in set (0.00 sec)

Ahora bien, este es el conjunto de resultados que queremos, por lo que esta vista se llama cm_result_data. Todo lo que falta es algo para transformar los meses absolutos en tuplas (a, m).

Para hacer eso, simplemente nos uniremos a la tabla month_value.

Solo hay dos problemas:1) queremos el mes anterior last_am en nuestra salida, y 2) tenemos nulos donde no hay un próximo mes en nuestros datos; para cumplir con las especificaciones del OP, deben ser rangos de un solo mes.

EDITAR:en realidad, estos podrían ser rangos más largos que un mes, pero en todos los casos significan que necesitamos encontrar el último mes para el eid, que es:

(select max(am) from cm_abs_month d where d.eid = a.eid )

Debido a que las vistas descomponen el problema, podríamos agregar este "límite final" un mes antes, agregando otra vista, pero solo insertaré esto en la fusión. Lo que sería más eficiente depende de cómo su RDBMS optimice las consultas.

Para obtener el mes anterior, nos uniremos (cm_result_data.last_am - 1 =cm_abs_month.am)

Siempre que tengamos un valor nulo, el OP quiere que el mes "hasta" sea el mismo que el mes "desde", por lo que solo usaremos coalesce en eso:coalesce( last_am, am). Dado que last elimina los valores nulos, no es necesario que nuestras uniones sean uniones externas.

> select a.eid, b.m, b.y, c.m, c.y, a.v 
   from cm_result_data a 
    join cm_abs_month b 
      on ( a.eid = b.eid and a.am = b.am)  
    join cm_abs_month c 
      on ( a.eid = c.eid and 
      coalesce( a.last_am - 1, 
              (select max(am) from cm_abs_month d where d.eid = a.eid )
      ) = c.am)
    order by 1, 3, 2, 5, 4;
+-----+------+------+------+------+------+
| eid | m    | y    | m    | y    | v    |
+-----+------+------+------+------+------+
| 100 |    1 | 2008 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |    4 | 2008 |   80 |
+-----+------+------+------+------+------+

Al volver a unirnos, obtenemos el resultado que desea el OP.

No es que tengamos que volver a unirnos. Da la casualidad de que nuestra función de mes_absoluto es bidireccional, por lo que podemos volver a calcular el año y compensar el mes a partir de ella.

Primero, ocupémonos de agregar el mes "límite final":

> create or replace view cm_capped_result as 
select eid, am, 
  coalesce( 
   last_am - 1, 
   (select max(b.am) from cm_abs_month b where b.eid = a.eid)
  ) as last_am, v  
 from cm_result_data a;

Y ahora obtenemos los datos, formateados según el OP:

select eid, 
 ( (am - 1) % 12 ) + 1 as sm, 
 floor( ( am - 1 ) / 12 ) as sy, 
 ( (last_am - 1) % 12 ) + 1 as em, 
 floor( ( last_am - 1 ) / 12 ) as ey, v    
from cm_capped_result 
order by 1, 3, 2, 5, 4;

+-----+------+------+------+------+------+
| eid | sm   | sy   | em   | ey   | v    |
+-----+------+------+------+------+------+
| 100 |    1 | 2008 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |    4 | 2008 |   80 |
+-----+------+------+------+------+------+

Y ahí están los datos que quiere el OP. Todo en SQL que debe ejecutarse en cualquier RDBMS y se descompone en vistas simples, fáciles de entender y fáciles de probar.

¿Es mejor reincorporarse o recalcular? Dejaré eso (es una pregunta capciosa) para el lector.

(Si su RDBMS no permite agrupar bys en las vistas, tendrá que unirse primero y luego agrupar, o agrupar y luego extraer el mes y el año con subconsultas correlacionadas. Esto se deja como ejercicio para el lector). /P>

Jonathan Leffler pregunta en los comentarios,

¿Qué sucede con su consulta si hay lagunas en los datos (por ejemplo, hay una entrada para 2007-12 con un valor de 80 y otra para 2007-10, pero ninguna para 2007-11? La pregunta no está clara sobre lo que debería suceder allí.

Bueno, tienes toda la razón, el OP no especifica. Tal vez haya una condición previa (no mencionada) de que no haya lagunas. En ausencia de un requisito, no deberíamos tratar de codificar algo que podría no estar allí. Pero, el hecho es que las brechas hacen fracasar la estrategia de "reunión"; la estrategia de "recalcular" no falla en esas condiciones. Diría más, pero eso revelaría el truco en la pregunta capciosa a la que aludí anteriormente.