SQL文を組み立てるには?SQLが書ける様になるには?
SQL文ができない人は、どこまでも文法で考えようとしている。
私が、SQLに違和感を感じずに理解することができたのは、文法を気にしてなかったからで、私の勉強法というか、どうやって習得してきたのかを整理してみました。
例えば IN と EXISTS の違い
カラオケで説明します。
カラオケ10万曲から、とある曲リストと一致する曲を抽出するとします。
手続き型では、次のいずれかになるでしょう。(細かいところは端折るよ)
Aパターン
foreach (string 曲名 in 曲リスト){ 結果セット.add(カラオケ.find(曲名)); }
Bパターン
foreach (Song 曲 in カラオケ){ if(曲リスト.exists(曲.曲名)){ 結果セット.add(曲); } }
Bパターンなんて使わないと思いますか?
「とある曲リスト」が「男性ヴォーカリストの曲」でも?
COBOL世代の人は処理時間が
カラオケ.find(曲名) >> 曲リスト.exists(曲.曲名)
となると理解しているので、「とある曲リスト」の件数が、母数に比べてどれぐらいの割合か(インデックスがあるなら25%以上、インデックスがないなら数件以上ならBパターンにすべき)考えてプログラムを変えます。
※ ただ、COBOL世代の人は .find のロジックをそこにベタ書きする人が……(苦笑)
この使い分けが普段からできてない人は、「SQL以前に手続き型でも全然できてない」ってことになってしまうので、もうちょっと修行すべきですね。
Aパターンのプログラムが欲しい
Aパターンのプログラムが欲しいと思って仕様書を書こうとするとどうなるでしょう。
何から取ってくるか書く場所(つまりFROM句)
どんな条件で取るか書く場所(つまりWHERE句)
何が欲しいか書く場所(つまりSELECT句)
結果セットとして欲しいモノは、あくまでもカラオケの曲ですから、何から取るかは「カラオケ」になります。
何が欲しいかは特に指定してない(曲全体)ですから * で良いでしょう。
問題はどんな条件かしかありませんが、Aパターンでは、まず、「曲リスト」が必要になります。
これは外付けでもできますね。
(SELECT 曲名 FROM 曲リスト) = ('逢いたくて', '愛のメモリー', ……)
SELECT * FROM カラオケ WHERE 曲名 IN ('逢いたくて', '愛のメモリー', ……); つまり、 SELECT * FROM カラオケ WHERE 曲名 = '逢いたくて' OR 曲名 = '愛のメモリー' OR ……; つまり、 SELECT * FROM カラオケ WHERE 曲名 = '逢いたくて' UNION ALL SELECT * FROM カラオケ WHERE 曲名 = '愛のメモリー' UNION ALL ……; つまり、 SELECT * FROM カラオケ WHERE 曲名 IN (SELECT 曲名 FROM 曲リスト);
無茶苦茶、書いていますが、どのパターンで仕様書(SQL)を書いても、実行計画(=プログラム)は同じになります。(むしろ、実行前に UNION ALL の SQL に展開することが多いはず)
Bパターンのプログラムが欲しい
FROM句まではAパターンと同じですね。
抽出条件としては、FROM句のカラオケをループしながら「曲リスト」に存在するかチェックして欲しい。
これを表現する手段としてサブクエリの中に、FROM句のカラムを入れ込むのが分かり易いと考えて、その表現を決めました。
SELECT * FROM カラオケ WHERE EXISTS (SELECT * FROM 曲リスト WHERE カラオケ.曲名 = 曲リスト.曲名)
(INで書いてもFROM句のカラムを指定したら同じですが)
サブクエリの中にFROM句のカラムが入っていますから、サブクエリ単独では実行することができないのが特徴です。
一般的な仕様書ならどう表現しても良いので、「Bパターンのプログラムが欲しい」と考えて出来の悪いプログラマ向けに仕様書を書いてみたら良い。その仕様書を、SQLの文法に当て嵌めれば、EXISTSを使ったSQLになるハズです。
文法から考えるのではなく、まずは、どんなプログラムが欲しいか、じっくり考えてみたらどうでしょう。
逆にSQL(仕様書)からプログラムを書く
逆にも考えてみましょう。
あなたが下のSQL(仕様書)を受け取ったとしましょう。
SELECT * FROM カラオケ WHERE 曲名 IN (SELECT 曲名 FROM 曲リスト);
あなたのプログラムはこうなりますか?
foreach (Song 曲 in カラオケ){ if(曲リスト.exists(曲.曲名)){ 結果セット.add(曲); } }
SQLに限らず、カッコの中から先に解析・実行するのが普通で、カッコの中を先に解析・実行すれば、Bパターンのプログラムは出てこないでしょう。
逆に、こちらのSQLでは、
SELECT * FROM カラオケ WHERE EXISTS (SELECT * FROM 曲リスト WHERE カラオケ.曲名 = 曲リスト.曲名)
カッコの中を解析しようとしても単独では実行できないので、普通にプログラミングすれば、Bパターンのプログラムになるでしょう。
foreach (Song 曲 in カラオケ){ if(曲リスト.exists(曲.曲名)){ 結果セット.add(曲); } }
私は実行計画の見方すら分からない初心者の頃(インターネットもほとんどない時代ね)から、このSQLならプログラムはこうなるはずで、そうじゃないプログラムを返すDBエンジンは創れない。実行速度から間違いない。と常に考えてコーディングしていました。
その後、実行計画の見方を覚えて確認したら、思った通りで安心したという経緯を辿りました。
スタートからSQLは仕様書で自動でプログラムを書いてくれるモノ、と考えて来たからSQLを書くことに何の違和感も感じないだけなのです。
(後は経験の問題ですけれど……)
逆に、嫌な言い方をすれば、「IN と EXISTS の違い」が分かってないという人は、SQL文が(直接実行される)プログラミング言語と思っているから中々理解できないし、現実のプログラムと全く繋がってないし、「文法を覚えて何とかしよう」と思ってしまうのでしょう。
余談ですが、私がHTMLを覚えたときも同じやり方。
「自分がブラウザを作るとしたら、HTMLはこんな言語仕様になってないとブラウザが作れない。だから、言語仕様はこんな感じだろう」と荒く考えて、細かい仕様はリファレンスを引く訳です。
SQLが仕様書なら手続き型と等価
ちょっとしたプロジェクトに入ったことがある人なら、何度も経験があるはずですが、プログラミングができない人が書いた仕様書は、何が書いてあるか分からないことが多々ある。逆に考えれば、プログラミングができるなら、「仕様書であるSQLは効率的に簡単に書けるはず」なんです。
ところが、プログラミングができるのに仕様書であるSQLが書けない人が沢山いるのは、SQLとプログラムを別物と考えているからです。(くどいけどSQLを魔法か呪文と勘違いしている)
良いSQLを書くには、Aパターンのプログラムが欲しいのか、Bパターンのプログラムが欲しいのかが、まず、あなたの頭の中になければ書けない。これは大前提。
裏を返せば、SQLがまともにできる人は、同時にプログラムもできているはずです。
自分が欲しいプログラムが出てきそうな仕様書(SQL)を書き終わったら、それを貰ったプログラマ(DBエンジン)の立場になってプログラミングしてみる。更に、確認として、実行計画が問題ないか見るのです。
これを繰り返していれば、自然に良いSQLが書ける様になる。DBエンジンが変わっても、ある程度予想できるし、RDBMS毎の文法の違いなんて、プロジェクト毎の仕様書のフォーマットや、お作法の違い程度の差しか感じなくなります。(いちいち、リファレンスを引いても大したことない)
しかし、文法から入っていると、RDBMS毎の文法の違いが気になって仕方がないでしょうし、ちょっと変わればできなくなる。そういう人は、本質が分かってないのです。
この規模のSQLになれば、書くのも、読むのも、SQLと手続き型を行ったり来たりしないと無理です。逆に行ったり来たりできるレベルになれば、30分もあればできる様になります。
また、SQLと手続き型を行ったり来たりできるレベルになれば、「小さなSQLに分割した方がDBサーバの負荷が減る」なんて勘違いは絶対に起きないし、逆に、システム全体を俯瞰して本当に分割すべきポイントも見えてきます。
しかし、現実的には、多くのプロジェクトで、「小さなSQLに分割した方がDBサーバの負荷が減る」なんて勘違いする連中がクラスタ構成までやっている訳で……、私には悪い冗談としか思えない。彼らは「できる」つもりになっているけれど、彼らのいう「できる」は、「クラスタを構成するツールを使うことができる」であって、「本質を分かって構成することができる」では決してないのですから。
とにかく、SQLは仕様書ですから、手続き型プログラムと等価でなければおかしい。
そこが分かってない人は、本質的に何も分かってないのです。