SQL-запрос для свертывания дублирующихся значений по диапазону дат - PullRequest
7 голосов
/ 31 марта 2009

У меня есть таблица со следующей структурой: идентификатор, месяц, год, значение со значениями для одной записи на идентификатор в месяц, большинство месяцев имеют одинаковое значение.

Я хотел бы создать представление для этой таблицы, которое объединяет такие же значения, как это: ID, Начальный месяц, Конечный месяц, Начальный год, Конечный год, Значение, с одной строкой на идентификатор на значение.

Подвох в том, что если значение изменяется и затем возвращается к оригиналу, в таблице должно быть две строки

Итак:

  • 100 1 2008 80
  • 100 2 2008 80
  • 100 3 2008 90
  • 100 4 2008 80

должен произвести

  • 100 1 2008 2 2008 80
  • 100 3 2008 3 2008 90
  • 100 4 2008 4 2008 80

Следующий запрос работает для всего, кроме этого особого случая, когда значение возвращается к оригиналу.

select distinct id, min(month) keep (dense_rank first order by month) 
over (partition   by id, value) startMonth, 
max(month) keep (dense_rank first order by month desc) over (partition
by id, value) endMonth, 
value

База данных Oracle

Ответы [ 4 ]

48 голосов
/ 03 апреля 2009

Я собираюсь разрабатывать свое решение постепенно, раскладывая каждое преобразование в представление. Это помогает объяснить, что делается, и помогает в отладке и тестировании. По сути, он применяет принцип функциональной декомпозиции к запросам к базе данных.

Я также собираюсь сделать это без использования расширений Oracle, с SQL, который должен работать на любой современной RBDMS. Таким образом, нет хранения, разделения, просто подзапросы и групповые байсы. (Сообщите мне в комментариях, если это не работает на вашей РСУБД.)

Во-первых, таблица, которую я не знаю, я назову month_value. Поскольку идентификатор на самом деле не является уникальным идентификатором, я назову его «Ид». Другими столбцами являются «m», «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. Все, чего не хватает, это что-то, чтобы преобразовать абсолютные месяцы обратно в (у, м) кортежи.

Для этого мы просто присоединимся к таблице month_value.

Есть только две проблемы: 1) мы хотим, чтобы месяц до last_am в нашем выводе, и 2) у нас есть нули, которых нет в следующем месяце в наших данных; чтобы соответствовать спецификации OP, это должны быть месячные диапазоны.

РЕДАКТИРОВАТЬ: На самом деле это могут быть более длинные диапазоны, чем один месяц, но в каждом случае они означают, что мы должны найти самый последний месяц для ИД, который является:

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

Поскольку представления разлагают проблему, мы могли бы добавить эту «заглушку» месяцем ранее, добавив другое представление, но я просто вставлю это в объединение. Что будет наиболее эффективным, зависит от того, как ваша СУБД оптимизирует запросы.

Чтобы получить месяц раньше, мы присоединимся (cm_result_data.last_am - 1 = cm_abs_month.am)

В тех случаях, когда у нас есть ноль, OP хочет, чтобы месяц "до" был таким же, как месяц "с", поэтому мы просто используем для этого 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;

И теперь мы получаем данные, отформатированные в соответствии с ОП:

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, которое должно работать в любой СУБД и разбито на простые, понятные и легко тестируемые представления.

Лучше ли присоединиться или пересчитать? Я оставлю это (это вопрос с подвохом) читателю.

(Если ваша СУБД не разрешает групповые байсы в представлениях, вам нужно сначала присоединиться, а затем к группе или группе, а затем указать месяц и год с соответствующими подзапросами. Это оставлено в качестве упражнения для читателя .)


Джонатан Леффлер спрашивает в комментариях,

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

Ну, вы совершенно правы, ОП не указывает. Возможно, есть (не упомянутое) предварительное условие, что нет пробелов. В отсутствие требования мы не должны пытаться кодировать что-то, чего может не быть. Но факт в том, что из-за пробелов стратегия «присоединения назад» терпит неудачу; стратегия пересчета не терпит неудачу в этих условиях. Я бы сказал больше, но это раскрыло бы уловку в вопросе об уловке, на который я ссылался выше.

1 голос
/ 02 мая 2009

Этот использует только одно сканирование таблицы и работает по годам. Тем не менее, лучше смоделировать ваш столбец месяца и года как один столбец с датой:

SQL> create table tbl (id,month,year,value)
  2  as
  3  select 100,12,2007,80 from dual union all
  4  select 100,1,2008,80 from dual union all
  5  select 100,2,2008,80 from dual union all
  6  select 100,3,2008,90 from dual union all
  7  select 100,4,2008,80 from dual union all
  8  select 200,12,2007,50 from dual union all
  9  select 200,1,2008,50 from dual union all
 10  select 200,2,2008,40 from dual union all
 11  select 200,3,2008,50 from dual union all
 12  select 200,4,2008,50 from dual union all
 13  select 200,5,2008,50 from dual
 14  /

Tabel is aangemaakt.

SQL> select id
  2       , mod(min(year*12+month-1),12)+1 startmonth
  3       , trunc(min(year*12+month-1)/12) startyear
  4       , mod(max(year*12+month-1),12)+1 endmonth
  5       , trunc(max(year*12+month-1)/12) endyear
  6       , value
  7    from ( select id
  8                , month
  9                , year
 10                , value
 11                , max(rn) over (partition by id order by year,month) maxrn
 12             from ( select id
 13                         , month
 14                         , year
 15                         , value
 16                         , case lag(value) over (partition by id order by year,month)
 17                           when value then null
 18                           else rownum
 19                           end rn
 20                      from tbl
 21                  ) inner
 22         )
 23   group by id
 24       , maxrn
 25       , value
 26   order by id
 27       , startyear
 28       , startmonth
 29  /

        ID STARTMONTH  STARTYEAR   ENDMONTH    ENDYEAR      VALUE
---------- ---------- ---------- ---------- ---------- ----------
       100         12       2007          2       2008         80
       100          3       2008          3       2008         90
       100          4       2008          4       2008         80
       200         12       2007          1       2008         50
       200          2       2008          2       2008         40
       200          3       2008          5       2008         50

6 rijen zijn geselecteerd.

С уважением, Роб.

1 голос
/ 01 апреля 2009

Я получил это работать следующим образом. Он тяжел для аналитических функций и специфичен для Oracle.

select distinct id, value,
decode(startMonth, null,
  lag(startMonth) over(partition by id, value order by startMonth, endMonth),  --if start is null, it's an end so take from the row before
startMonth) startMonth,

  decode(endMonth, null,
  lead(endMonth) over(partition by id, value order by startMonth, endMonth),  --if end is null, it's an start so take from the row after
endMonth) endMonth    

from (
select id, value, startMonth, endMonth from(
select id, value, 
decode(month+1, lead(month) over(partition by id,value order by month), null, month)     
startMonth, --get the beginning month for each interval
decode(month-1, lag(month) over(partition by id,value order by month), null, month)     
endMonth --get the end month for each interval from Tbl
) a 
where startMonth is not null or endMonth is not null --remain with start and ends only
)b

Возможно, можно несколько упростить некоторые внутренние запросы

Внутренний запрос проверяет, является ли месяц первым / последним месяцем интервала следующим образом: если месяц + 1 == следующий месяц (лаг) для этой группировки, то, поскольку следующий месяц существует, этот месяц очевидно, не конец месяца. В противном случае это является последним месяцем интервала. Эта же концепция используется для проверки в течение первого месяца.

Внешний запрос сначала отфильтровывает все строки, которые не являются ни начальными, ни конечными месяцами (where startMonth is not null or endMonth is not null). Затем каждая строка является либо начальным месяцем, либо конечным месяцем (или обоими), определяемыми тем, является ли начало или конец не нулевым). Если месяц является начальным месяцем, получите соответствующий конечный месяц путем получения следующего (опережающего) endMonth для этого идентификатора, значения, упорядоченного по endMonth, а если это endMonth, то получите startMonth путем поиска предыдущего startMonth (lag)

0 голосов
/ 01 апреля 2009

Мне не удалось получить ответ от ngz, если входная таблица содержит несколько идентификаторов и диапазонов дат, охватывающих годы. У меня есть решение, которое работает, но с квалификацией. Он даст вам правильные ответы, только если вы знаете, что у вас есть строка для каждой комбинации месяц / год / идентификатор в пределах диапазона. Если есть "дыры", это не сработает. Если у вас есть дыры, я знаю хороший способ сделать это, кроме написания некоторого PL / SQL и использования цикла курсора для создания новой таблицы в нужном вам формате.

Кстати, именно поэтому данные, смоделированные таким образом, являются мерзостью. Вы всегда должны хранить данные как записи начала / из диапазона, а не как записи с дискретным периодом времени. Преобразовать первое в второе с помощью таблицы «множителей» тривиально, но почти невозможно (как вы видели) пойти в другом направлении.

SELECT ID
     , VALUE
     , start_date
     , end_date
  FROM (SELECT ID
             , VALUE
             , start_date
             , CASE
                  WHEN is_last = 0
                     THEN LEAD(end_date) OVER(PARTITION BY ID ORDER BY start_date)
                  ELSE end_date
               END end_date
             , is_first
          FROM (SELECT ID
                     , VALUE
                     , TO_CHAR(the_date, 'YYYY.MM') start_date
                     , TO_CHAR(NVL(LEAD(the_date - 31) OVER(PARTITION BY ID ORDER BY YEAR
                                  , MONTH), the_date), 'YYYY.MM') end_date
                     , is_first
                     , is_last
                  FROM (SELECT ID
                             , YEAR
                             , MONTH
                             , TO_DATE(TO_CHAR(YEAR) || '.' || TO_CHAR(MONTH) || '.' || '15', 'YYYY.MM.DD') the_date
                             , VALUE
                             , ABS(SIGN(VALUE -(NVL(LAG(VALUE) OVER(PARTITION BY ID ORDER BY YEAR
                                                   , MONTH), VALUE - 1)))) is_first
                             , ABS(SIGN(VALUE -(NVL(LEAD(VALUE) OVER(PARTITION BY ID ORDER BY YEAR
                                                   , MONTH), VALUE - 1)))) is_last
                          FROM test_table)
                 WHERE is_first = 1
                    OR is_last = 1))
 WHERE is_first = 1
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...