17

バックグラウンド:

元のケースは非常にシンプルでした。収益の高いものから低いものまで、ユーザーごとの現在の合計を計算します。

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);

クエリ:

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

出力:

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

特定のウィンドウ関数を使用すると、別の方法で計算できます。


SUMここで、windowedを使用して書き直すことができないと仮定しましょう。

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

列リストCROSS APPLYの相関サブクエリが好きではなく、2 回使用されるため、使用しました。 SELECTc3

すべてが正常に機能します。しかし、よく見るc2c3とても似ています。それでは、それらを組み合わせて単純な条件付き集計を使用してみませんか。

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;

残念ながら、それは不可能です。

外部参照を含む集計式に複数の列が指定されています。集計される式に外部参照が含まれる場合、その外部参照は、式で参照される唯一の列である必要があります。

もちろん、別のサブクエリでラップすることを回避できますが、少し「醜い」ものになります。

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バージョン。唯一の違いはLATERAL、代わりに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

それは非常にうまく機能します。


SQLite/MySQLバージョン (それが私が好む理由です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-SQLite SQLFiddleDemo-MySQL


Aggregates with an Outer Referenceを読みました:

制限のソースはSQL-92標準にあり、コードベースSQL Serverから継承されています。Sybase問題は、SQL Server が集計を計算するクエリを特定する必要があることです。

それを回避する方法だけを示す答えは探しません。

質問は次のとおりです。

  1. 標準のどの部分がそれを禁止または妨害しますか?
  2. 他の RDBMS では、この種の外部依存関係に問題がないのはなぜですか?
  3. それらは本来あるべきように拡張SQL StandardしてSQL Server動作しますか、SQL Serverそれとも完全に (正しく) 実装していませんか?

以下を参考にしていただければ幸いです。

編集:

SQL-92の概念がないことはわかっていLATERALます。ただし、( のような) サブクエリを使用するバージョンSQLite/MySQLも機能しません。

LiveDemo

編集2:

少し単純化するために、相関サブクエリのみをチェックしてみましょう。

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;

上記のバージョンは で正常に動作しMySQL/SQLite/Postgresqlます。

SQL Serverエラーが発生します。サブクエリでラップして1つのレベルに「フラット化」すると、次のように機能します。

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;

この質問のポイントは、それをどのようにSQL standard規制するかです。

LiveDemo

4

2 に答える 2

4

の SQL 標準には、このような制限はありませんLATERALCROSS APPLYは Microsoft のベンダー固有の拡張機能であり (Oracle は互換性のために後で採用しました)、その制限は明らかに ISO/IEC SQL 標準によるものではありませ

LATERAL標準SQLでは、基本的に、結合ツリーで横方向の参照を許可するための結合の修飾子にすぎません。参照できる列数に制限はありません。

そもそも奇妙な制限の理由がわかりません。おそらく、CROSS APPLY元々はテーブル値関数を許可することを目的としていたためであり、後でサブルーチンを許可するように拡張されましたSELECT

PostgresマニュアルLATERALでは、次のように説明されています。

LATERALキーワードは、サブSELECT FROMアイテムの前に置くことができます。これにより、sub-は、リスト内でそれより前に表示される項目SELECTの列を参照できます。( がない場合、各サブは個別に評価されるため、他の項目を相互参照できません。)FROMFROMLATERALSELECTFROM

クエリの Postgres バージョン (より洗練されたウィンドウ関数を除く) は、より単純になります。

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+ には、FILTER条件付き集計のためのより洗練された集計があります。

  • NULLIF理にかなっていますが、少し単純化することをお勧めします。

  • 一度にキャストsum_totalnumericます。

  • 目的の結果に一致するように結果を丸めます。

于 2016-04-11T15:32:37.350 に答える