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

#sql #oracle

#sql #Oracle

Вопрос:

У меня есть таблица, которая выглядит следующим образом:

  -------------------- --------- 
|    Month (date)    |  amount |
 -------------------- --------- 
| 2016-10-01         | 20      |
| 2016-08-01         | 10      |
| 2016-07-01         | 17      |
 -------------------- --------- 
  

Я ищу запрос (оператор sql), который удовлетворяет следующим условиям:

  • Дайте мне значение за предыдущий месяц.
  • Если для предыдущего месяца нет значения, заблокируйте время назад, пока не будет найдено значение.
  • Если есть только значение для текущего месяца, дайте мне это значение.

В таблице примеров строка, которую я ищу, будет такой:

  -------------------- --------- 
| 2016-08-01         | 10      |
 -------------------- --------- 
  

У кого-нибудь есть идея для несложного запроса select?

Заранее спасибо, Питер

Ответ №1:

Вам может понадобиться следующее:

 SELECT *
  FROM (  SELECT *
            FROM test
           WHERE TRUNC(SYSDATE, 'month') >= month
        ORDER BY CASE
                    WHEN TRUNC(SYSDATE, 'month') = month
                        THEN 0 /* if current month, ordered last */
                        ELSE 1 /* previous months are ordered first */
                    END DESC,
                    month DESC /* among previous months, the greatest first */
       )
 WHERE ROWNUM = 1    
  

Комментарии:

1. Это правильный подход, 1. Я придумал гораздо более сложное решение, которое я удалил, увидев ваше, которое намного проще. Однако я опубликовал новое решение, отталкиваясь от вашей идеи, поскольку, как мне кажется, я упростил его несколькими способами, что должно сделать его более эффективным. keep (dense_rank first/last) — это аккуратная агрегирующая / аналитическая функция, которая очень полезна в такого рода задачах. При всем сказанном снимаю шляпу за поиск самого простого подхода!

2. Хотя я не отмечал этот ответ как решение, он мне очень нравится. order by Было то, о чем я не подумал. Я уже использую keep (dense_rank , поэтому мне просто нужно изменить небольшую часть моего запроса. Также спасибо @mathguy за явное указание этого. 🙂

Ответ №2:

Другой способ, использующий MAX

 WITH tbl AS (
    SELECT TO_DATE('2016-10-01', 'YYYY-MM-DD') AS "month", 20 AS amount FROM dual
    UNION 
    SELECT TO_DATE('2016-08-01', 'YYYY-MM-DD') AS "month", 10 AS amount FROM dual    
    UNION 
    SELECT TO_DATE('2016-07-01', 'YYYY-MM-DD') AS "month", 5 AS amount FROM dual        
)
SELECT * 
  FROM tbl
 WHERE TRUNC("month", 'MONTH') = NVL((SELECT MAX(t."month") 
                                        FROM  tbl t 
                                       WHERE t."month" < TRUNC(SYSDATE, 'MONTH')), 
                                     TRUNC(SYSDATE, 'MONTH'));
  

Комментарии:

1. Мне нравится простота запроса. Спасибо. 🙂

Ответ №3:

Я бы использовал row_number() :

 select t.*
from (select t.*,
             row_number() over (order by (case when to_char(dte, 'YYYY-MM') = to_char(sysdate, 'YYYY-MM') then 1 else 2 end) desc,
                                          dte desc
                               ) as seqnum
      from t
     ) t
where seqnum = 1;
  

На самом деле, вам не нужно row_number() для этого:

 select t.*
from (select t.*
      from t
      order by (case when to_char(dte, 'YYYY-MM') = to_char(sysdate, 'YYYY-MM') then 1 else 2 end) desc,
               dte desc
     ) t
where rownum = 1;
  

Комментарии:

1. Это выглядит нормально, но поскольку столбец «месяц» уже находится в формате даты и, по-видимому, уже усечен до начала месяца, условие в инструкции CASE лучше записать как dte = trunc(sysdate, 'mm') .

Ответ №4:

Это не самый приятный запрос, но он должен сработать.

 select amount, date from (
  select amount, date, row_number over(partition by HERE_PUT_ID order by 
    case trunc(date, 'month') when trunc(sysdate, 'month') then to_date('00010101', 'yyyymmdd') else trunc(date, 'month')  end
    desc) r)
where r = 1;
  

Я предполагаю, что у вас есть какой-то идентификатор в таблице, поэтому поместите столбец id вместо HERE_PUT_ID, если вы хотите запросить всю таблицу, просто удалите: partition by HERE_PUT_ID

Ответ №5:

Я добавил больше данных для тестирования и столбец «id» (более реалистичный сценарий), чтобы показать, как это будет работать. Если в ваших данных нет «id», просто удалите любую ссылку на него из решения.

Примечания — month это зарезервированное слово Oracle, не используйте его в качестве имени столбца. Решение предполагает, что столбец date содержит даты, которые уже усечены до начала месяца. Хитрость в «порядке по» в dense_rank last заключается в присвоении значения (ЛЮБОГО значения!), когда месяц является текущим месяцем; по умолчанию значение, присвоенное всем другим месяцам, равно NULL, которые по умолчанию следуют за любым ненулевым значением в порядке возрастания.

Возможно, вы захотите протестировать различные решения на эффективность, если важно время выполнения.

 with
     inputs ( id, mth, amount ) as (
       select 1, date '2016-10-01', 20 from dual union all
       select 1, date '2016-08-01', 10 from dual union all
       select 1, date '2016-07-01', 17 from dual union all
       select 2, date '2016-10-01', 30 from dual union all
       select 2, date '2016-09-01', 25 from dual union all
       select 3, date '2016-10-01', 20 from dual union all
       select 4, date '2016-08-01', 45 from dual union all
       select 4, date '2016-06-01', 30 from dual
     )
-- end of TEST DATA - the solution (SQL query) is below this line
select id,
       max(mth) keep(dense_rank last order by
                case when mth = trunc(sysdate, 'mm') then 0 end, mth) as mth,
       max(amount) keep(dense_rank last order by
                case when mth = trunc(sysdate, 'mm') then 0 end, mth) as amount
from inputs
group by id
order by id   -- ORDER BY is optional
;


 ID MTH         AMOUNT
--- ---------- -------
  1 2016-08-01      10
  2 2016-09-01      25
  3 2016-10-01      20
  4 2016-08-01      45
  

Ответ №6:

Вы могли бы отсортировать данные в нужном направлении:

 with MyData as
(
  SELECT to_date('2016-10-01','YYYY-MM-DD') MY_DATE, 20 AMOUNT FROM DUAL UNION
  SELECT to_date('2016-08-01','YYYY-MM-DD') MY_DATE, 10 AMOUNT FROM DUAL UNION
  SELECT to_date('2016-07-01','YYYY-MM-DD') MY_DATE, 17 AMOUNT FROM DUAL 
),
MyResult AS (
  SELECT 
    D.*  
  FROM MyData D
  ORDER BY 
    DECODE(
      12*TO_CHAR(MY_DATE,'YYYY')   TO_CHAR(MY_DATE,'MM'),
      12*TO_CHAR(SYSDATE,'YYYY')   TO_CHAR(SYSDATE,'MM'),
      -1,
      12*TO_CHAR(MY_DATE,'YYYY')   TO_CHAR(MY_DATE,'MM')) 
  DESC
)
SELECT * FROM MyResult WHERE RowNum = 1