2016-04-07 22 views
16

Bối cảnh:Máy chủ SQL - tập hợp có điều kiện với tương quan

original case rất đơn giản. Tính chạy tổng cho mỗi người dùng từ doanh thu cao nhất đến thấp nhất:

CREATE TABLE t(Customer INTEGER NOT NULL PRIMARY KEY 
       ,"User" VARCHAR(5) NOT NULL 
       ,Revenue INTEGER NOT NULL); 

INSERT INTO t(Customer,"User",Revenue) VALUES 
(001,'James',500),(002,'James',750),(003,'James',450), 
(004,'Sarah',100),(005,'Sarah',500),(006,'Sarah',150), 
(007,'Sarah',600),(008,'James',150),(009,'James',100); 

Query:

SELECT *, 
    1.0 * Revenue/SUM(Revenue) OVER(PARTITION BY "User") AS percentage, 
    1.0 * SUM(Revenue) OVER(PARTITION BY "User" ORDER BY Revenue DESC) 
     /SUM(Revenue) OVER(PARTITION BY "User") AS running_percentage 
FROM t; 

LiveDemo

Output:

╔════╦═══════╦═════════╦════════════╦════════════════════╗ 
║ ID ║ User ║ Revenue ║ percentage ║ running_percentage ║ 
╠════╬═══════╬═════════╬════════════╬════════════════════╣ 
║ 2 ║ James ║  750 ║ 0.38  ║ 0.38    ║ 
║ 1 ║ James ║  500 ║ 0.26  ║ 0.64    ║ 
║ 3 ║ James ║  450 ║ 0.23  ║ 0.87    ║ 
║ 8 ║ James ║  150 ║ 0.08  ║ 0.95    ║ 
║ 9 ║ James ║  100 ║ 0.05  ║ 1     ║ 
║ 7 ║ Sarah ║  600 ║ 0.44  ║ 0.44    ║ 
║ 5 ║ Sarah ║  500 ║ 0.37  ║ 0.81    ║ 
║ 6 ║ Sarah ║  150 ║ 0.11  ║ 0.93    ║ 
║ 4 ║ Sarah ║  100 ║ 0.07  ║ 1     ║ 
╚════╩═══════╩═════════╩════════════╩════════════════════╝ 

Nó có thể được tính toán khác nhau bằng cách sử dụng các chức năng cửa sổ cụ thể.


Bây giờ chúng ta hãy giả sử rằng chúng ta không thể sử dụng cửa sổ SUM và viết lại nó:

SELECT c.Customer, c."User", c."Revenue" 
    ,1.0 * Revenue/NULLIF(c3.s,0) AS percentage 
    ,1.0 * c2.s /NULLIF(c3.s,0) AS running_percentage 
FROM t c 
CROSS APPLY 
     (SELECT SUM(Revenue) AS s 
     FROM t c2 
     WHERE c."User" = c2."User" 
      AND c2.Revenue >= c.Revenue) AS c2 
CROSS APPLY 
     (SELECT SUM(Revenue) AS s 
     FROM t c2 
     WHERE c."User" = c2."User") AS c3 
ORDER BY "User", Revenue DESC; 

LiveDemo

Tôi đã sử dụng CROSS APPLY bởi vì tôi không muốn truy vấn con tương quan trong SELECT colums danh sách và c3 được sử dụng hai lần.

Mọi thứ hoạt động như mong muốn. Nhưng khi chúng ta nhìn gần hơn c2c3 thì rất giống nhau. Vì vậy, tại sao không kết hợp chúng và sử dụng tổng hợp có điều kiện đơn giản:

SELECT c.Customer, c."User", c."Revenue" 
    ,1.0 * Revenue  /NULLIF(c2.sum_total,0) AS percentage 
    ,1.0 * c2.sum_running/NULLIF(c2.sum_total,0) AS running_percentage 
FROM t c 
CROSS APPLY 
     (SELECT SUM(Revenue) AS sum_total, 
       SUM(CASE WHEN c2.Revenue >= c.Revenue THEN Revenue ELSE 0 END) 
       AS sum_running 
     FROM t c2 
     WHERE c."User" = c2."User") AS c2 
ORDER BY "User", Revenue DESC; 

Thật không may là không thể.

Nhiều cột được chỉ định trong biểu thức tổng hợp chứa tham chiếu ngoài. Nếu một biểu thức được tổng hợp chứa tham chiếu ngoài, thì tham chiếu ngoài đó phải là cột duy nhất được tham chiếu trong biểu thức.

Dĩ nhiên tôi có thể phá vỡ nó gói với subquery khác, nhưng nó trở nên một chút "xấu xí":

SELECT c.Customer, c."User", c."Revenue" 
    ,1.0 * Revenue  /NULLIF(c2.sum_total,0) AS percentage 
    ,1.0 * c2.sum_running/NULLIF(c2.sum_total,0) AS running_percentage 
FROM t c 
CROSS APPLY 
( SELECT SUM(Revenue) AS sum_total, 
      SUM(running_revenue) AS sum_running 
    FROM (SELECT Revenue, 
        CASE WHEN c2.Revenue >= c.Revenue THEN Revenue ELSE 0 END 
        AS running_revenue 
      FROM t c2 
      WHERE c."User" = c2."User") AS sub 
) AS c2 
ORDER BY "User", Revenue DESC 

LiveDemo


Postgresql phiên bản. Sự khác biệt duy nhất là LATERAL thay vì CROSS APPLY.

SELECT c.Customer, c."User", c.Revenue 
    ,1.0 * Revenue  /NULLIF(c2.sum_total,0) AS percentage 
    ,1.0 * c2.running_sum/NULLIF(c2.sum_total,0) AS running_percentage 
FROM t c 
,LATERAL (SELECT SUM(Revenue) AS sum_total, 
       SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END) 
       AS running_sum 
     FROM t c2 
     WHERE c."User" = c2."User") c2 
ORDER BY "User", Revenue DESC; 

SqlFiddleDemo

Nó hoạt động rất tốt đẹp.


SQLite/MySQL phiên bản (có nghĩa là lý do tại sao tôi thích LATERAL/CROSS APPLY):

SELECT c.Customer, c."User", c.Revenue, 
    1.0 * Revenue/(SELECT SUM(Revenue) 
        FROM t c2 
        WHERE c."User" = c2."User") AS percentage, 
    1.0 * (SELECT SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END) 
      FROM t c2 
      WHERE c."User" = c2."User")/
      (SELECT SUM(c2.Revenue) 
      FROM t c2 
      WHERE c."User" = c2."User") AS running_percentage 
FROM t c 
ORDER BY "User", Revenue DESC; 

SQLFiddleDemo-SQLiteSQLFiddleDemo-MySQL


Tôi đã đọc Aggregates with an Outer Reference:

Các nguồn cho những hạn chế là trong tiêu chuẩn SQL-92SQL Server thừa hưởng nó từ Sybase codebase. Vấn đề là SQL Server cần phải tìm ra truy vấn nào sẽ tính tổng hợp.

Tôi không tìm kiếm câu trả lời rằng chỉ hiển thị cách phá vỡ nó.

Các câu hỏi là:

  1. Phần nào của disallow tiêu chuẩn hoặc gây trở ngại với nó?
  2. Tại sao các RDBMS khác không có vấn đề với loại phụ thuộc bên ngoài này?
  3. Họ có mở rộng SQL StandardSQL Server hoạt động bình thường hoặc SQL Server không triển khai đầy đủ (đúng không?) ?.

tôi sẽ rất biết ơn về tài liệu tham khảo để:

  • ISO standard (92 hoặc mới hơn)
  • SQL Server Standards Support
  • documenation chính thức từ bất kỳ RDBMS giải thích nó (SQL Server/Postgresql/Oracle/...).

EDIT:

Tôi biết rằng SQL-92 không có khái niệm về LATERAL. Nhưng phiên bản với các truy vấn phụ (như trong SQLite/MySQL) cũng không hoạt động.

LiveDemo

EDIT 2:

Để đơn giản hóa nó một chút, chúng ta hãy kiểm tra chỉ tương quan chỉ subquery:

SELECT c.Customer, c."User", c.Revenue, 
     1.0*(SELECT SUM(CASE WHEN c2.Revenue >= c.Revenue THEN c2.Revenue ELSE 0 END) 
       FROM t c2 
       WHERE c."User" = c2."User") 
    /(SELECT SUM(c2.Revenue) 
      FROM t c2 
      WHERE c."User" = c2."User") AS running_percentage 
FROM t c 
ORDER BY "User", Revenue DESC; 

Phiên bản trên hoạt động tốt trong MySQL/SQLite/Postgresql.

Trong SQL Server chúng tôi gặp lỗi. Sau wraping nó với subquery để "san bằng" nó đến một mức độ hoạt động:

SELECT c.Customer, c."User", c.Revenue, 
     1.0 * (
       SELECT SUM(CASE WHEN r1 >= r2 THEN r1 ELSE 0 END) 
       FROM (SELECT c2.Revenue AS r1, c.Revenue r2 
        FROM t c2 
        WHERE c."User" = c2."User") AS S)/
      (SELECT SUM(c2.Revenue) 
       FROM t c2 
       WHERE c."User" = c2."User") AS running_percentage 
FROM t c 
ORDER BY "User", Revenue DESC; 

Mấu chốt của câu hỏi này là như thế nào SQL standard điều chỉnh nó.

LiveDemo

Trả lời

4

Có một giải pháp dễ dàng hơn:

SELECT c.Customer, c."User", c."Revenue", 
     1.0 * Revenue/ NULLIF(c2.sum_total, 0) AS percentage, 
     1.0 * c2.sum_running/NULLIF(c2.sum_total, 0) AS running_percentage 
FROM t c CROSS APPLY 
    (SELECT SUM(c2.Revenue) AS sum_total, 
      SUM(CASE WHEN c2.Revenue >= x.Revenue THEN c2.Revenue ELSE 0 END) 
       as sum_running 
     FROM t c2 CROSS JOIN 
      (SELECT c.REVENUE) x 
     WHERE c."User" = c2."User" 
    ) c2 
ORDER BY "User", Revenue DESC; 

Tôi không chắc chắn lý do tại sao hoặc nếu hạn chế này là trong tiêu chuẩn SQL 92. Tôi đã ghi nhớ nó khá tốt từ 20 năm trước, nhưng tôi không nhớ lại giới hạn cụ thể đó.

tôi nên lưu ý:

  • Vào thời điểm chuẩn SQL 92, bên tham gia đã không thực sự trên radar. Sybase chắc chắn không có khái niệm như vậy.
  • Các cơ sở dữ liệu khác do gặp sự cố với tham chiếu bên ngoài. Đặc biệt, họ thường giới hạn phạm vi đến một cấp độ sâu.
  • Bản thân tiêu chuẩn SQL có xu hướng có tính chính trị cao (có nghĩa là, do nhà cung cấp điều khiển) thay vì được thúc đẩy bởi các yêu cầu người dùng cơ sở dữ liệu thực tế. Vâng, theo thời gian, nó di chuyển đúng hướng.
+0

Yup hoặc sử dụng 'CROSS APPLY' ** [Demo] (http://rextester.com/LOBM67950) **. Nó "ẩn" truy vấn phụ. – lad2025

+0

'(GIÁ TRỊ (c.REVENUE)) x (REVENUE)' làm một công việc tốt hơn, theo ý kiến ​​của tôi. –

+0

Đường cú pháp nó phụ thuộc vào hương vị :) Lưu ý đầu tiên là đúng, 'LATERAL' (cũng là loại đường cú pháp) là khái niệm mới hơn. – lad2025

4

Không có giới hạn nào trong tiêu chuẩn SQL cho LATERAL. CROSS APPLY là một phần mở rộng dành riêng cho nhà cung cấp từ Microsoft (Oracle đã chấp nhận nó sau này để tương thích) và các giới hạn của nó rõ ràng không được nợ theo chuẩn ANSI SQL, vì tính năng MS trước ngày tiêu chuẩn.

LATERAL theo ANSI SQL về cơ bản chỉ là công cụ sửa đổi cho phép nối để cho phép tham chiếu bên trong cây nối. Không có giới hạn về số cột có thể được tham chiếu.

Tôi sẽ không thấy lý do cho giới hạn lẻ bắt đầu. Có thể bởi vì CROSS APPLY ban đầu được thiết kế để cho phép các hàm có giá trị bảng, sau này được mở rộng để cho phép sub-SELECT s.

Các Postgres manual giải thích LATERAL như thế này:

Các LATERAL từ khóa có thể đứng trước một phụ SELECT FROM mục. Điều này cho phép phụ số SELECT tham chiếu đến các cột của FROM mục xuất hiện trước nó trong danh sách FROM. (Nếu không có LATERAL, mỗi phụ số SELECT được đánh giá độc lập và vì vậy không thể tham chiếu chéo bất kỳ mục FROM nào khác.)

Các Postgres phiên bản của truy vấn của bạn (không có chức năng cửa sổ thanh lịch hơn) có thể đơn giản hơn:

SELECT c.* 
    , round(revenue  /c2.sum_total, 2) END AS percentage 
    , round(c2.running_sum/c2.sum_total, 2) END AS running_percentage 
FROM t c, LATERAL (
    SELECT NULLIF(SUM(revenue), 0)::numeric AS sum_total -- NULLIF, cast once 
     , SUM(revenue) FILTER (WHERE revenue >= c.revenue) AS running_sum 
    FROM t 
    WHERE "User" = c."User" 
    ) c2 
ORDER BY c."User", c.revenue DESC; 
  • Postgres 9.4+ có thêm thanh lịch tổng hợp FILTER cho uẩn có điều kiện.

  • NULLIF là không cần thiết. revenue được xác định NOT NULL, tổng hợp được đảm bảo tìm 1 hoặc nhiều hàng và LATERAL sub- SELECT được kết hợp trong một CROSS JOIN, vì vậy sum_total không thể là NULL. Đó là ngược, tôi nghĩ về COALESCE. NULLIF có ý nghĩa, tôi chỉ đề xuất đơn giản hóa nhỏ.

  • Truyền sum_total đến numeric một lần.

  • Kết quả vòng để khớp với kết quả mong muốn của bạn.

+0

Tôi đã biết tính năng 'FILTER'. Phiên bản Postgresql là cho bản demo đó là lý do tại sao tôi muốn nó như 1: 1 càng tốt. Phiên bản SQL Sever 'CROSS APPLY' cũng cho phép tham chiếu chéo. Điểm của câu hỏi này là cách SQL Standard xử lý nó với các hàm tổng hợp. Hãy quên đi 'LATERAL' trong một lát và lấy phiên bản truy vấn con tương ứng đơn giản [demo] (http://sqlfiddle.com/#!15/94a35/1/0) hoạt động hoàn hảo trong MySQL/SQLite/Postgresql. Chuẩn SQL có định nghĩa cột nào chúng ta có thể tham chiếu từ hàm agg? – lad2025

+0

'Chuẩn SQL có định nghĩa cột nào chúng ta có thể tham chiếu từ hàm agg?' Tôi muốn nói, với tất cả các cột nhìn thấy được. (Nếu không thực sự điều tra các tiêu chuẩn SQL.) –

+0

Có lẽ đó là hạn chế 'SQL Server' để thực thi rằng tất cả các cột refrence phải được từ" cùng cấp ". Đến điểm: Có phải nó không xác định/lỏng lẻo được xác định trong tiêu chuẩn hoặc nhà cung cấp cụ thể. – lad2025

Các vấn đề liên quan