Oracle
 sql >> Baza danych >  >> RDS >> Oracle

Zapytanie SQL w celu zwinięcia zduplikowanych wartości według zakresu dat

Zamierzam rozwijać swoje rozwiązanie stopniowo, rozkładając każdą transformację na widok. Pomaga to zarówno wyjaśnić, co jest robione, jak i pomaga w debugowaniu i testowaniu. Zasadniczo stosuje zasadę funkcjonalnej dekompozycji do zapytań do bazy danych.

Zamierzam to również zrobić bez używania rozszerzeń Oracle, z SQL, który powinien działać na każdym nowoczesnym RBDMS. Więc nie trzymaj, nad, partycji, tylko podzapytania i grupuj według. (Poinformuj mnie w komentarzach, jeśli to nie działa na twoim RDBMS.)

Najpierw tabelę, którą jako niekreatywną nazwę miesiąc_wartość. Ponieważ identyfikator nie jest w rzeczywistości unikalnym identyfikatorem, nazwę go „eid”. Pozostałe kolumny to „m”onth, „y”ear i „v”alue:

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

Po wstawieniu danych dla dwóch eidów mam:

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

Następnie mamy jedną jednostkę, miesiąc, która jest reprezentowana jako dwie zmienne. Powinna to być jedna kolumna (data lub data-godzina, a może nawet klucz obcy do tabeli dat), więc zrobimy z niej jedną kolumnę. Zrobimy to jako transformację liniową, która sortuje tak samo jak (y, m) i taka, że ​​dla każdej krotki (y, m) istnieje jedna i jedyna wartość, a wszystkie wartości są kolejne:

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

To daje nam:

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

Teraz użyjemy sprzężenia własnego w skorelowanym podzapytaniu, aby znaleźć, dla każdego wiersza, najwcześniejszy następny miesiąc, w którym zmienia się wartość. Oprzemy ten widok na poprzednim, który utworzyliśmy:

> 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 jest teraz „miesiącem bezwzględnym” pierwszego (najwcześniejszego) miesiąca (po miesiącu bieżącego wiersza), w którym zmienia się wartość v. W tabeli nie ma późniejszego miesiąca dla tego eid.

Ponieważ last_am jest takie samo we wszystkich miesiącach poprzedzających zmianę v (która ma miejsce w last_am), możemy pogrupować według last_am i v (i oczywiście eid), a w dowolnej grupie min(am) jest wartością bezwzględną miesiąc pierwszego kolejny miesiąc, który miał tę wartość:

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

Teraz jest to zestaw wyników, który chcemy, dlatego ten widok nazywa się cm_result_data. Brakuje tylko czegoś, co pozwoliłoby przekształcić bezwzględne miesiące z powrotem do (y, m) krotek.

Aby to zrobić, po prostu dołączymy do tabeli wartość_miesiąca.

Są tylko dwa problemy:1) chcemy mieć miesiąc przed last_am w naszych danych wyjściowych, oraz2) mamy wartości null, gdy w naszych danych nie ma następnego miesiąca; aby spełnić specyfikację OP, powinny to być zakresy jednomiesięczne.

EDYCJA:W rzeczywistości mogą to być zakresy dłuższe niż jeden miesiąc, ale w każdym przypadku oznaczają, że musimy znaleźć ostatni miesiąc dla eid, czyli:

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

Ponieważ widoki rozkładają problem, moglibyśmy dodać ten „koniec” miesiąc wcześniej, dodając kolejny widok, ale po prostu wstawię to do połączenia. Który byłby najbardziej wydajny, zależy od tego, jak twój RDBMS optymalizuje zapytania.

Aby otrzymać miesiąc wcześniej, dołączymy (cm_result_data.last_am - 1 =cm_abs_month.am)

Gdziekolwiek mamy wartość null, OP chce, aby miesiąc „do” był taki sam jak miesiąc „od”, więc użyjemy po prostu w tym przypadku:koalesce( last_am, am). Ponieważ last eliminuje wszelkie wartości null, nasze złączenia nie muszą być złączeniami zewnętrznymi.

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

Dołączając z powrotem, otrzymujemy dane wyjściowe, których chce OP.

Nie, że musimy się dołączyć. Tak się składa, że ​​nasza funkcja absolute_month jest dwukierunkowa, więc możemy po prostu ponownie obliczyć rok i odsunąć od niego miesiąc.

Najpierw zajmijmy się dodaniem miesiąca „zakończenia”:

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

A teraz otrzymujemy dane sformatowane zgodnie z 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 |
+-----+------+------+------+------+------+

I są dane, których chce OP. Wszystko w języku SQL, które powinno działać na dowolnym RDBMS i jest rozłożone na proste, łatwe do zrozumienia i łatwe do przetestowania widoki.

Czy lepiej jest ponownie dołączyć, czy przeliczyć? Zostawię to (to podchwytliwe pytanie) czytelnikowi.

(Jeśli twój RDBMS nie zezwala na grupowanie według widoków, musisz najpierw dołączyć, a następnie do grupy lub grupy, a następnie pobrać miesiąc i rok ze skorelowanymi podzapytaniami. To jest ćwiczenie dla czytelnika.)

Jonathan Leffler pyta w komentarzach:

Co się stanie z Twoim zapytaniem, jeśli w danych pojawią się luki (powiedzmy, że jest wpis dla 2007-12 o wartości 80 i inny dla 2007-10, ale nie dla 2007-11? Pytanie nie jest jasne, co powinno się tam stać.

Cóż, masz rację, PO nie precyzuje. Być może istnieje (nie wymieniony) warunek wstępny, że nie ma luk. W przypadku braku wymagań nie powinniśmy próbować kodować czegoś, czego może nie być. Ale w rzeczywistości luki powodują, że strategia „połączenia zwrotnego” zawodzi; strategia „przeliczenia” nie zawodzi w tych warunkach. Powiedziałbym więcej, ale to ujawniłoby sztuczkę w podchwytliwym pytaniu, o którym wspomniałem powyżej.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Funkcja uśpienia w ORACLE

  2. przekazanie nazwy tabeli jako parametru plsql

  3. jak zmienić datę na godzinę w Oracle 10g

  4. Oracle RAC i sekwencje

  5. Lista zadań