(Translated by https://www.hiragana.jp/)
第3回 ループ依存症~手続き型の呪縛を打ち破れ! | gihyo.jp

SQL緊急きんきゅう救命きゅうめいしつ

だい3かいループ依存いぞんしょう手続てつづがた呪縛じゅばくやぶれ!

ここはとあるまち総合そうごう病院びょういん

ここには通常つうじょう診療しんりょうのほかに、一風いっぷうわった診療しんりょう存在そんざいする。

なんけんもの病院びょういんをたらいまわしにされた、ほどこしようのないSQLや、いますぐに改善かいぜん必要ひつようなSQLがかつまれる救命きゅうめいしつである。

それがSQL緊急きんきゅう救命きゅうめいしつりゃくしてSER(SQL Emergency Room⁠⁠。

そう、ここは国内こくないでも唯一ゆいいつのプログラミング専門せんもん外来がいらいである。

ロバート

救命きゅうめいしつ部長ぶちょううでつエンジニアだが、くちわる性格せいかくはもっとわるよんじゅうオヤジ。

ループという病

SQLをまなうえもっとたかいハードルとなるのが、順序じゅんじょ手続てつづきではなく、集合しゅうごう論理ろんり観点かんてんからかんがえることだ。

J.Celko『Joe Celko's SQL Programming Style』
⁠Morgan Kaufmann、2005ねんp.184

ループによる表現ひょうげん

(PM3:00 手術しゅじゅつしつさきほどかつぎまれたばかりの患者かんじゃまえにして、なにやらめている)

ワイリー、そこをどけ!


いーえ、どきません。ぼくは、どうしても今回こんかい患者かんじゃ治療ちりょう必要ひつようだとはおもえないのです。先生せんせいは、健康けんこう患者かんじゃに、功名こうみょうしんのあまり必要ひつようのない治療ちりょうをしようとしていませんか? 先生せんせいらなきゃがすまないんでしょう!

若僧わかぞうが、きいたふうくちを! いいからどけっ(ドンッ)


ああっ!(へなへなとくずちる)


ワイリー、喧嘩けんかしている時間じかんはないわ。さっさとはじめるわよ。


カルテ:1のような2つのテーブルがあるとする。Salesテーブルは企業きぎょうごとの会計かいけいねんごとのげを記録きろくしている。ただしとし連続れんぞくしているとはかぎらない。このデータから、おな企業きぎょうについてあるとしとその直近ちょっきんとしげの変化へんか調しらべたい。

その結果けっかを、varれつ追加ついかしたSales2に登録とうろくする。varれつつぎのルールによってめられる。

  • よりふるとしのデータが存在そんざいしない場合ばあい:NULL
  • 直近ちょっきんとしのデータよりげがびた場合ばあい:+
  • 直近ちょっきんとしのデータよりげがった場合ばあい:-
  • 直近ちょっきんとしのデータよりげとおな場合ばあい:=

登録とうろくのSales2テーブルは2のようになる。

1Sales、Sales2テーブル
図1 Sales、Sales2テーブル
2 登録とうろくのSales2テーブル
図2 登録後のSales2テーブル

SQLでループをえるには

患者かんじゃのコードはリスト1ですね。


リスト1 患者かんじゃのコード(Oracle PL/SQL⁠



CREATE OR REPLACE PROCEDURE PROC_INSERT_VAR
IS

  /* ①カーソル宣言せんげん */
  CURSOR c_sales IS
       SELECT company, year, sale
         FROM Sales
        ORDER BY company, year;

  /* レコードタイプ宣言せんげん */
  rec_sales c_sales%ROWTYPE;

  /* カウンタ */
  i_pre_sale INTEGER := 0;
  c_company CHAR(1) := '*';
  c_var CHAR(1) := '*';

BEGIN

OPEN c_sales;

  LOOP
    /* レコードをフェッチして変数へんすう代入だいにゅう */
    fetch c_sales into rec_sales;
    /* レコードがなくなったらループ終了しゅうりょう */
    exit when c_sales%notfound;

    IF (c_company = rec_sales.company) THEN
        /* 直前ちょくぜんのレコードがおな会社かいしゃのレコードの場合ばあい */
        /* 直前ちょくぜんのレコードとげを比較ひかく(②③)*/
        IF (i_pre_sale < rec_sales.sale) THEN
            c_var := '+';
        ELSIF (i_pre_sale > rec_sales.sale) THEN
            c_var := '-';
        ELSE
            c_var := '=';
        END IF;

    ELSE
        c_var := NULL;
    END IF;

    /* ④登録とうろくさきテーブルにデータを登録とうろく */
    INSERT INTO Sales2 (company, year, sale, var) VALUES (rec_sales.company, rec_sales.year, rec_sales.sale, c_var);

    c_company := rec_sales.company;
    i_pre_sale := rec_sales.sale;

  END LOOP;

  CLOSE c_sales;
  commit;

END;

 OracleのPL/SQL のコードを提示ていじしましたが、便宜べんぎてきなサンプルとして使つかっているだけなので、のDBMSのプロシージャ言語げんごおよびJavaのようなホスト言語げんご適宜てきぎえてください。

問題もんだいのパターンとしては、行間ぎょうかん比較ひかく[1]とコントロールブレーク処理しょり[2]ね。患者かんじゃのコードからアルゴリズムを抽出ちゅうしゅつすると、こうなるわ。

  • ① Salesテーブルからぜんけんレコードを取得しゅとくする。このとき企業きぎょう,とし昇順しょうじゅんにソートしておく
  • ② 1レコードずつループをおこない、おな会社かいしゃのレコードであるか比較ひかくする
  • おな会社かいしゃのレコードであれば、そのげと直近ちょっきんげを比較ひかくし、比較ひかく結果けっかおうじて変化へんかれつ設定せっていする
  • ④ Sales2テーブルへINSERTする
  • ⑤ ②~④の処理しょりをレコードがなくなるまでつづける

素朴そぼく疑問ぎもんなんですけど、このコードにはなに問題もんだいがあるんですか? すごくわかりやすい、たりまえかいじゃないですか。やっぱり治療ちりょう必要ひつようだとはおもえないなあ。

たしかに、手続てつづがた言語げんごとしてはオーソドックスなかいだ。だがそれを宣言せんげんがた世界せかいむと、ループ依存いぞんしょうになる。ループというのはまじなみたいなものだ。我々われわれ思考しこうつね規定きていし、しばろうとしてくる。

そののろいからだっするにはどうすればいいんですか?


「レコード」ではなく、⁠レコードの集合しゅうごうという観点かんてんからかんがえること。SQLで行間ぎょうかん比較ひかくおこな手段しゅだんなに

えっと……ウィンドウ関数かんすう参考さんこう資料しりょう1、2⁠、それが使つかえない実装じっそうでは相関そうかんサブクエリ参考さんこう資料しりょう3……でしたっけ。

そうね。ループの部分ぶぶんまるごとそれでえることが可能かのうよ。あとは、IFぶんはSQLではつねにCASEしき変換へんかんできることは、前回ぜんかいったとおり。ウィンドウ関数かんすう使つかったかいはこうなるわリスト2⁠。

リスト2 ウィンドウ関数かんすう使つかったかい
INSERT INTO Sales2
SELECT company,
       year,
       sale,
       CASE WHEN MAX(company) ―①直前ちょくぜんのレコードがおな会社かいしゃのレコードの場合ばあい
                     OVER (ORDER BY company, year
                            ROWS BETWEEN 1 PRECEDING ―②
                                     AND 1 PRECEDING) = company
            THEN CASE SIGN(sale - MAX(sale) ―③直前ちょくぜんのレコードとげを比較ひかく
                                      OVER (ORDER BY company, year
                                             ROWS BETWEEN 1 PRECEDING ―④
                                                      AND 1 PRECEDING) )
                 WHEN 0 THEN '='
                 WHEN 1 THEN '+'
                 WHEN -1 THEN '-'
                 ELSE NULL END
            ELSE NULL END AS var
  FROM Sales;

ウィンドウ関数かんすうでループをえる

リスト2の2つのCASEしきが、それぞれおな会社かいしゃであるかの比較ひかく直近ちょっきんげとの比較ひかく対応たいおうしています。これは手続てつづがた言語げんごいた場合ばあい同様どうようです。

このかい重要じゅうよう技術ぎじゅつは、ウィンドウ関数かんすうにおいてROWS BETWEENオプションを使つかっていることです②④⁠。これはさかのぼる対象たいしょう範囲はんいのレコードを、直前ちょくぜんの1ぎょう制限せいげんしています。ROWS BETWEEN 1 PRECEDING AND 1 PRECEDINGは、カレントレコードの1ぎょうまえから1ぎょうまえ範囲はんいという意味いみなので、結局けっきょく直前ちょくぜんの1ぎょうだけをふくみます3⁠。

3 ROWS BETWEENの動作どうさ
図3 ROWS BETWEENの動作

つまり、2つのウィンドウ関数かんすうは、それぞれ「1ぎょうまえ会社かいしゃめい「1ぎょうまえげ」もどとします。

リスト34のように、結果けっか表示ひょうじさせてみればはっきりします。

リスト3 ウィンドウ関数かんすう「1ぎょうまえ会社かいしゃめい「1ぎょうまえげ」取得しゅとく
SELECT company,
       year,
       sale,
       MAX(company)
          OVER (ORDER BY company, year
                 ROWS BETWEEN 1 PRECEDING
                          AND 1 PRECEDING) AS pre_company,
       MAX(sale)
          OVER (ORDER BY company, year
                 ROWS BETWEEN 1 PRECEDING
                          AND 1 PRECEDING) AS pre_sale
FROM Sales;
4 リスト3の実行じっこう結果けっか
company year sale pre_company pre_sale
------- ---- ----- ----------- --------
      A 2002    50
      A 2003    52           A       50
      A 2004    55           A       52
      A 2007    55           A       55
      B 2001    27           A       55
      B 2005    28           B       27
      B 2006    28           B       28
      B 2009    30           B       28
      C 2001    40           B       30
      C 2005    39           C       40
      C 2006    38           C       39
      C 2010    35           C       38

もし、比較ひかく対象たいしょうのレコードを「1ぎょうまえではなく「2ぎょうまえにしたいならば、ROWS BETWEEN 2 PRECEDING AND 2 PRECEDINGと、さかのぼるレンジをえてやることで簡単かんたん対応たいおうできます。この柔軟じゅうなんさは、つぎ相関そうかんサブクエリのかいにはない利点りてんです。

またこのかいでは、SIGN関数かんすう使つかっているのもワンポイントです(リスト2⁠。これは、数値すうちがた引数ひきすうり、符号ふごうがマイナスなら-1を、プラスなら1を、0の場合ばあいは0をかえ関数かんすうで、直近ちょっきんとしとのげの変化へんかるために利用りようしています。CASEしき条件じょうけん部分ぶぶんに、なんもウィンドウ関数かんすう記述きじゅつしないためのちょっとしたしょうわざですSQLには変数へんすうがないため、こうしたしょうわざ必要ひつようになります⁠⁠。

ROWS BETWEENオプションを利用りようできる環境かんきょう

ただし、このアプローチを利用りようするときには注意ちゅういてんがあります。このコードが実行じっこうできるのは、現在げんざいのところOracleおよびDB2のみです。MySQLはもともとウィンドウ関数かんすう実装じっそうのため使つかえないのは自明じめいですが、PostgreSQLとSQL Serverの場合ばあいウィンドウ関数かんすう一応いちおう実装じっそうしているのですが、ROWS BETWEENオプションを使つかうことができません。

ですがこれは標準ひょうじゅんSQLの機能きのうなので、時間じかんてばいずれおおくの実装じっそう普及ふきゅうしていくことが期待きたいされます。ただ、まだ比較的ひかくてきあたらしい機能きのうのため、使つかえる実装じっそうかぎられることも事実じじつです。

ウィンドウ関数かんすう使つかえない場合ばあいのために、つぎ相関そうかんサブクエリによるアプローチをてみましょう。

相関そうかんサブクエリでループをえる

うーん、むずかしい。むずかしいですよこれは。


自慢じまんうな。では、相関そうかんサブクエリのかいはどうなる?


直前ちょくぜんの1ぎょうをスカラサブクエリで表現ひょうげんすれば、ウィンドウ関数かんすうつくっているvarれつおなじようにつくれるはずってことですよね。よし……これでどうでしょうリスト4⁠。

リスト4 相関そうかんサブクエリを使つかったワイリーの回答かいとう
INSERT INTO Sales2 
SELECT company,
       year,
       sale,
       CASE SIGN(sale - (SELECT sale 直近ちょっきんとし売上うりあげを選択せんたく
                           FROM Sales S2
                          WHERE S1.company = S2.company
                            AND S2.year =
                              (SELECT MAX(year)  直近ちょっきんとし選択せんたく
                                 FROM Sales S3
                                WHERE S1.company = S3.company
                                  AND S1.year > S3.year )))
       WHEN 0  THEN '='
       WHEN 1  THEN '+'
       WHEN -1 THEN '-'
       ELSE NULL END AS var
  FROM Sales S1;

うむ、いいだろう。相関そうかんサブクエリではS1.company = S2.companyという条件じょうけんによっておな会社かいしゃならばという条件じょうけん実現じつげんできる。こちらのほうが、典型てんけいてきなSQLの集合しゅうごう指向しこうかんがかただな。

相関そうかんサブクエリのなかとう結合けつごう使つかうことによってカレントレコードを起点きてんとした集合しゅうごうつくるのは、ウィンドウ関数かんすう導入どうにゅうされるまえのSQLでは定石じょうせき技術ぎじゅつでした。

いま相関そうかんサブクエリのなかでは、S2テーブルからある条件じょうけんでレコードを選択せんたくし、そのなかから最大さいだいとしMAX(sale)選択せんたくしています。これがすなわち直近ちょっきんとしげ」になります。その条件じょうけん中心ちゅうしんとなるのが、S1.year > S3.yearという不等式ふとうしきです。カレントレコードはS1.yearのほうですから、⁠それよりちいさい(=むかしの)としという意味いみになります。

S1.yearと、S1.year > S3.year条件じょうけん合致がっちするレコード集合しゅうごう対応たいおうをマッピングすると5のようになります太字ふとじとしは、集合しゅうごうなか最大さいだいしめします⁠⁠。

5 レコード集合しゅうごう対応たいおうをマッピング
図5 レコード集合の対応をマッピング

このように、⁠ある基準きじゅんにそれよりちいさい集合しゅうごう集合しゅうごうろん下界げかいlower boundびます。基準きじゅんとなるとしすすむにしたがって、下界げかい要素ようそすうは1つずつえていく様子ようすがわかります。

相関そうかんサブクエリの正体しょうたい集合しゅうごう

へえ~、おもしろいなあ。S1はS0をふくみ、S2はS1をふくみ……というふうにどんどんじょう集合しゅうごうおおきくなっていくんですね。

そうね。そういう見方みかたをすれば、この相関そうかんサブクエリがつく下界げかい再帰さいきてき集合しゅうごうでもあるわね。あなたがいまったように、S0~S3にはつぎのような包含ほうがん関係かんけい成立せいりつするから。

S0⊂S1⊂S2⊂S3

この包含ほうがん関係かんけい図示ずしすると、6のような同心円どうしんえんてき再帰さいき集合しゅうごうえがけるわ。

6 とう結合けつごう同心円どうしんえんてき集合しゅうごうつく
図6 非等値結合は同心円的な入れ子集合を作る

奇妙きみょうなサンドイッチ

とう結合けつごうは、数学すうがくてきにはいろいろ興味深きょうみぶか操作そうさではあるな。では、ウィンドウ関数かんすう相関そうかんサブクエリを使つかった2つのかいについて実行じっこう計画けいかくておくとしよう7、8⁠。

7 リスト2:ウィンドウ関数かんすうによるかい実行じっこう計画けいかく(Oracle)
-------------------------------------------------------------------------------------------
| Id | Operation                    | Name        | Rows | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |             |   12 |   108 |       2 (0)| 00:00:01 |
|  1 |  WINDOW BUFFER             |             |   12 |   108 |       2 (0)| 00:00:01 |
|  2 |   TABLE ACCESS BY INDEX ROWID| SALES       |   12 |   108 |       2 (0)| 00:00:01 |
|  3 | I  NDEX FULL SCAN            | SYS_C004248 |   12 |       |       1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
8 リスト4:相関そうかんサブクエリによるかい実行じっこう計画けいかく(Oracle)
-----------------------------------------------------------------------------------------------
| Id  | Operation                       | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |             |    12 |   348 |       3 (0)| 00:00:01 |
|   1 |  SORT AGGREGATE                 |             |     1 |    29 |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID   | SALES       |     1 |    29 |       2 (0)| 00:00:01 |
|*  3 |    INDEX UNIQUE SCAN            | SYS_C004280 |     1 |       |       1 (0)| 00:00:01 |
|   4 |     SORT AGGREGATE              |             |     1 |    16 |            |          |
|   5 |      FIRST ROW                  |             |     1 |    16 |       2 (0)| 00:00:01 |
|*  6 |       INDEX RANGE SCAN (MIN/MAX)| SYS_C004280 |     1 |    16 |       2 (0)| 00:00:01 |
|   7 |  TABLE ACCESS FULL              | SALES       |    12 |   348 |       3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("S2"."COMPANY"=:B1 AND "S2"."YEAR"= (SELECT "YEAR" FROM "SALES"
              "S3" WHERE "S3"."YEAR"<:B2 AND "S3"."COMPANY"=:B3))
   6 - access("S3"."COMPANY"=:B1 AND "S3"."YEAR"<:B2)

あれ? ウィンドウ関数かんすう使つかったほうのSQL、実行じっこう計画けいかくでウィンドウ関数かんすう処理しょり(WINDOW BUFFER)が1かいしかあらわれませんね。2つ使つかっているから、てっきり2かいあらわれるとおもったのですが。

それはウィンドウ関数かんすうのソートキーがどちらも(company、year)昇順しょうじゅんだからよ。ソートじゅんおなじなら、1かい十分じゅうぶんでしょ。ぎゃくに、もしソートキーがちがえば、ウィンドウ関数かんすうかずだけソート処理しょり必要ひつようになるわ。

そっか。ウィンドウ関数かんすう内部ないぶてきにはソート処理しょりをしていたんだった。うーん、なんかへん気分きぶん

へん


いや、ぼくらはいまループがムダだという理由りゆうでループをふくまないSQLにえたじゃないですか。でも、結局けっきょくDBMSは内部ないぶでSQLをもう一度いちどループに変換へんかんしているんですよね。DBMS内部ないぶでは、結局けっきょく処理しょり手続てつづがた言語げんご実行じっこうされるじゃないですか。結合けつごうのアルゴリズムであるNested Loopsなんかも、まさにループだし。SQLってその意味いみでは、9みたいに、手続てつづがた言語げんごはさまれたサンドイッチみたいだ、おもって。

9 SQLは手続てつづがたのレイヤにはさまれている
図9 SQLは手続き型のレイヤに挟まれている

ループからの脱出だっしゅつ

うまいことをうようになったな。おまえ指摘してきはSQLの微妙びみょう立場たちばをうまくいいあてている。もともと、SQLは手続てつづき」隠蔽いんぺい(いんぺい)するためにかんがされた言語げんごだった。RDBとSQLのみのおやであるCoddはこうっている。

関係かんけい操作そうさでは、関係かんけい全体ぜんたいをまとめて操作そうさ対象たいしょうとする。目的もくてき繰返くりかえ(ループ)をなくすことである。いやしくも末端まったん利用りようしゃ生産せいさんせいかんがえようというのであれば、この条件じょうけんくことはできないし、応用おうようプログラマの生産せいさんせい向上こうじょう有益ゆうえきであることもあきらかである。”

強調きょうちょう筆者ひっしゃ
―E.F.Codd関係かんけいデータベース:生産せいさんせい向上こうじょうのための実用じつようてき基盤きばん
⁠ACMチューリングしょう講演こうえんしゅうあか翻訳ほんやく共立きょうりつ出版しゅっぱん1989ねんp.455

Codd博士はかせはっきりいいきっていますね。⁠ループをなくす」って。なんか自民党じみんとうをぶっこわす」みたいでかっくいー。

アメリカにそんな政党せいとうあった? Coddには、データベースをひろいユーザに提供ていきょうするには、一部いちぶプログラマしか使つかえない言語げんごではなく、だれでもあつかえる言語げんご必要ひつようだ、というおもいがあったのね。SQLの構文こうぶん自然しぜん言語げんご英語えいごせたのも、そういう理由りゆうからでしょうね。

その意味いみで、SQLそのものが手続てつづがた言語げんご隠蔽いんぺいする一種いっしゅのフレームワークというか、ラッパーの役割やくわりたしているわけだ。ところが皮肉ひにくなことに、SQLの上位じょういでさらに手続てつづがた言語げんご使つかって、SQLを隠蔽いんぺいしようとするコードがこうたん。フレームワークを使つかうと、問答もんどう無用むようでこういうループにえられることもおおい。

先生せんせいはフレームワーク否定ひていですか?


べつ否定ひてい肯定こうていもせんよ。一長一短いっちょういったんさ。フレームワークにも処理しょり隠蔽いんぺいによる開発かいはつ効率こうりつ改善かいぜん共通きょうつうによる保守ほしゅせい向上こうじょうなど、メリットはある。ただ、何事なにごとにもトレードオフはあるというだけだ。

あれ、意外いがいにリベラルですね。ぼくはまたてっきり「スクラッチ以外いがい選択肢せんたくしなどありえん!」うかとおもっていました。

まえワシをただの偏屈へんくつオヤジだとおもっているんじゃないだろうな?

ぎくっ!


更新におけるループ依存症

と、ところで、この患者かんじゃべつテーブルへINSERTする処理しょりでしたけど、これがおなじテーブルへのUPDATEだったら、どんなかいになっていたんでしょう。

……では10のようなテーブルSales3を想定そうていしよう。今度こんどはvarれつおなじテーブルないれつとしてっており、これをさっきとおなじルールで更新こうしんする。この問題もんだいをループでくときは、患者かんじゃのコードのINSERTの部分ぶぶんをUPDATEにえるだけだ。では、SQLで場合ばあいはどうする?

10 Sales3テーブル
図10 Sales3テーブル

相関そうかんサブクエリによる更新こうしん

うーんと、相関そうかんサブクエリのほうはこうリスト5……あれ、エラーになった。たんいちぎょうふくわせにより2つ以上いじょうくだりもどされます」って。

たりまえよ。これじゃSalesテーブルの1ぎょうにSales3テーブルから選択せんたくしたぜんくだり対応たいおうさせようとしているのだから。ちゃんと一対一いちたいいち条件じょうけんしぼりなさい。

リスト5 相関そうかんサブクエリによるかいあいだちがい)
UPDATE Sales3
   SET var =(SELECT CASE SIGN(sale - (SELECT MAX(sale) 直前ちょくぜんのレコードとげを比較ひかく
                                        FROM Sales3 S2
                                       WHERE S1.company = S2.company
                                         AND S1.year > S2.year ))
                    WHEN 0 THEN '='
                    WHEN 1 THEN '+'
                    WHEN -1 THEN '-'
                    ELSE NULL END AS var
               FROM Sales3 S1);

そっか。SET相関そうかんサブクエリを使つかえば……できた!リスト6

リスト6 相関そうかんサブクエリを使つかった更新こうしん
UPDATE Sales3
   SET var =(SELECT CASE SIGN(sale - (SELECT MAX(sale) 直前ちょくぜんのレコードとげを比較ひかく
                                        FROM Sales3 S2
                                       WHERE S1.company = S2.company
                                         AND S2.year =
                                          (SELECT MAX(year) 直近ちょっきんとし選択せんたく
                                             FROM Sales3 S3
                                            WHERE S1.company = S3.company
                                              AND S1.year > S3.year )))
                    WHEN 0  THEN '='
                    WHEN 1  THEN '+'
                    WHEN -1 THEN '-'
                    ELSE NULL END AS var
               FROM Sales3 S1
              WHERE Sales3.company = S1.company
                AND Sales3.year = S1.year);    ┘―更新こうしんさきのレコードと更新こうしんもとのレコードを一対一いちたいいち対応たいおうさせる

正解せいかいね。


ウィンドウ関数かんすうによる更新こうしん

じゃあ、ウィンドウ関数かんすうのほうはどうなる?


こんなんでどうでしょうリスト7⁠。単純たんじゅんにSETでウィンドウ関数かんすう部分ぶぶんだけ代入だいにゅうしてみたんですけど。

リスト7 ウィンドウ関数かんすう使つかった更新こうしん(DB2でのみOK)
UPDATE Sales3
   SET var = CASE WHEN MAX(company) 直前ちょくぜんのレコードがおな会社かいしゃのレコードの場合ばあい
                          OVER (ORDER BY company, year
                                ROWS BETWEEN 1 PRECEDING
                                         AND 1 PRECEDING) = company
                  THEN CASE SIGN(sale - MAX(sale) 直前ちょくぜんのレコードとげを比較ひかく
                                           OVER (ORDER BY company, year
                                                  ROWS BETWEEN 1 PRECEDING
                                                           AND 1 PRECEDING) )
                       WHEN 0 THEN '='
                       WHEN 1 THEN '+'
                       WHEN -1 THEN '-'
                       ELSE NULL END
             ELSE NULL END ;

基本きほんてきにはこれでOKよ。留保りゅうほつきなのは、このコードがとおるのはDB2だけだから。OracleはSETでのウィンドウ関数かんすう使用しようみとめていないので、これだとエラーになるわ。Oracleの場合ばあい一段いちだんサブクエリをあいだにかませる必要ひつようがあるのリスト8⁠。

リスト8 Oracleの場合ばあい
UPDATE Sales3
   SET var = (SELECT var
        FROM (SELECT company,―サブクエリをあいだにかませる
                     year,
                     CASE WHEN MAX(company) 直前ちょくぜんのレコードがおな会社かいしゃのレコードの場合ばあい
                                  OVER (ORDER BY company, year
                                         ROWS BETWEEN 1 PRECEDING
                                                  AND 1 PRECEDING) = company
                          THEN CASE SIGN(sale - MAX(sale) 直前ちょくぜんのレコードとげを比較ひかく
                                                   OVER (ORDER BY company, year
                                                          ROWS BETWEEN 1 PRECEDING
                                                                   AND 1 PRECEDING) )
                               WHEN 0 THEN '='
                               WHEN 1 THEN '+'
                               WHEN -1 THEN '-'
                               ELSE NULL END
                     ELSE NULL END AS var
                FROM Sales3) TMP
        WHERE Sales3.company = TMP.company
          AND Sales3.year = TMP.year);

あきらかに外側そとがわのサブクエリは冗長じょうちょうですね。


ま、これにかんしちゃOracleにさらなる改善かいぜん期待きたいするしかないな。


ループを使うのは悪いことか症

もし、Salesテーブルがファイルで、データがCSV形式けいしき格納かくのうされていたとします。その場合ばあい今回こんかいかんがえたような処理しょり実現じつげんするには、ループはほとんど唯一ゆいいつ選択肢せんたくしです。しかし、RDBにおけるテーブルはファイルではありません11⁠。それはむしろ、雑多ざったなコインがはいった財布さいふみたいなもので、財布さいふなかではコインは順序じゅんじょづけられて格納かくのうされていません。テーブルのレコードには順序じゅんじょがないためです。したがって、SQLでは「1レコードずつ順次じゅんじアクセスする」という発想はっそうがありません。つねに、複数ふくすうのレコードをまとめて処理しょりしようとします。

11 テーブルはファイルではない
図11 テーブルはファイルではない

しかし、SQLのこのようなかんがかたは、手続てつづがた言語げんごのスキームでそだったエンジニアには特異とくいかんじられます。したがって、SQLよりも手続てつづがた言語げんごがわ処理しょりせようとする発想はっそうは、自然しぜんなものです。そしてこのかんがえは、一概いちがいわるいものではありません。ロバートがうように、どちらにも一長一短いっちょういったんあるため、よくかんがえる必要ひつようがあります。それぞれのメリット・デメリットを比較ひかくしてみましょう。

手続てつづがた言語げんごてきかた(ループ)積極せっきょくてき使つか場合ばあい

まず、手続てつづがた言語げんご業務ぎょうむロジックをせて、SQLをライトにする方法ほうほうのメリットは、手続てつづがた言語げんごかんがかたさえっていれば、だれでもコーディングできることです。開発かいはつメンバーにSQLにくわしい人間にんげんがいなくても、アプリケーションの品質ひんしつ担保たんぽできます。

一方いっぽうこの方法ほうほうのデメリットは、性能せいのうないことです。レイヤをかさねるわけですから、おおくのオーバーヘッドがてしまうことはけられませんし、SQLを単純たんじゅんすると、チューニングの手段しゅだん非常ひじょうかぎられます。たとえば、今回こんかいれいのように単純たんじゅんなINSERTぶんというのは、それ自体じたいをチューニングする選択肢せんたくしはほとんどありません[3]⁠。

つまり、ループ処理しょり遅延ちえんするケースというのは、1かいあたりの処理しょり時間じかんみじか処理しょりもりもっておそくなる「チリツモ」かたがほとんどであり、これを高速こうそくするにはアプリケーションのロジックにまなければならないのです。

また、アプリケーションそうおおくのループをおこな場合ばあい実装じっそうにもよりますが、アプリケーションサーバのリソースをおお消費しょうひすることになるでしょう。一般いっぱんてきに、アプリケーションサーバはデータベースサーバよりも貧弱ひんじゃくなリソースしかっていないことがおおいため、よわたまき(わ⁠⁠」にたか負荷ふかをかけてしまうことになりがちで、これも欠点けってんひとつです[4]⁠。

SQLのみで記述きじゅつする場合ばあい

一方いっぽうSQLのみで処理しょり実現じつげんする場合ばあいは、その長所ちょうしょ短所たんしょ裏返うらがえしです。

SQLで記述きじゅつすることのメリットはパフォーマンスにあります。フレームワークを使つか場合ばあいよりもレイヤが1つすくないぶんオーバーヘッドがりますし、SQLはチューニングポテンシャルのたか言語げんごで、性能せいのう改善かいぜん選択肢せんたくし豊富ほうふです。DBMSの進歩しんぽ日進月歩にっしんげっぽで、せっかくベンダーが日夜にちやんでいるSQL高速こうそく恩恵おんけいをあえてけないというのも、そんはなしです。

デメリットは、複雑ふくざつ処理しょりをSQL単体たんたい実現じつげんしようとすると、そのSQLはたいへん長大ちょうだいみにくいものになります。SQLは手続てつづがた言語げんごちがってモジュール分割ぶんかつはできません。たとえば32だん分岐ぶんきったSQLというのは、むにこたえない代物しろものになるでしょう。

また、SQLというのはコーディングする人間にんげんのレベルがかなり顕著けんちょ言語げんごです。SQLに業務ぎょうむロジックをむと、品質ひんしつ水準すいじゅんにバラつきがしょうじるリスクをうことになります。残念ざんねんなことに、SQLはCoodが期待きたいしたほどだれもがはなせる言語げんごにはなりませんでした。

トレードオフをかんがえる

このように、業務ぎょうむロジックをアプリケーションとSQLのどちらにどのように配分はいぶんするか、という問題もんだいにはつねにトレードオフが存在そんざいしており、最適さいてきかいはそれぞれの開発かいはつプロジェクトの事情じじょうによってわってきます。冒頭ぼうとうでワイリーがからだって(?⁠⁠、治療ちりょうめようとしていましたが、それが正解せいかいになるケースもあるのです。

ただ、ひとえることは、武器ぶきおおいほうがよいということです。どちらか一方いっぽうしか選択せんたくできない場合ばあいそれがダメだった場合ばあい進退しんたいきわまります。状況じょうきょうおうじて選択肢せんたくしえられるしのおおさとアタマの柔軟じゅうなんさが、すぐれたエンジニアにもとめられる資質ししつではないでしょうか。

参考さんこう資料しりょう

1.ミック『SQL ゼロからはじめるデータベース操作そうさしょうおよげしゃ2010ねん
だい8しょう SQLで高度こうど処理しょりおこなう」でウィンドウ関数かんすうかんがかたについて、くわしく解説かいせつしています。⁠ROWS BETWEEN」オプションの使つかかたについてもれています。
2.ミック WEB+DBVol.55 連載れんさい「SQLアタマアカデミー」最終さいしゅうかい「OLAP関数かんすう強力きょうりょく統計とうけい処理しょり実現じつげん!」
ウィンドウ関数かんすう基礎きそてき使つかかたについてはこのかい参照さんしょうしてください。なお「OLAP関数かんすうとは、ウィンドウ関数かんすう(ややふるい)別名べつめいです。gihyo.jpでも公開こうかいしております。
3.ミック相関そうかんサブクエリでくだりくだり比較ひかくする
相関そうかんサブクエリによる行間ぎょうかん比較ひかく解説かいせつはこちらをどうぞ。

おすすめ記事きじ

記事きじ・ニュース一覧いちらん