ホームリレーショナル・データベースの世界セルコ・セレクション


和をもって貴しとなす

(DBAzine (2005/04/20), "Set Operations")



はじめに

 SQL の基礎をなす理論の一つが集合論です。関係モデルの創始者コッドは、テーブルを行集合と解釈することによって、データ操作にダイレクトに集合論を適用することを可能としました。それゆえ、SQL には集合指向言語という名前が冠せられており、コーディングの際にもテーブルを集合と見なす発想が非常に重要です。
 ところが、集合指向言語としての SQL は、これまで決して十分な力を備えていたとは言えません。SQL-92 が制定されるまで、SQL は共通部分(INTERSECT)や差(EXCEPT)といった、高校で習う程度の基本的な集合演算子さえ持っていなかったからです。しかも、SQL が扱うテーブルは、数学の集合と違って重複を許す多重集合であるため、集合論と完全に類比的には考えられないという欠陥があります。SQL プログラマは、ALL と DISTINCT という修飾子を使って集合と多重集合を制御することもできますが、SQL のデフォルトは ALL が選択されているので、多くの場合は多重集合のもたらす災難に悩まされるハメになります。デイトのように「デフォルトで DISTINCT を指定するべきだ」という過激な主張をする論者もいますが、はっきり言って一般的な支持は得られていません(それでも、彼がなぜそういう主張をするか、という理由は真剣な考慮に値するのですが)[1]
 本稿は、SQL が持つ集合演算子の中でも、最も基本的で重要な 和(UNION)について、その重要な性質と使用の際に注意するべきポイントを解説するものです。



UNION の性質

 UNION は SQL-86 から標準に存在する最古参です。それだけこの演算子が重要だということで、UNION を用いることで SQL の表現力は非常に強力なものとなります。他の演算子のかなりのものが、UNION を使って作ることができるからです。かつて 1992年にマイクロソフト社が MS-Access を世に送り出したとき、UNION を持っていなかったことで一部のユーザから不満が寄せられたことがありました。今の Access はもちろん UNION を持っていますが、それぐらい重要な演算子だ、ということです。
 UNION の構文は

     クエリ UNION クエリ

という形を取ります。たたし、左と右のクエリが作るテーブルは「UNION 互換」である必要があります。UNION 互換とは、以下の二つの条件を共に満たすことです。
  1. 列の数が同一であること
  2. 同じ列位置の列のデータ型が同じ(または自動的に型変換可能)であること
この条件は、常識的に考えて納得のいくものでしょう。テーブル1とテーブル2で列数が異なっていた場合、足りない方のテーブルの列に勝手に NULL を補ってくれる、というような器用な真似はしません(多分そういう発想で SQL-92 では 「UNION結合」なる操作が定義されていますが、これはまた別の話)。また、UNION 時の型変換は実装依存の機能なので、基本的にはデータ型が完全に一致するよう、プログラマが配慮しておくべきです。
 UNION の結果作られるテーブル(より厳密には関係)の面白い性質は、そのテーブルも列も名前を持たないという点です。名前を与えたいならば、以下のように AS 演算子を使う必要があります。

  --ユーザが命名する必要がある
    ((SELECT a, b, c FROM TableA WHERE city = 'Boston')
      UNION (SELECT x, y, z FROM TableB WHERE city = 'New York'))
  AS Cities (tom, dick, harry)

 もっとも、実際の DBMS は次の四つの選択肢のうちの一つに従って列へのアクセスを行なおうとするでしょう。
  1. 最初のテーブルの列名を使用する。
  2. 最後のテーブルの列名を使用する。
  3. SQL エンジンが生成する列名を使用する。
  4. 位置番号で参照できるようにする。これは SQL-89 の規定。
 皆さんが使っている DBMS がどの方法を採用しているかは、調べておくべきでしょう。

 また、UNION は可換的であるため[2]、テーブルを並べる順番を気にする必要はありません ・・・・・・ 理論的には。現実には、テーブルの並び順はパフォーマンスに影響を及ぼす可能性があり、悩ましいところです。例えば、次のような三つのテーブルを UNION するケースを考えます。

  (TABLE SmallTable1)     --小テーブル1
  UNION
  (TABLE BigTable)       --大テーブル
  UNION
  (TABLE SmallTable2);    --小テーブル2

 素直に上から順に実行すると、まず小テーブル1と大テーブルをマージし、その結果と小テーブル2をマージすることになります。これは、次のように、小テーブル同士を最初にマージする書き方よりも遅くなるかもしれません。

  (TABLE SmallTable1)     --小テーブル1
  UNION
  (TABLE SmallTable2);  --小テーブル2:大テーブルと入れ替えた
  UNION
  (TABLE BigTable)       --大テーブル

 なぜなら、最初の例だと、マージは「小-大」+「小-大」という組み合わせになるのに対し、二番目の例だと「小-小」+「小-大」という組み合わせになるからです。中には、テーブルのサイズに応じて テーブル の順番を入れ替えて実行するという最適化を行なっている DBMS もあるようですが、一般的にはあまり期待できません。複数の UNION を使うときの最適化がおざなりにされている理由として、セルコは、そういう演算は例外的なケースなので重要視されていないという理由と、UNION と UNION ALL が混合された場合には、実行順序が操作全体の結果に影響を及ぼすからだ、という二つを挙げています。この第二の理由は重要なので考えてみる必要がありますが、そのためには、まず UNION ALL についての説明をしましょう。



UNION と UNION ALL

 実は UNION には二つの形式が存在します。一つが、今まで使ってきた UNION。これは集合論の和集合に正確に対応します。もう一つが、UNION ALL。こちらは、SQL のテーブルが多重集合であるために導入された、SQL 独特の演算子です。両者の違いは、UNION は入力となる二つのテーブルに重複行が存在した場合、それらを排除して一意な結果を出力するのに対し、UNION ALL は重複行をそのまま保存して多重集合の結果を出力するという点です。そうすると、UNION の方を正確に名づけるなら UNION DISTINCT とするべきであるような気がしますが、そういう書き方はありません(これは INTERSECT や EXCEPT でも同様)。
 ここまで聞くと、どちらかというと UNION ALL というのはあまり使用することが推奨されない例外的な演算子であるように思われるかもしれません。重複行を極力排除するという関係モデルの掟に従うなら、その感想は自然なものです。ところが、必ずしもそういうわけでもありません。というのも、UNION は重複行を排除するためにソートを必要としますが、UNION ALL はそうではないという大きな相違が存在するからです。それゆえ、パフォーマンスを確保するために UNION ALL を選択するのがよいケースがあります[3]

 さて、それでは前節で問題となっていた、UNION と UNION ALL を混合した場合に、演算順序が結果に影響を与えるケースを考えます。UNION も UNION ALL もそれぞれ単独で使用しているときはこういう問題は起きず、結合法則を満たしています。ところがこれらを混合すると 問題が起きます。例えば、次の二つのクエリは結果が異なります。

  --UNION を先に実行
  (TABLE X UNION TABLE Y)
  UNION ALL
  TABLE Z

  --UNION ALL を先に実行
  TABLE X
  UNION
  (TABLE Y UNION ALL TABLE Z)

 具体的には、テーブル Y と Z が X の部分集合であるようなテーブルを考えると分かりやすいでしょう。
X
col
1
2
3
Y
col
1
Z
col
2


 最初のクエリでは、X UNION Y が先に実行されるので、中間結果では重複が排除されて { 1, 2, 3 } 、それに対して Z を付加するので、最終結果は重複が残る { 1, 2, 2, 3 }。一方、後のクエリでは、Y UNION ALL Z が先に実行されるので、中間結果では { 1, 2 } 、しかし最終的には X と UNION するため重複が排除されて { 1, 2, 3 }。このように、UNION と UNION ALL を組み合わせる場合(そう多いとは思いませんが)は注意が必要ですし、これが UNION の最適化を阻む大きな要因になっているということも、理解しておきましょう。全く、多重集合を許すというのはデメリットの大きい選択肢です。デイトが DISTINCT に固執するわけです。



まとめ

 以上で、UNION を使用するときに満たすべき条件(UNION 互換)、その基本的な性質(結果が無名、可換的)、UNION ALL との違い(重複排除)について解説してきました。UNION はとても基本的かつ重要な演算子なので、よく理解しておいてください。

 最後に、セルコが言及していない UNION の性質で、面白いものを一つ紹介しましょう。それは、冪等性(べきとうせい:idempotence)です。これは、同じ操作を何度実行しても、常に同じ結果を得るということです。プログラミングの色々な局面で、この性質を保つことが重要な意味を持ちます。例えば、HTTP の GET メソッドは冪等性を持つよう設計されています。それによって、同じ要求を安全に繰り返し発行させることが可能になっています。
 さて、UNION の場合、A UNION A = A という冪等性が成立します。同一のテーブルに対して、何度 UNION を適用しても結果はもとのテーブル A だよ、ということです(ただし A に重複行がない場合に限る)。UNION ALL だと、この冪等性は成立しません。違いはもうお分かりでしょう、ポイントは重複排除。テーブル A に対して UNION ALL を次々に適用すると、結果の行数は膨れ上がっていきます。

 ・・・・・・ それは分かったけど、この性質が何かの役に立つの?

 もちろん、役に立ちますとも。試しに「二つのテーブルをコンペアする」を見ていただければ、その有用性の一端が垣間見えるでしょう。同一のテーブル、というか集合に UNION を使ったなら、行数が増えるはずがないのです[4]




[1] C.J.デイト『C.J.Dateのデータベース実践講義』 pp.54-5.

[2] ちなみに UNION は交換法則だけでなく、分配法則と結合法則も満たします。

[3] 「SQLを速くするぞ」の「UNIONではなくUNION ALLを使う」を参照。

[4] UNION の冪等性については、以下から示唆を受けました。 C.J.Date, "Expression Transformation: Part 1 of 2" in Relational Database Writings 1991-1994, p.64


Copyright (C) ミック
作成日:2006/07/17
最終更新日:2006/07/18
戻る