Неожиданная логика оценки CASE

8

Я всегда понимал, что CASEутверждение работает по принципу «короткого замыкания» в том смысле, что оценка последующих шагов не происходит, если предыдущий шаг оценивается как истинный. (Этот ответ Оценивает ли оператор SQL Server CASE все условия или выход при первом ИСТИННОМ условии? Связан, но, по-видимому, не охватывает эту ситуацию и относится к SQL Server).

В следующем примере я хочу рассчитать MAX(amount)разницу между месяцами, которая зависит от количества месяцев между датами начала и оплаты.

(Это, очевидно, сконструированный пример, но логика имеет обоснованные бизнес-аргументы в реальном коде, где я вижу проблему).

Если между датами начала и оплаты осталось менее 5 месяцев, то будет использовано выражение 1 , в противном случае будет использовано выражение 2 .

Это приводит к ошибке «ORA-01428: аргумент« -1 »выходит за пределы диапазона», потому что 1 запись имеет недопустимое условие данных, что приводит к отрицательному значению для начала предложения BETWEEN в ORDER BY.

Запрос 1

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
-- Expression 1
          MAX(amount)
             OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
             ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
             AND CURRENT ROW)
       ELSE
-- Expression 2
           MAX(amount)
             OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
             ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
       END                
    END 
  FROM payment

Итак, я пошел на этот второй запрос, чтобы сначала устранить все, где это может произойти:

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN 0
       ELSE
          CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
             MAX(amount)
                OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
                ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING 
                AND CURRENT ROW)
          ELSE
             MAX(amount)
                OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
                ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
          END                
       END
  FROM payment

К сожалению, есть непредвиденное поведение, которое означает, что значения, которые Выражение 1 будет использовать, будут проверены, даже если оператор не будет выполнен, потому что отрицательное условие теперь перехватывается внешним CASE.

Я могу обойти проблему, используя ABSна MONTHS_BETWEENв Expression 1 , но я чувствую , что это должно быть ненужным.

Это поведение, как ожидалось? Если так, то почему, поскольку это кажется мне нелогичным и больше похоже на ошибку?


Это создаст таблицу и тестовые данные. Запрос просто я проверяю, что выбран правильный путь в CASE.

CREATE TABLE payment
(ref_no NUMBER,
 start_date DATE,
 paid_date  DATE,
 amount  NUMBER)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('01-01-2016','DD-MM-YYYY'),3000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('12-12-2015','DD-MM-YYYY'),5000)

INSERT INTO payment
VALUES (1001,TO_DATE('10-03-2016','DD-MM-YYYY'),TO_DATE('10-02-2016','DD-MM-YYYY'),2000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('03-03-2016','DD-MM-YYYY'),6000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('28-11-2015','DD-MM-YYYY'),10000)

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN '<0'
       ELSE
          CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
             '<5'
         --    MAX(amount)
         --       OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS
         --       BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
         --       AND CURRENT ROW)
          ELSE
             '>=5'
         --    MAX(amount)
         --       OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS
         --       BETWEEN 5 PRECEDING AND CURRENT ROW)
          END                
       END
  FROM payment
BriteSponge
источник
3
FWIW SQL Server также имеет свои причуды в этой области, где вещи не совсем работают, как рекламируется dba.stackexchange.com/a/12945/3690
Мартин Смит,
3
В SQL Server размещение агрегата внутри выражения CASE может привести к тому, что части выражения будут оцениваться раньше, чем вы ожидаете . Интересно, происходит ли что-то подобное здесь?
Аарон Бертран
Это звучит довольно близко к этой ситуации. Заставляет меня задуматься о логике реализации CASE в двух разных RDBMS, которая приводит к одинаковому эффекту. Интересно.
BriteSponge
1
Интересно, разрешено ли это (и показывает ли это то же самое плохое поведение):MAX(amount) OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS BETWEEN GREATEST(0, LEAST(5, MONTHS_BETWEEN(paid_date, start_date))) PRECEDING AND CURRENT ROW)
ypercubeᵀᴹ
@ ypercubeᵀᴹ: предложенная вами агрегация не приводит к ошибке. Возможно, есть предел тому, насколько «глубокой» будет выглядеть оценка. Спекуляция.
BriteSponge

Ответы:

2

Поэтому мне было трудно определить, какой у вас был настоящий вопрос по почте, но я предполагаю, что это так, когда вы выполняете:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN 0
   ELSE
      CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
         MAX(amount)
            OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
            ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING 
            AND CURRENT ROW)
      ELSE
         MAX(amount)
            OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
            ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
      END                
   END
FROM payment

Вы все еще получаете ORA-01428: аргумент '-1' находится вне диапазона ?

Я не думаю, что это ошибка. Я думаю, что это порядок работы вещь. Oracle необходимо выполнить аналитику для всех строк, возвращаемых набором результатов. Тогда это может перейти к мельчайшим изменениям производительности.

Пара дополнительных способов обойти это - исключить строку с предложением where:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
   -- Expression 1
      MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
         AND CURRENT ROW)
   ELSE
   -- Expression 2
       MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
   END                
END 
FROM payment
-- this excludes the row from being processed
where MONTHS_BETWEEN(paid_date, start_date) > 0 

Или вы можете встроить кейс в свою аналитику, например:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
-- Expression 1
      MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
               ROWS BETWEEN 
               -- This case will be evaluated when the analytic is evaluated
               CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 
                THEN 0 
                ELSE MONTHS_BETWEEN(paid_date, start_date) 
                END 
              PRECEDING
              AND CURRENT ROW)
   ELSE
-- Expression 2
       MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
   END                
END 
FROM payment

объяснение

Хотел бы я найти какую-то документацию для резервного копирования порядка операций, но я не смог ничего найти ... пока.

Оценка CASEкороткого замыкания происходит после оценки аналитической функции. Порядок операций для рассматриваемого запроса будет следующим:

  1. от оплаты
  2. макс сверх ()
  3. дело.

Таким образом, так как max over()происходит раньше, запрос не выполняется.

Аналитические функции Oracle будут считаться источником строк . Если вы выполняете план объяснения для своего запроса, вы должны увидеть «сортировку окна», которая является аналитической, генерирующей строки, которые передаются ему предыдущим источником строки, таблицей платежей. Оператор case - это выражение, которое оценивается для каждой строки в источнике строки. Так что имеет смысл (по крайней мере для меня), что случай происходит после аналитического.

Ник С
источник
Я ценю потенциальные обходные пути - всегда интересно видеть, как другие делают вещи. Однако у меня есть и простой способ обойти это; функция ABS работает в моей ситуации. Кроме того, возможно, что на самом деле это не так, но если нет, то Oracle нужно заявить, что широкое соглашение относительно логики «короткого замыкания» не применимо в случае аналитических функций.
BriteSponge
Этот ответ имеет обходные пути и логическое объяснение. Я не думаю, что все станет более определенным, и поэтому я отмечу это как ответ. Спасибо
BriteSponge
1

SQL определяет, что делать, а не как это делать. В то время как Oracle обычно выполняет оценку короткого замыкания, это является оптимизацией, и поэтому ее следует избегать, если оптимизатор полагает, что другой путь выполнения обеспечивает превосходную производительность. Такой разницы в оптимизации можно ожидать, когда аналитика участвует.

Разница в оптимизации не ограничивается случаем. Ваша ошибка может быть воспроизведена с помощью coalesce, которая обычно также приводит к короткому замыканию.

select coalesce(1
   , max(1) OVER (partition by ref_no order by paid_date asc 
     rows between months_between(paid_date,start_date) preceding and current row)) 
from payment;

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

Все операторы SQL используют оптимизатор, часть базы данных Oracle, которая определяет наиболее эффективные средства доступа к указанным данным.

Этот вопрос показывает, что оценка короткого замыкания игнорируется даже без аналитики (хотя есть группировка).

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

Вы должны открыть SR с Oracle. Я подозреваю, что они примут это как ошибку документации и улучшат документацию в следующей версии, чтобы включить предупреждение об оптимизаторе.

Ли Риффель
источник
Я собирался открыть SR, но, похоже, я не смогу сделать это в моей организации, к сожалению.
BriteSponge
-1

Похоже, именно из-за окон Oracle начинает оценивать все выражения в CASE. Видеть

create table t (val int);   
insert into t select 0  from dual;  
insert into t select 1  from dual;  
insert into t select -1  from dual;  

select * from t;

select case when val = -1 then 999 else 2/(val + 1) end as res from t;  

select case when val = -1 then 999 else 2/(val + 1 + sum(val) over())  end as res from t;    

select case when val = -1 then 999 else sum(1) over(ORDER BY 1 ROWS BETWEEN val PRECEDING AND CURRENT ROW) end as res from t;    

drop table t;

Первые два запроса выполняются нормально.

Serg
источник