この記事は?
普段 SQL を書くときに意識していることを整理してみた(業務では分析クエリを書くことが多い)。
具体的な SQL の説明、How to 的な内容は載せていない(一部具体例として載せている)。
本記事の内容は、以下の本を読んで自分なりに整理したものなので、気になる方はぜひ!
前提知識として押さえておくこと
メンタルモデルを認識する
まずメンタルモデル(基本的な考え方の枠組み)について認識する必要がある。
SQL は「集合指向」という考え方に基づいて設計された言語。一方で、多くのエンジニアが最初に学ぶのは手続き型言語。
最初に学んだ言語の考え方がメンタルモデルとして固定され、それを通して世界を見るようになるため、無意識のうちに異なる考え方を持つ言語の理解を妨げてしまう。なので、既に身に付いている無意識のメンタルモデルを切り替えて、「集合指向」という考え方を習得していく必要がある。
以降、既存のメンタルモデルが手続き型言語によって構築されているものとして、両者を比較する形で説明していく。
還元論と全体論のイメージを掴む
メンタルモデルの違いについて、少し抽象的な表現で説明する(個人的にはとてもしっくり来たので)。
全体を分解して捉える。複雑で理解困難な対象も、具体的な要素に分解して単純化すれば理解できるという考え方。
具体的に言えば、
- 代入・分岐・ループという基本的な処理単位に分割していく
- 大量データをレコードという小さな単位に分割して扱うファイルシステム
全体を全体として捉える。体系とそれを構成する要素は、部分の集合としてではなく、全体として捉えるべきだという考え方。
手続き型言語の特徴と比較すると、
- SQL には代入やループなどの手続きは現われない
- データもレコードではなく、もっと複合的な集合の単位として扱う
還元論と全体論について補足しておく。これら考え方はどちらが正しいというものではなく、適材適所で使っていくことが大切だと理解している。還元論に基づいて、分解した具体的な要素をすべて理解しても、対象の全体を理解できるとは限らない。全体を要素に分解する・要素を全体に再構築するときに、「重要ではない」と考えられた要素は切り捨てられているかもしれない(本当に「重要ではない」ことは保証されていない)。その部分を見落とさないよう、全体論に基づいて、体系とそれを構成する要素を、部分の集合としてではなく、全体として捉えてみる。この辺りの話はこの記事にまとまっているので気になっている方はぜひ(このブログためになる)。
基礎理論を押さえる
SQL と RDB を支える基礎理論は大きく 2 つあり、SQL はそれぞれの側面を併せ持つ。
- 数学の一分野である集合論
- 現代論理学の標準的な体系である述語論理
つまり、全てを集合と関数でやろうとするのが SQLの発想と言える。
ちなみに、手続き型言語における処理は、SQL で以下の機能を使って表現できる。
- 分岐
- CASE 式
- ループ
- 相関サブクエリ(ループクエリ)
- window 関数
- GROUP BY 句(集合演算)
なので、一見手続き的にしか解けないと思うような問題も SQL に落とし込むことができる。
以降、「集合指向」「関数指向」それぞれの側面で具体的なポイントを並べる。
集合指向としての側面
テーブルは集合である
処理単位を「行」ではなく「(行の)集合」として記述する。
SQL は集合操作の機能が充実しているため、手続き型言語であればループや分岐を使って記述する複雑な処理を、非常に簡単で見通し良く記述することができる。
例えば、以下テーブルについて、歯抜けデータを探すことを考える。
SeqTbl テーブル
seq | name |
---|---|
1 | ichiro |
2 | ziro |
4 | siro |
仮にこのテーブルがファイルで、手続き型言語を使って調べるなら、次のような手順になる。
- 連番の昇順か降順にソートする
- ソートキーの昇順(または降順)にループさせて、1 行ずつ次の行と seq 列の値を比較する
この単純な手順の中にも、手続き型言語とファイルシステムの特徴が浮き彫りになっている。ファイルのレコードは順序を持ち、それを扱うためにプログラミング言語はソートを行なう。
代わりに SQL は、複数行をひとまとめにして集合として扱う(テーブル全体を 1 つの 集合 と見なす)
-- 欠番があれば'歯抜けあり'を返す SELECT '歯抜けあり' AS gap FROM SeqTbl HAVING COUNT(*) <> MAX(seq) - MIN(seq) + 1 ;
このクエリを集合論の言葉で表現すると、自然数の集合(歯抜けのない連番)と SeqTbl 集合(SeqTbl に実際に含まれている行数)の間に 一対一対応(全単射)が存在するかどうかをテストしている、ということになる。
閉包性を意識する
テーブルはただの集合ではなく、閉包性を持っている。
閉包性は、端的に言うと「演算子の入力と出力が共にテーブルになる(テーブルの世界が閉じていることを保証する)」という性質のこと。
SELECT 句は、テーブルを引数にとってテーブルを返す関数として捉えられ、サブクエリやビュー利用の前提になっている。
他にも閉包性の例として、UNIX のファイル・シェルコマンドが挙げられる。UNIX は、デバイスからコンソールまで全て「ファイル」として扱っている(プリンターやディスプレイのような物理的なデバイスも、/dev ディレクトリの下にある普通のファイルのように見える)。これらファイルが、様々なコマンドに対して入力・出力になることで、UNIX のシェルプログラミングに高い柔軟性を与えている(例. cat test.txt | sort +1 | more
)。
また、テーブルの閉包性は、代数構造のうち「体」の条件もクリアしているため、「自由に四則演算が可能な集合」と考えることが可能(和の射影や制限の差等が行える)。数学的に厳密な基礎付けを持つことで、集合論や群論など、すでに多くの実績の積み重ねがある分野の成果を援用している。
四角を描くな、円を描け
集合指向を身に付ける近道として、入れ子集合をイメージする(データを集合の観点から把握する)ことが 1 つの鍵になる。
- GROUP BY・PARTITION BY 句によって、テーブルを部分集合に切り分ける
- HAVING 句によって、集合単位の条件を設定する(WHERE 句と違って、集合そのものに対する条件を設定できる!)
それらを的確に表わす視覚図は、「ベン図」つまり「円」になる。
手続き型言語においての視覚的なツール(構造図や DFD 等)では、手続きが箱(四角)・データの流れが矢印で表現されるが、この伝統的な道具は SQL には不向き。
関数指向としての側面
処理の分岐は「式」で行う
手続き型言語では、処理の分岐は「文」の単位で行なうが、SQL では「式」の単位で行う。
具体的には、IF 文や CASE 文は、CASE 式で置き換える。
CASE 式は、最終的に 1 つの値に定まるために、他の式や関数の引数に取ることもできる。
一階述語論理の導入
SQL のEXISTS 述語が一階の存在までしか引数に取れないため、SQL が採用している述語論理は「一階述語論理」になる。
RDB における存在の階層は以下になる。
- 0 階:行
- 一階:テーブル(行集合)
- 二階:テーブルの集合
述語論理において「複数の対象を一つの入力として扱う道具」である「量化子」「全称量化子」は、SQL では前者を EXISTS、後者を NOT EXISTS を使って表現する。
特性関数と組み合わせる
特性関数とは、各要素(行)が特定の条件を満たす集合に含まれるかどうかを決める関数のこと(集合を定義するという意味で定義関数と呼ぶ)。
例えば、条件を満たす行については 1・そうでない行については 0 を返す CASE 式 は、「ある行が条件を満たすかどうかを判別する関数」を作っていると言える。
CASE 式による特性関数と HAVING句 組み合わせることで、集合の性質を詳細に調べることができる。
例えば、以下テーブルについて、女子の平均点が男子の平均点より高いクラスを探すことを考える。
TestResults
student_id | class | sex | score |
---|---|---|---|
1 | A | 男 | 70 |
2 | A | 女 | 100 |
3 | A | 女 | 50 |
4 | B | 男 | 100 |
5 | B | 男 | 50 |
6 | C | 女 | 100 |
7 | C | 女 | 50 |
-- 男子と女子の平均点を比較するクエリ:その2 空集合に対する平均をNULLで返す -- B,C クラスは比較対象が存在しないため対象外になる SELECT class FROM TestResults GROUP BY class HAVING AVG(CASE WHEN sex = '男' THEN score ELSE NULL END) < AVG(CASE WHEN sex = '女' THEN score ELSE NULL END);
FROM 句から書く
上記までの内容を意識した上で、自分が SQL を書く際に必ず実践していることが一つある。
それは、SQL の実行順序に沿って、FROM 句から書くこと。
具体的には、FROM → WHERE → GROUP BY → HAVING → SELECT(→ ORDER BY)
の順番で書いていく。
複雑な SQL を書く場合、いきなり SELECT 句から書くより、実行順序に沿って FROM 句から書いたほうが自然にロジックを追える。SELECT 句がやっているのは、表示用に見た目を整形したり、計算列を算出することであり、より重要な WHERE、GROUP BY、HAVING から考えていく。
SQL のメンタルモデルが養成されていれば、FROM 句から書いていくことで、集合を操作している過程がイメージできると思う。
終わりに
自分は、言語的にも利便性からも SQL が好き。
SQL的な観点から考えることを学ぶことは、多くのプログラマにとって1つの飛躍である。きっとあなた方の多くは、そのキャリアの大半を手続き型のコードを書いて過ごしてきたことだろう。そしてある日突然、非‐手続き型のコードに取り組まねばならなくなる。そこで肝心なのは、順序から集合へ思考パターンを変えることだ。 J.セルコ
達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ 19章より
サピア=ウォーフの仮説 的なことを体感していて、それも新しい発見だった。