Oracle
 sql >> база данни >  >> RDS >> Oracle

SQL заявка за свиване на дублиращи се стойности по период от време

Ще развивам решението си постепенно, като разлагам всяка трансформация в изглед. Това едновременно помага да се обясни какво се прави и помага при отстраняване на грешки и тестване. По същество се прилага принципът на функционална декомпозиция към заявките към база данни.

Също така ще го направя без да използвам разширения на Oracle, със SQL, който трябва да работи на всеки съвременен RBDMS. Така че няма задържане, над, разделяне, само подзаявки и групови bys. (Информирайте ме в коментарите, ако не работи на вашата RDBMS.)

Първо, таблицата, която тъй като не съм креативен, ще нарека month_value. Тъй като идентификаторът всъщност не е уникален идентификатор, ще го нарека "eid". Другите колони са "m"onth, "y"ear и "v"alue:

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

След въвеждане на данните, за две eids, имам:

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

След това имаме един обект, месецът, който е представен като две променливи. Това наистина трябва да е една колона (или дата, или дата и час, или може би дори външен ключ към таблица с дати), така че ще я направим една колона. Ще направим това като линейна трансформация, така че да сортира същото като (y, m) и така, че за всеки (y,m) кортеж има една и единствена стойност и всички стойности са последователни:

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

Това ни дава:

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

Сега ще използваме самостоятелно присъединяване в корелирана подзаявка, за да намерим за всеки ред най-ранния следващ месец, в който стойността се променя. Ще базираме този изглед на предишния изглед, който създадохме:

> 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 сега е "абсолютният месец" на първия (най-ранния) месец (след месеца на текущия ред), в който стойността v се променя. То е нула, когато в таблицата няма по-късен месец за този eid.

Тъй като last_am е едно и също за всички месеци, водещи до промяната в v (която се случва в last_am), можем да групираме по last_am и v (и eid, разбира се), и във всяка група min(am) е абсолютното месец на първия пореден месец, който имаше тази стойност:

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

Сега това е наборът от резултати, който искаме, поради което този изглед се нарича cm_result_data. Всичко, което липсва, е нещо, което да трансформира абсолютните месеци обратно в (y,m) кортежи.

За да направим това, просто ще се присъединим към таблицата month_value.

Има само два проблема:1) искаме месец преди last_am в нашия изход и2) имаме нулеви стойности, където в нашите данни няма следващия месец; за да отговарят на спецификацията на ОП, те трябва да са едномесечни диапазони.

РЕДАКТИРАНЕ:Това всъщност може да са по-дълги обхвати от един месец, но във всеки случай те означават, че трябва да намерим последния месец за eid, който е:

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

Тъй като изгледите разграждат проблема, бихме могли да добавим тази „крайна шапка“ месец по-рано, като добавим друг изглед, но просто ще вмъкна това в обединението. Кое би било най-ефективно зависи от това как вашата RDBMS оптимизира заявките.

За да получите месец преди това, ще се присъединим (cm_result_data.last_am - 1 =cm_abs_month.am)

Където и да имаме нула, ОП иска месецът "до" да бъде същият като месеца "от", така че просто ще използваме coalesce за това:coalesce( last_am, am). Тъй като last елиминира всички нули, не е необходимо нашите съединения да бъдат външни съединения.

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

Като се присъединим обратно, получаваме изхода, който OP иска.

Не че трябва да се присъединим обратно. Както се случва, нашата функция absolute_month е двупосочна, така че можем просто да преизчислим годината и да изместим месеца от нея.

Първо, нека се погрижим за добавянето на месец „крайна шапка“:

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

И сега получаваме данните, форматирани според 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 |
+-----+------+------+------+------+------+

И ето данните, които ОП иска. Всичко в SQL, което трябва да работи на всяка RDBMS и е разложено на прости, лесни за разбиране и лесни за тестване изгледи.

По-добре ли е да се присъедините отново или да преизчислите? Ще оставя това (това е трик въпрос) на читателя.

(Ако вашата RDBMS не позволява групиране в изгледи, ще трябва първо да се присъедините и след това да групирате или групирате и след това да изтеглите месеца и годината с корелирани подзаявки. Това е оставено като упражнение за читателя.)

Джонатан Лефлър пита в коментарите,

Какво се случва с вашата заявка, ако има пропуски в данните (да речем, че има запис за 2007-12 със стойност 80 и друг за 2007-10, но не и за 2007-11? Въпросът не е ясен какво трябва да се случи там.

Е, абсолютно си прав, ОП не уточнява. Може би има (неспоменато) предварително условие да няма пропуски. При липса на изискване не трябва да се опитваме да кодираме нещо, което може да не е там. Но факт е, че пропуските правят стратегията за "връщане" неуспешна; стратегията "преизчисляване" не се проваля при тези условия. Бих казал повече, но това ще разкрие трика във въпроса с трик, за който споменах по-горе.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Изпълняване на съхранена процедура в SQL Developer?

  2. Пример за Oracle Dynamic SQL за вмъкване на запис с помощта на DBMS_SQL

  3. SQLT и разделяне

  4. ORA-03113:край на файла на комуникационния канал

  5. Използване на Oracle JDeveloper 12c с Oracle Database, част 1