SHOEISHA iD

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

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

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

達人に学ぶSQL

SQLで数列を扱う

SQLにおける順序を持ったデータの取り扱い方


3人なんですけど、座れますか?

 友人同士で旅行にでかける際、列車や飛行機の座席を予約しようとしたら、人数分の連続した空席がなくて、一人だけ離れ小島の席になってしまった――こんな淋しい経験をしたことのある人もいるのではないでしょうか。

 次のような、座席の空席状況を表すテーブルを考えます。

Seats
seat(座席) status(状態)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

 いま、3人で旅行に出かけようと思って、この列車に予約を取ろうとしているとします。問題は、この1~15までの席番号の中から、連続する3個の空席の組み合わせをすべて探すことです。この連続した整数の集合を「シーケンス」と呼ぶことにします。

 上のデータから求めたい結果は、

  • 3 ~ 5
  • 7 ~ 9
  • 8 ~ 10
  • 9 ~ 11

 の四つです。(7, 8, 9, 10, 11)というシーケンスは、部分集合として(7, 8, 9)、(8, 9, 10)、(9, 10, 11)という三つのシーケンスを含みますが、これらも区別して求めます。また、普通こういう座席は途中で行の折り返しが入るものですが、今はそれは無視して、席は直線的に並んでいるとします。

7~11のシーケンスは3つの部分のシーケンスを持つ
7~11のシーケンスは3つの部分のシーケンスを持つ

 上図を使って考えれば、数nを始点として、n + (3 - 1) までの数が、すべて空席の状態にあるということです(マイナス1しないと席を1つ多く取ってしまうので注意してください)。答えは次のようになります。

人数分の空席を探す その1:行の折り返しを考慮しない
SELECT S1.seat   AS start_seat, '~' , S2.seat AS end_seat
  FROM Seats S1, Seats S2
 WHERE S2.seat = S1.seat + (:head_cnt -1)  --始点と終点を決める
   AND NOT EXISTS
          (SELECT *
             FROM Seats S3
            WHERE S3.seat BETWEEN S1.seat AND S2.seat
              AND S3.status <> '空' );

 :head_cntは、座りたい人数を表すパラメータです。パラメータに代入する人数を変えることで、集団の人数が何人でも対応できます。

 このクエリには、SQLで順序集合を扱うときの原理がよく現れています。今から、それを詳しく説明しましょう。このクエリの要点は、二段階に分けて考えると理解しやすくなります。まずは、第一段階、

ステップ1:自己結合で始点と終点の組み合わせを作る。

 このクエリで言えばS2.seat = S1.seat + (:head_cnt -1)の部分に相当します。これによって、「1~8」とか「2~3」のような大きさが3以外の組み合わせを排除して、始点と終点までにちょうど3個の座席を含むシーケンスのみに制限できます。お次は、

ステップ2:始点―終点間のすべての点が満たすべき条件を記述する

 始点と終点が決まったら、今度はその内部の各点が満たすべき条件を記述します。そのために、始点と終点の間を移動する点集合を追加します(上のクエリの「S3」)。移動する範囲を画定するにはBETWEEN述語が便利です。

 今回、シーケンス内の要素が満たすべき条件は、「すべての座席の状態が「空」であること」です。しかし、SQLではこの条件をストレートに記述できません。実は、ここもSQLを考える際のワンポイントなのですが、SQLでは「すべての行が条件Pを満たす」という文を、「条件Pを満たさない行が存在しない」と同値変形して、NOT EXISTS述語を使う必要があるのです。それゆえ、サブクエリ内の条件も「S3.status = '空'」ではなく、その否定形「S3.status <> '空'」となります。この妙な言語仕様の裏事情については、コラム「SQLと量化子」を参照してください。

 では、次に応用版として、行の折り返しも考慮した修正を考えましょう。例えば、この列車が5列で折り返すと仮定します。テーブルに、行の識別子(row_id)を追加します。

Seats2
seat(座席) row_id(行ID) status(状態)
1 A
2 A
3 A
4 A
5 A
6 B
7 B
8 B
9 B
10 B
11 C
12 C
13 C
14 C
15 C

 このケースにおいては、たとえ連番だけ見ればシーケンスをなしていても、(9, 10, 11)のような集合は選択してはいけません。11番に座る人が実質ひとりぼっちになってしまうからです。

9~11は折り返しが入るのでダメ
9~11は折り返しが入るのでダメ

 折り返しに対応するには、シーケンス内の座席がすべて空席であるだけではなく、「すべて同じ行内にある」ことを条件に組み込む必要があります。これは、次のような簡単な修正で可能です。

人数分の空席を探す その2:行の折り返しも考慮する
SELECT S1.seat   AS start_seat, '~' , S2.seat AS end_seat
  FROM Seats2 S1, Seats2 S2
 WHERE S2.seat = S1.seat + (:head_cnt -1)  --始点と終点を決める
   AND NOT EXISTS
          (SELECT *
             FROM Seats2 S3
            WHERE S3.seat BETWEEN S1.seat AND S2.seat
              AND (    S3.status <> '空'
                    OR S3.row_id <> S1.row_id));
結果
start_seat  ‘~’   end_seat
------------  -----  ------------
         3    ~      5
         8    ~     10
        11    ~     13

 シーケンス内の要素が満たすべき条件は「すべての行について、状態が「空」である、かつ、行IDが同じである」です。後半の「行IDが同じ」という条件が追加されたわけですが、これは要するに「始点の行IDと同じである」と同値です(もちろん終点でもかまいません)。それを素直にSQLに訳せば、

 S3.status = '空' AND S3.row_id = S1.row_id

 です。しかし、先述の通り、SQLではこの条件の否定を使わねばなりませんから、

   NOT (S3.status = '空' AND S3.row_id = S1.row_id)
 = S3.status <> '空' OR S3.row_id <> S1.row_id

 という条件になるわけです。この同値変形は、SQLで量化を扱うための必須技術ですから、スムーズにできるようにしてください。

SQLと量子化
これらのことから、形式言語ではEXISTSとFORALLの両方を明示的にサポートする必要がないことが分かる。だが、現実的には、両方をサポートしていることが非常に望ましい。なぜなら、EXISTSで表すほうが「自然な」問題と、FORALLで表すほうが「自然」な問題があるからだ。例えば、SQLはEXISTSをサポートするが、FORALLをサポートしない。結果として、SQLで表現しようとすると、非常にやっかいなクエリが存在する。(C.J.デイト)
  

SQLの基礎の1つ、述語論理には、量化子(限量子、数量詞)という特別な述語が存在します。これは、日本語で言えば「すべてのxが条件Pを満たす」、「条件Pを満たすxが(少なくとも1つ)存在する」という文を書くための道具です。前者が全称量化子、後者が存在量化子と呼ばれ、∀、∃と略記します。すぐにピンときた人もいるでしょうが、SQLのEXISTS述語は、述語論理の存在量化子を実装したものです(まったく同等ではありませんが)。
ところが、SQLはもう一方の全称量化子に対応する述語を持っていません。デイトは自分の本でFORALL述語を導入していますが、現実のSQLにはありません。
ただ、それでSQLの表現力が致命的に不足するかと言うと、そうでもないのです。というのも、全称量化子と存在量化子は、片方が定義されていれば、もう片方をそれによって表現できるからです。具体的には、次のような同値変形の規則(ド・モルガンの法則)があります。

∀xPx = ¬∃x¬Px (全てのxが条件Pを満たす = 条件Pを満たさないxが存在しない)
∃xPx = ¬∀x¬Px (条件Pを満たすxが存在する = 全てのxが条件Pを満たさないわけではない)

これで、なぜ上の問題で「すべての行が条件Pを満たす」という文を、SQLでは「条件Pを満たさない行が存在しない」へ変換する必要があったか、お分かりいただけたでしょう。デイトの言うようにSQLにも全称量化子があれば便利なのですが、こればかりは言語仕様なので仕方ありません。
述語論理と量化子についてさらに知りたい方は、参考文献に挙げた資料を参照してください。

次のページ
最大何人まで座れますか?

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

達人に学ぶ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/

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

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

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/1076 2008/09/03 13:40

イベント

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

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

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

メールバックナンバー