Logic đánh giá CASE bất ngờ


8

Tôi luôn hiểu rằng CASEtuyên bố hoạt động theo nguyên tắc 'ngắn mạch' trong việc đánh giá các bước tiếp theo sẽ không diễn ra nếu bước trước được đánh giá là đúng. (Câu trả lời này Câu lệnh CASE của SQL Server có đánh giá tất cả các điều kiện hoặc thoát khỏi điều kiện TRUE đầu tiên không? Có liên quan nhưng dường như không bao gồm tình huống này và liên quan đến SQL Server).

Trong ví dụ sau, tôi muốn tính toán MAX(amount)giữa một loạt các tháng khác nhau dựa trên số tháng bắt đầu và ngày trả tiền.

(Đây rõ ràng là một ví dụ được xây dựng nhưng logic có lý do kinh doanh hợp lệ trong mã thực tế nơi tôi thấy vấn đề).

Nếu có <5 tháng giữa ngày bắt đầu và ngày trả tiền thì Biểu thức 1 sẽ được sử dụng nếu không Biểu thức 2 sẽ được sử dụng.

Điều này dẫn đến lỗi "ORA-01428: đối số '-1' nằm ngoài phạm vi" vì 1 bản ghi có điều kiện dữ liệu không hợp lệ dẫn đến giá trị âm khi bắt đầu mệnh đề BETweEN của ORDER BY.

Truy vấn 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

Vì vậy, tôi đã tìm kiếm truy vấn thứ hai này để loại bỏ bất cứ nơi nào điều này có thể xảy ra:

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

Thật không may, có một số hành vi không mong muốn có nghĩa là các giá trị Biểu thức 1 NÊN sử dụng được xác thực, mặc dù câu lệnh sẽ không được thực thi vì điều kiện tiêu cực hiện bị bẫy bên ngoài CASE.

Tôi có thể khắc phục vấn đề này bằng cách sử dụng ABStrên MONTHS_BETWEENtrong Biểu 1 , nhưng tôi cảm thấy như thế này nên không cần thiết.

Là hành vi như mong đợi? Nếu vậy 'tại sao' vì nó có vẻ phi logic với tôi và giống như một lỗi hơn?


Điều này sẽ tạo ra một bảng và dữ liệu thử nghiệm. Truy vấn chỉ đơn giản là tôi kiểm tra xem đường dẫn chính xác CASEđang được thực hiện.

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

3
FWIW SQL Server cũng có những điểm kỳ quặc trong lĩnh vực này, nơi mọi thứ không hoạt động như quảng cáo dba.stackexchange.com/a/12945/3690
Martin Smith

3
Trong SQL Server, việc đặt tổng hợp bên trong biểu thức CASE có thể buộc các phần của biểu thức được ước tính trước khi bạn mong đợi . Tôi tự hỏi nếu một cái gì đó tương tự đang xảy ra ở đây?
Aaron Bertrand

Nghe có vẻ khá gần với tình huống này. Làm cho tôi tự hỏi về logic của việc triển khai CASE trong hai RDBMS khác nhau dẫn đến cùng một loại hiệu ứng. Hấp dẫn.
BriteSprid

1
Tôi tự hỏi nếu điều này được cho phép (và liệu nó có biểu hiện hành vi xấu tương tự không):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ᵀᴹ: Tập hợp bạn đề xuất không đưa ra lỗi. Có lẽ có giới hạn cho việc đánh giá 'sâu sắc' như thế nào. Đầu cơ.
BriteSprid

Câu trả lời:


2

Vì vậy, thật khó cho tôi để xác định câu hỏi thực sự của bạn là gì từ bài đăng, nhưng tôi cho rằng đó là khi bạn thực hiện:

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

Bạn vẫn nhận được ORA-01428: đối số '-1' nằm ngoài phạm vi ?

Tôi không nghĩ rằng đây là một lỗi. Tôi nghĩ rằng đó là một thứ tự của hoạt động. Oracle cần thực hiện phân tích trên tất cả các hàng được trả về bởi tập kết quả. Sau đó, nó có thể giảm xuống mức độ khó chịu của việc chuyển đổi đầu ra.

Một vài cách bổ sung xung quanh điều này sẽ là loại trừ hàng có mệnh đề 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 

Hoặc bạn có thể nhúng một trường hợp vào phân tích của bạn như:

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

Giải trình

Tôi ước tôi có thể tìm thấy một số tài liệu để sao lưu thứ tự vận hành, nhưng tôi chưa thể tìm thấy bất cứ thứ gì ....

Các CASEđánh giá ngắn mạch xảy ra sau khi các chức năng phân tích được đánh giá. Thứ tự của các hoạt động cho truy vấn trong câu hỏi sẽ là:

  1. từ thanh toán
  2. tối đa trên ()
  3. trường hợp

Vì vậy, kể từ khi max over()xảy ra trước vụ án, truy vấn không thành công.

Các chức năng phân tích của Oracle sẽ được coi là một nguồn hàng . Nếu bạn thực hiện kế hoạch giải thích cho truy vấn của mình, bạn sẽ thấy "sắp xếp cửa sổ" là phân tích, tạo các hàng, được cung cấp cho nó bởi nguồn hàng trước đó, bảng thanh toán. Một câu lệnh tình huống là một biểu thức được ước tính cho mỗi hàng trong nguồn hàng. Vì vậy, nó có ý nghĩa (với tôi ít nhất), rằng trường hợp xảy ra sau khi phân tích.


Tôi đánh giá cao những công việc tiềm năng xung quanh - thật thú vị khi xem người khác làm việc như thế nào. Tuy nhiên, tôi có và cách dễ dàng để khắc phục điều này; chức năng ABS hoạt động trong tình huống của tôi. Ngoài ra, có thể điều này không thực sự bị loại bỏ nhưng nếu không thì Oracle cần phải nói rằng quy ước rộng về logic 'ngắn mạch' không áp dụng trong trường hợp các hàm phân tích.
BriteSprid

Câu trả lời này có công việc xung quanh và một lời giải thích hợp lý. Tôi không nghĩ mọi thứ sẽ trở nên dứt khoát hơn và vì vậy tôi sẽ đánh dấu đây là câu trả lời. Cảm ơn
BriteSprid

1

SQL định nghĩa những gì cần làm, không phải làm như thế nào. Mặc dù thông thường, Oracle sẽ đánh giá trường hợp ngắn mạch, đây là một tối ưu hóa và do đó sẽ tránh được nếu trình tối ưu hóa tin rằng một đường dẫn thực thi khác cung cấp hiệu suất vượt trội. Một sự khác biệt tối ưu hóa như vậy sẽ được dự kiến ​​khi phân tích có liên quan.

Sự khác biệt tối ưu hóa không giới hạn trong trường hợp. Lỗi của bạn có thể được sao chép bằng cách sử dụng kết hợp, thông thường cũng sẽ bị đoản mạch.

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;

Dường như không có bất kỳ tài liệu nào nói rõ ràng rằng đánh giá ngắn mạch có thể bị bỏ qua bởi trình tối ưu hóa. Điều gần nhất (mặc dù không đủ gần) tôi có thể tìm thấy là đây :

Tất cả các câu lệnh SQL đều sử dụng trình tối ưu hóa, một phần của Cơ sở dữ liệu Oracle để xác định các phương tiện hiệu quả nhất để truy cập dữ liệu đã chỉ định.

Câu hỏi này cho thấy đánh giá ngắn mạch bị bỏ qua ngay cả khi không có phân tích (mặc dù có nhóm).

Tom Kyte đề cập rằng ngắn mạch có thể bị bỏ qua trong câu trả lời của ông cho một câu hỏi trên Thứ tự đánh giá vị ngữ .

Bạn nên mở một SR với Oracle. Tôi nghi ngờ họ sẽ chấp nhận nó như một lỗi tài liệu và nâng cao tài liệu trong phiên bản tiếp theo để đưa vào lời cảnh báo về trình tối ưu hóa.


Tôi sẽ mở một SR nhưng có vẻ như tôi không thể làm điều đó trong tổ chức của mình một cách đáng tiếc.
BriteSprid

-1

Có vẻ như cửa sổ điều gì khiến Oracle bắt đầu đánh giá tất cả các biểu thức trong CASE. Xem

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;

Hai truy vấn đầu tiên chạy OK.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.