SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

達人に学ぶSQL

SQLで集合演算

集合指向言語としてのSQL:その4


  • X ポスト
  • このエントリーをはてなブックマークに追加

ダウンロード サンプルコード (1.9 KB)

テーブル同士のコンペア―集合の相等性チェック:応用編

 前の解法では、テーブルの比較をするのに、事前準備として2つのテーブルの行数を調べておく必要がありました。それほど大きな手間ではありませんが、この準備なしで、いきなり相等性チェックを行えるような改良版を考えましょう。サンプルは、さっきの「tbl_A」と「tbl_B」を使います。

 集合論では、一般的に集合の相等性を調べる公式として、以下の2つが知られています。

  1. (A ⊆ B ) かつ (A ⊇ B) ⇒ (A = B)
  2. (A ∪ B ) = (A ∩ B) ⇒ (A = B)

 1番の方法は、集合の包含関係をもとに相等性を調べる方法です。「AがBを含み、かつBがAを含むなら、両者は等しい」という意味です。これを利用することも可能ですが、少し面倒です(この方法については、後の例題で触れます)。

 一方、2番の方法は、集合の和と交差をもとに相等性を調べる公式です。SQLに翻訳すれば、「A UNION B = A INTERSECT Bなら、AとBは互いに等しい」ということです。こちらの方が簡単に書けそうです。

 AとBが等しい集合なら、「A UNION B = A = B」でした。そして実は、INTERSECTにも、AとBが等しければ、「A INTERSECT B = A = B」が成り立ちます。そう、UNION以外に冪等性が成立する演算子とは、INTERSECTのことです。

 反対に、A ≠ Bであれば、UNIONINTERSECTの結果は異なります。UNIONの方が絶対に行数が多くなります。最初は異なる集合だったAとBが、次第に接近するアニメーションを想像すると分かりやすいでしょう。

 残る問題は、UNIONINTERSECTの結果をどう比較するかですが、いま常に、

( A INTERSECT B ) ⊆ ( A UNION B )

 であることが分かっています。従って、( A UNION B ) EXCEPT ( A INTERSECT B )の結果が空集合かどうかという判定をすればOKです。A = Bのときに空集合になり、A ≠ Bのときに1行以上の行が残ります。

2つのテーブルが相等なら「等しい」、そうでなければ「異なる」を返すクエリ
SELECT DISTINCT CASE WHEN COUNT(*) = 0
                     THEN '等しい'
                     ELSE '異なる' END AS result
  FROM ((SELECT * FROM  tbl_A
         UNION
         SELECT * FROM  tbl_B)
         EXCEPT
        (SELECT * FROM  tbl_A
         INTERSECT
         SELECT * FROM  tbl_B)) TMP;

 このクエリもやはり、列名や列数は一切知る必要がなく、NULLを含むテーブルにも使えるという前問と同じ利点を兼ね備えており、さらに、行数を調べる事前準備が不要というすぐれものです。ただし高機能にした分、欠点も発生します。集合演算3回+DISTINCT1回 の計4回のソートが発生するため、パフォーマンスが落ちます(そう頻繁に実行するタイプのクエリではないでしょうから、このぐらいは許容範囲だと思いますが)。また、INTERSECTEXCEPTを使うので、現時点ではMySQLでは使えません。利点と欠点を検討して、前問のクエリと使い分けてください。

 さて、実際に2つのテーブルが相違することが分かったら、今度は相違した行を具体的に表示しましょう。ファイルに対するdiffコマンドを、テーブルに行うイメージです。これは、排他的和集合を選択すればいいので、次のようになります。

テーブル同士のdiff
(SELECT * FROM  tbl_A
 EXCEPT
 SELECT * FROM  tbl_B)
UNION ALL
(SELECT * FROM  tbl_B
 EXCEPT
 SELECT * FROM  tbl_A);
結果
key     col_1     col_2     col_3
-----   -------   -------   -------
B           0         7         9
B           0         7         8

 A-BとB-Aの間に共通部分は存在しえないので、両者をマージするときは「UNION ALL」でかまいません。このクエリは、片方の集合がもう一方の部分集合である場合にも正しく動きます(その場合は、A-BとB-Aのどちらかが空集合になります)。括弧は演算の順序を指定する極めて重要なものなので、外すと正しい結果が得られません。

 それでは、ここで読者に演習問題を出しましょう。実は、前問のUNIONだけを使うクエリも、少し強引な修正を加えることで、事前準備なしで使えるように改善できます。どんな修正を加えればよいか、考えてみてください。

差集合で関係除算を表現する

 導入で述べたように、SQLにはまだ除算用の演算子がありません。そのため、除算を行うには自前でクエリを書く必要があります。方法は数多くありますが、代表的なものとしては、

  1. NOT EXISTSを入れ子にする
  2. HAVING句を使った1対1対応を利用する
  3. 割り算を引き算で表現する

 の3通りが挙げられます。今回は、3番の方法を解説します。

 集合代数における引き算とは、差集合演算のことです。以前、「外部結合の使い方」で、差集合を外部結合で書く方法を紹介したときに、この方法を考えてもらう問題を出しましたが、今回はその解答と解説です。

 サンプル・データは、社員の技術情報を管理する次の2つのテーブルを使います。

 問題は、「EmpSkills」テーブルから、「Skills」テーブルの技術すべてに精通した社員を探すことです。すなわち、答えは相田氏と神崎氏です。平井氏も惜しいのですが、Javaが使えないので選外です。

 今回紹介する方法は、HAVINGを使う方法より理解しやすいかもしれません。というのも、見方によっては非常に手続き型に近い発想をするからです。まずは、答えを見てもらいましょう。

差集合で関係除算(剰余を持った除算)
SELECT DISTINCT emp
  FROM EmpSkills ES1
 WHERE NOT EXISTS
        (SELECT skill
           FROM Skills
         EXCEPT
         SELECT skill
           FROM EmpSkills ES2
          WHERE ES1.emp = ES2.emp);
結果
emp
------
相田
神崎

 ポイントは、EXCEPT演算子と相関サブクエリにあります。相関サブクエリは、同じ「EmpSkills」テーブルを関係づけていますが、これは、引き算を社員ごとに行うためです。社員ごとのスキルを、要求されるスキルの集合から引き算して、結果が空集合(φ)なら全部備えていた、残る行があれば足りないスキルがあった、ということです。

 例えば相田氏については、

 となり、結果が空集合なので合格です。他方、例えば平井氏の場合、

 となり、「Java」の行が残るので、結果に含まれません。いわば、処理単位を社員ごとに分割して、割り算よりも簡単な引き算に還元して解いているわけです。「困難は分割せよ」の格言に則った、巧みな解法です。

 ところで、このロジックを見て、何か気付いたところはないでしょうか? 実はこのクエリは、手続き型言語で言うコントロール・ブレイクに似た処理を記述しているのです。試しに、この2つのテーブルがファイルで、1行づつループして処理をすると考えてください。ある社員について、スキルが存在している間は引き算を行い、スキルがなくなったら次の店舗に移り、同じ作業を繰り返します。

 相関サブクエリは、よく知られているように、ループの代用としてSQLに導入されました。この解法の方が理解しやすいかもしれない、と言ったのは、こういう意味です。

 それでは、ここでも演習問題を出しましょう。上のクエリを「厳密な除算」をするよう修正してください(「厳密な除算」の定義、覚えていますか?)。 今度は過不足なくスキルを満たす社員だけを選択するので、結果は神崎氏ひとりになります。答えはサンプルコードに載せています。

次のページ
等しい部分集合を見つける

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
達人に学ぶSQL連載記事一覧

もっと読む

この記事の著者

ミック(ミック)

日本では、主にBI/DWHの設計からチューニングまでを専門とするデータベースエンジニアとして活動。2018年より米国シリコンバレーに活動拠点を移し、技術調査とビジネス開発に従事している。主な著書・訳書:『達人に学ぶSQL徹底指南書 第2版』(2018)『SQL実践入門』(2015)Joe Celko『プログラマのためのSQL 第4版』(2015)翔泳社 - 著者ページ:https://www.shoeisha.co.jp/book/author/3964著者個人ページ:http://mickindex.sakura.ne.jp/

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/1304 2007/10/09 13:03

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング