2016-04-07 18 views
16

Background:SQL Server - aggregazione condizionale con correlazione

Il original case era molto semplice. Calcola totale corrente per utente dal più alto al più basso reddito:

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

uscita:

╔════╦═══════╦═════════╦════════════╦════════════════════╗ 
║ 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     ║ 
╚════╩═══════╩═════════╩════════════╩════════════════════╝ 

Potrebbe essere calcolato in modo diverso utilizzando funzioni specifiche della finestra.


Ora supponiamo che non possiamo usare finestrato SUM e riscriverlo:

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

ho usato CROSS APPLY perché non mi piace Sottointerrogazioni correlate a SELECT colonne lista e c3 viene utilizzato due volte.

Tutto funziona come dovrebbe. Ma quando guardiamo più da vicino c2 e c3 sono molto simili. Quindi, perché non combinarli e utilizzare un'aggregazione condizionale semplice:

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; 

Purtroppo non è possibile.

Più colonne sono specificate in un'espressione aggregata contenente un riferimento esterno. Se un'espressione aggregata contiene un riferimento esterno, allora quel riferimento esterno deve essere l'unica colonna referenziata nell'espressione.

Naturalmente potrei aggirarlo avvolgendo con un altro subquery, ma diventa un po ' "brutto":

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 versione. L'unica differenza è LATERAL anziché 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

Funziona molto bello.

versione

SQLite/MySQL (è per questo che preferisco 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


Ho letto Aggregates with an Outer Reference:

La fonte per la restrizione è nello standard SQL-92, e SQL Server ereditato dal Sybase codebase. Il problema è che SQL Server deve capire quale query calcolerà l'aggregato.

Non cercare le risposte che solo mostrano come aggirarlo.

Le domande sono:

  1. Quale parte di respingere standard o interferire con esso?
  2. Perché altri RDBMS non hanno problemi con questo tipo di dipendenza esterna?
  3. Si estendono SQL Standard e SQL Server si comporta come dovrebbe o SQL Server non lo implementa completamente (correttamente?) ?.

sarei molto grato per i riferimenti a:

  • ISO standard (92 o più recente)
  • SQL Server Standards Support
  • documenation ufficiale da qualsiasi RDBMS che lo spiega (SQL Server/Postgresql/Oracle/...).

EDIT:

so che SQL-92 non hai concetto di LATERAL. Ma la versione con sottoquery (come in SQLite/MySQL) non funziona troppo.

LiveDemo

EDIT 2:

Per semplificare un po ', cerchiamo di controllare solo subquery solo correlata:

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; 

La versione di cui sopra funziona bene in MySQL/SQLite/Postgresql.

In SQL Server otteniamo errore. Dopo wraping con sottoquery per "appiattire" ad un livello funziona:

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; 

Il punto di questa domanda è come si fa SQL standard lo regolano.

LiveDemo

risposta

4

C'è una soluzione più semplice:

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; 

io non sono sicuro perché o se questa limitazione è nel SQL '92 standard. L'ho fatto memorizzare abbastanza bene circa 20 anni fa, ma non ricordo quella particolare limitazione.

Vorrei sottolineare:

  • Al momento dello standard SQL 92, laterale unisce non erano in realtà sul radar. Sybase sicuramente non aveva questo concetto.
  • Altri database do hanno problemi con riferimenti esterni. In particolare, spesso limitano l'ambito a un livello profondo.
  • Lo standard SQL tende a essere altamente politico (ovvero guidato dal fornitore) anziché guidato dai reali requisiti dell'utente del database. Bene, nel tempo, si muove nella giusta direzione.
+0

Sì, oppure utilizzare 'CROSS APPLY' ** [Demo] (http://rextester.com/LOBM67950) **. "Nasconde" la sottoquery. – lad2025

+0

'(VALORI (c.REVENUE)) x (REVENUE)' fa un lavoro migliore, secondo me. –

+0

zucchero sintattico dipende dal gusto :) La prima nota è vera, 'LATERAL' (anche il tipo di zucchero sintattico) è un concetto più recente. – lad2025

4

Non esiste alcuna limitazione dello standard SQL per LATERAL. CROSS APPLY è un'estensione specifica del fornitore di Microsoft (Oracle l'ha adottata in seguito per compatibilità) e le sue limitazioni non sono ovviamente dovute allo standard ANSI SQL, poiché la funzione MS pre-data è lo standard.

LATERAL secondo ANSI SQL è fondamentalmente solo un modificatore per i join per consentire i riferimenti laterali nell'albero dei join. Non c'è limite al numero di colonne a cui è possibile fare riferimento.

Non vedrei una ragione per la strana restrizione all'inizio. Forse perché CROSS APPLY originariamente era destinato a consentire funzioni con valori di tabella, che in seguito sono stati estesi per consentire i sub-SELECT s.

Il Postgres manual spiega LATERAL come questo:

La parola chiave LATERAL può precedere una voce di SELECT FROM sub. Ciò consente a di fare riferimento alle colonne di FROM voci che compaiono prima di esso nell'elenco FROM. (Senza LATERAL, ogni sub-SELECT viene valutato in modo indipendente e quindi non può fare un riferimento incrociato a nessun altro elemento FROM.)

La versione Postgres della query (senza le più eleganti funzioni finestra) può essere più semplice:

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+ ha la più aggregato FILTER per gli aggregati condizionali elegante.

  • NULLIF ridondante. revenue è definito NOT NULL, gli aggregati sono garantiti per trovare 1 o più righe e il LATERAL sub- SELECT è unito in un CROSS JOIN, quindi sum_total non può essere NULL. Che era indietro, ho pensato a COALESCE. NULLIF ha senso, suggerisco solo una piccola semplificazione.

  • Cast sum_total a numeric una volta.

  • Risultato rotondo per abbinare il risultato desiderato.

+0

Ero a conoscenza della funzione 'FILTER'. La versione di Postgresql era per demo ed è per questo che lo volevo come 1: 1 possibile. La versione 'CROSS APPLY' di SQL Sever consente anche di fare riferimenti incrociati. Il punto di questa domanda in che modo SQL Standard lo tratta con le funzioni di aggregazione. Dimentichiamo per un momento 'LATERAL' e otteniamo una versione subquery correlata semplice [demo] (http://sqlfiddle.com/#!15/94a35/1/0) che funziona perfettamente in MySQL/SQLite/Postgresql. Lo standard SQL definisce a quale colonna possiamo fare riferimento dalla funzione agg? – lad2025

+0

'Lo standard SQL definisce a quale colonna possiamo fare riferimento dalla funzione agg?" Direi, a tutte le colonne visibili. (Senza indagare effettivamente sugli standard SQL.) –

+0

Probabilmente è la restrizione di 'SQL Server' che impone che tutte le colonne di refrence debbano essere dallo" stesso livello ". Al punto: è indefinito/libero definito in standard o specifico del venditore. – lad2025