SQLer 生島勘富 のブログ

RDB・SQLの話題を中心に情報発信をしています。

OLAP(分析)関数について -- その前にサブクエリーで処理

 前回の続きです。

ざっくりとした考え方(毎回)

 GROUP BY は集約するので、結果が(集約キーを出力すれば)一意になる。つまり、出力される結果が一意になるまで集約される。

 しかし、OLAP(分析)関数は、SELECTされた結果を区切って処理する。そのため、レコード数に変化はない。

    集約する → GROUP BY
    区切る → PARTITION BY

 となる。これだけが分かっていれば実は簡単です。

イメージを確認。

 ■要望

 ■納品書明細(テーブル)
   納品書番号
   行
   商品ID
   単価
   数量
   消費税率

 というテーブル構造にで、消費税率は一律5%が入っています。
 消費税は、納品書単位に金額を合計したものに消費税率を掛け、四捨五入して請求しています。
 しかし、経理上、明細毎の消費税額が必要になり、明細単位に四捨五入すると、請求した消費税との誤差が発生します。
 その誤差を明細合計金額の多い順に1円ずつ増減して消費税額を計算してください。

 SQLで考えるときのイメージ。


拡大する

 SQLを書くときには、まずはイメージをもつことが非常に重要です。SQLは、図の通りイメージに向かって、1列ずつ横方向に考えて行きます。SQLは切れ目がないため分割できないと考える人が多いが、書き方(イメージの1列ずつ処理する)読み方(1列ずつイメージに戻していく)があり、切れ目はありますし、分けて見てて行けば難しくはありません。

 一方、おそらく手続き型で考えている人は、こういう方向に考えているでしょう。


拡大する

 SQLを書くには列単位に横に考えて行く必要があるのに、「(縦方向に)1行のデータが渡ってきた。さあどうしよう。」って考えて答えを出そうとしているでしょう(まあ、想像ですけどね)。つまり、どうしてもアルゴリズムを考えようとしているんです。これはCOBOLでもOO言語でも同じ。
 しかし、SQL手続き型言語のアンチテーゼとして生まれたため、あらゆることが逆になる。逆に考えればしっくり来るような言語体系になっています。

 先ほど読み方があると書いたけれど、もちろん、手続き型のイメージで考えている人が、そのイメージをSQL文法に当てはめて無理矢理にひねり出したSQLというのは、SQLを書いた本人が完成イメージを持ってないのだからどうしようもない。読める訳がないし、メンテなんてできない。SQLでやったらメンテできないという人が多いけれど、メンテできるも何も、そもそもできてない!もちろん、私もそんなモノをメンテしたりしない。ばっさりコメントアウト(笑)するしかないです。

納品書合計金額まで計算してみよう。

 SQLは列単位に考えて行くので、まずは、納品書明細金額までを計算してみましょう。

 RDBMSを使う以上、OO言語で処理しても、O/Rマッパなどを使っても、内部的には

    SELECT
      納品書番号, 行, 商品ID, 単価, 数量, 消費税率
    FROM 納品書明細
    WHERE (抽出条件);

 までは、必ず使うでしょう。

 これに、単純な導出項目を計算すると以下の通りになる。

    SELECT
      納品書番号, 行, 商品ID, 単価, 数量, 消費税率
      , 単価 * 数量 AS 合計
      , ROUND(単価 * 数量 * 消費税率) AS 明細毎の消費税
    FROM 納品書明細
    WHERE (抽出条件);

サブクエリーで計算する。

 続いて集計が必要になるが、OLAP関数か集計関数を使うことになります。OLAP関数が思いつかない、MySQLのように使えない場合はサブクエリーを使うしかありません。

 サブクエリーで計算する場合、考え方は2種類ある。

 ■ JOINするパターン

    SELECT
      m.納品書番号, m.行, m.商品ID, m.単価, m.数量, m.消費税率
      , m.単価 * m.数量 AS 合計
      , ROUND(m.単価 * m.数量 * m.消費税率) AS 明細毎の消費税
      , s.納品書合計金額
    FROM 納品書明細 m
      INNER JOIN
        (SELECT 納品書番号, SUM(単価 * 数量) AS 納品書合計金額
        FROM 納品書明細 WHERE (抽出条件)
        GROUP BY 納品書番号) s
      ON m.納品書番号 = s.納品書番号
    WHERE (抽出条件);

 OO言語で処理するときに、以下のSQLを実行して

    SELECT 納品書番号, SUM(単価 * 数量) AS 納品書合計金額
    FROM 納品書明細 WHERE (抽出条件)
    GROUP BY 納品書番号;

 OO言語側で結合処理するのとほぼ同じ処理になりイメージはこうなります。


 ■ループするパターンです。

    SELECT
      m.納品書番号, m.行, m.商品ID, m.単価, m.数量, m.消費税率
      , m.単価 * m.数量 AS 合計
      , ROUND(m.単価 * m.数量 * m.消費税率) AS 明細毎の消費税
      , (SELECT SUM(単価 * 数量)
        FROM 納品書明細 s
        WHERE s.納品書番号 = m.納品書番号) AS 納品書合計金額
    FROM 納品書明細 m
    WHERE (抽出条件);

 OO言語で処理するときに、ループの中で以下のSQLを実行しているとき

    SELECT SUM(単価 * 数量) AS 納品書合計金額
    FROM 納品書明細 WHERE 納品書番号 = ?;

 とほぼ同じ処理になりイメージはこうなります。

 いずれの処理でも、同じデータに2回以上アクセスしてしまう。特に、OO言語で行う場合は、そのやりとりをネットワーク(別プロセス)を挟んでループすることになり非常に非効率です。

 私が知る限り、OO言語でループするパターンで処理する人が多い(JOINを選ぶ人は少ない)。さすがに、同じ納品書番号で集計用のSQLを複数回実行するようなプログラムを書く人は少ない。しかし、O/Rマッパなどを使っていると、意識してないからか SELECT してきたすべてのレコードに対して集計用SQLを流す人もいる。

 SQLでは、FROM句でJOINする場合、結合処理にインデックスを使えないため、サブクエリーの処理結果の行数が多いときのパフォーマンスが悪い。SELECT句で処理すると、重複していてもデータの件数回、同じ集計処理が繰りかえされるため、遅い処理になる。もちろんキャッシュは効くし、将来、オプティマイザが賢くなる可能性はあるが、SQL文の意味として、SELECT句に書かれたサブクエリーは「件数回個別に実行する」ということだから、オプティマイザがまとめてくれることはないんじゃないかと思います。

 FROM句とSELECT句のどちらに書くのがよいかは、データの分散具合と抽出条件によって一概には言えないため、決め打ちはできません。システムによって違いますから、常に両方を検討する必要があります。

 また、OLAP(分析)関数までいけなかった。続きます。