Oracle
 sql >> Base de données >  >> RDS >> Oracle

Requête SQL pour réduire les valeurs en double par plage de dates

Je vais développer ma solution progressivement, en décomposant chaque transformation en une vue. Cela permet à la fois d'expliquer ce qui est fait et d'aider au débogage et aux tests. Il s'agit essentiellement d'appliquer le principe de décomposition fonctionnelle aux requêtes de base de données.

Je vais également le faire sans utiliser les extensions Oracle, avec SQL qui devrait fonctionner sur n'importe quel RBDMS moderne. Donc pas de keep, over, partition, juste des sous-requêtes et des bys de groupe. (Informez-moi dans les commentaires si cela ne fonctionne pas sur votre SGBDR.)

Tout d'abord, la table, que puisque je ne suis pas créatif, j'appellerai month_value. Étant donné que l'identifiant n'est pas réellement un identifiant unique, je l'appellerai "eid". Les autres colonnes sont "m"onth, "y"ear et "v"alue :

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

Après avoir inséré les données, pour deux eids, j'ai :

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

Ensuite, nous avons une entité, le mois, qui est représentée par deux variables. Cela devrait vraiment être une colonne (soit une date ou une date-heure, ou peut-être même une clé étrangère vers une table de dates), donc nous en ferons une colonne. Nous ferons cela comme une transformation linéaire, telle qu'elle trie de la même manière que (y, m), et telle que pour tout tuple (y, m) il y ait une et une seule valeur, et toutes les valeurs sont consécutives :

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

Cela nous donne :

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

Nous allons maintenant utiliser une jointure réflexive dans une sous-requête corrélée pour trouver, pour chaque ligne, le premier mois successeur au cours duquel la valeur change. Nous allons baser cette vue sur la vue précédente que nous avons créée :

> 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 est maintenant le "mois absolu" du premier mois (le plus ancien) (après le mois de la ligne actuelle) au cours duquel la valeur, v, change. C'est nul là où il n'y a pas de mois ultérieur, pour cet eid, dans le tableau.

Étant donné que last_am est le même pour tous les mois précédant le changement de v (qui se produit à last_am), nous pouvons grouper sur last_am et v (et eid, bien sûr), et dans n'importe quel groupe, le min(am) est l'absolu mois du premier mois consécutif qui avait cette valeur :

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

Maintenant, c'est le jeu de résultats que nous voulons, c'est pourquoi cette vue s'appelle cm_result_data. Tout ce qui manque, c'est quelque chose pour transformer des mois absolus en tuples (y, m).

Pour ce faire, nous allons simplement joindre à la table month_value.

Il n'y a que deux problèmes :1) nous voulons le mois d'avant last_am dans notre sortie, et 2) nous avons des valeurs nulles là où il n'y a pas de mois prochain dans nos données ; pour répondre aux spécifications de l'OP, celles-ci doivent être des plages d'un mois.

EDIT :Il peut s'agir en fait de plages plus longues qu'un mois, mais dans tous les cas, cela signifie que nous devons trouver le dernier mois pour l'eid, qui est :

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

Parce que les vues décomposent le problème, nous pourrions ajouter ce "bouchon final" un mois plus tôt, en ajoutant une autre vue, mais je vais juste l'insérer dans la fusion. Ce qui serait le plus efficace dépend de la façon dont votre SGBDR optimise les requêtes.

Pour obtenir un mois avant, nous allons rejoindre (cm_result_data.last_am - 1 =cm_abs_month.am)

Partout où nous avons un null, l'OP veut que le mois "à" soit le même que le mois "de", donc nous utiliserons simplement coalesce sur cela:coalesce( last_am, am). Puisque last élimine tous les nulls, nos jointures n'ont pas besoin d'être des jointures externes.

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

En nous rejoignant, nous obtenons la sortie souhaitée par l'OP.

Non pas que nous devions nous rejoindre. En l'occurrence, notre fonction absolute_month est bidirectionnelle, nous pouvons donc simplement recalculer l'année et décaler le mois à partir de celle-ci.

Tout d'abord, occupons-nous d'ajouter le mois "fin" :

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

Et maintenant, nous obtenons les données, formatées selon l'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 |
+-----+------+------+------+------+------+

Et il y a les données que l'OP veut. Le tout en SQL qui devrait s'exécuter sur n'importe quel RDBMS, et est décomposé en vues simples, faciles à comprendre et faciles à tester.

Vaut-il mieux rejoindre ou recalculer ? Je laisse cela (c'est une question piège) au lecteur.

(Si votre RDBMS n'autorise pas les regroupements dans les vues, vous devrez d'abord rejoindre, puis grouper, ou grouper, puis extraire le mois et l'année avec des sous-requêtes corrélées. Ceci est laissé comme un exercice pour le lecteur.)

Jonathan Leffler demande dans les commentaires,

Que se passe-t-il avec votre requête s'il y a des lacunes dans les données (disons qu'il y a une entrée pour 2007-12 avec une valeur de 80, et une autre pour 2007-10, mais pas une pour 2007-11 ? La question n'est pas claire sur ce qui devrait se passer là.

Eh bien, vous avez tout à fait raison, l'OP ne le précise pas. Il y a peut-être une condition préalable (non mentionnée) qu'il n'y ait pas de lacunes. En l'absence d'exigence, nous ne devrions pas essayer de coder autour de quelque chose qui pourrait ne pas être là. Mais, le fait est que les lacunes font échouer la stratégie de « rejoindre » ; la stratégie "recalculer" n'échoue pas dans ces conditions. J'en dirais plus, mais cela révélerait l'astuce de la question piège à laquelle j'ai fait allusion ci-dessus.