現在でも多く見かける症状です。
そうか。条件によって比較の列を切り替えればよかったのか。このクエリだと、もうサブクエリSUM_ITEMも一度しかアクセスしませんよね。
うむ。だが今回のように中で集約を行っていたり、比較的大きなサブクエリの場合は、やはり可読性の観点から共通表式を使うことに意味がある。
年輩のDBエンジニアには、CASE式ではなく、DECODE(Oracle)やIF(MySQL)といった実装依存の関数を使い慣れている人もいるけど、CASE式に比べて表現力が弱いし移植性もないので、これらも使うべきでないわ。SQLは改訂のたびに劇的に構文が変わっていくので、時代錯誤症候群 は致命的な病気よ。
今から新しく覚える僕は勝ち組ってことですか。
ふ、数年後にはあなたも時代遅れの仲間入りよ。常に勉強する努力が嫌いなら、この救命室には来ないほうがいいわ。
ぐぬぬ。
言語の進化とエンジニアの進化
さてここで少し、SQLの新機能がどのように追加されていくのかを説明しておこうと思います。ご存じの方もいるかもしれませんが、SQLには国際標準規格が定められていて、かつては米国のANSI、現在ではISO(International Organization for Standardization 、国際標準化機構)が数年に一度のペースで改訂を行っています。その標準規格は「SQL:xxxx」( xxxxは改訂年)という名称で管理されており、最近ではSQL:1999、SQL:2003、SQL:2008といった規格が定められています(表1 )( 注2 ) 。
表1 直近3回の標準SQL改訂とDBMSでの対応状況(○=対応、△=制限あり、×=非対応)
年 名称 主な機能追加 Oracle 11gR2 SQLServer 2008 R2 DB2 V9.7 PostgreSQL 9.1 MySQL 5.5
1999 SQL:1999 再帰クエリ ○ ○ ○ ○ ○
トリガー ○ ○ ○ ○ ○
正規表現 ○ ○ △[4] ○ ○
非スカラ型 △[1] ○[2] △[5] ○ △[6]
OLAP機能(ROLLUP、CUBE、GROUPING SETS) ○ ○ ○ × △[7]
共通表式 ○ ○ ○ ○ ×
2003 SQL:2003 XML機能 ○ ○ ○ ○ ×
ウィンドウ関数 ○ ○ ○ ○ ×
シーケンスオブジェクト ○ ×[3] ○ ○ ×
MERGE文 ○ ○ ○ × △[8]
オートナンバー型 × ○ ○ ○ ○
2008 SQL:2008 TRUNCATE文のサポート ○ ○ ○ ○ ○
INSTEAD OF トリガ ○ ○ ○ ○ ×
※1) PL/SQLでのみ配列型、レコード型など使用可能
※2) ユーザ定義型として利用可能
※3) SQL Server 2012でサポート予定
※4) 組み込み関数としては存在しない。ユーザ定義関数として定義可能
※5) SQL PLでのみ配列型、レコード型など使用可能
※6) ENUM、SETなどを使用可能
※7) ROLLUPのみ使用可能
※8) INSERT……ON DUPLICATE KEY UPDATEという独自構文として利用可能
改訂の内容は、基本的には機能追加です。一部廃止されたり非推奨になる機能も含まれていますが、一般的にDBMSの開発元はある程度の後方互換をサポートするため、ここで廃止されてもすぐにDBMSで使えなくなるわけではありません(徐々に廃止はされていくでしょうが) 。
新規追加された機能についても、そのサポートの進展はDBMSによってばらつきがあります。たとえば、本稿でも取り上げている共通表式やウィンドウ関数など、SQLコーディングの可能性を大きく広げる機能一つ取ってみても、PostgreSQLがウィンドウ関数をサポートしたのは8.4と最近ですし、MySQLはまだ未サポートの状態です。他方、Oracle、DB2、SQLServerなどは比較的早く改訂内容を取り入れる傾向にあります。
表1を見ていただいてもわかるように、1999年、2003年とSQLの機能追加はかなり大々的に行われており、どれ一つをとっても旧来のコーディングスタイルを大きく変えるものです[3] 。これはつまり、SQLは現在も活発に言語仕様が拡張されている言語であり、新しい機能をどんどん活用していかないと損だ、ということです。
SQLは寿命の長い言語か?
40年間主流であり続けた
SQLは寿命の長い言語だとよく言われます。たしかに、アプリケーションの実装言語が比較的早いサイクルで栄枯盛衰を繰り返すのに対して、SQLはリレーショナルデータベースの誕生からほぼ40年間、データベース操作言語の主流であり続けました。その理由は、リレーショナルデータベースが存在している以上はセットで存在しているから、というのもあるのですが、SQLが本当に非効率的な言語であるなら、もっと別の(おそらくは手続き型の)言語によって取って代わられてもおかしくなかったはずです。
自らを大きく変化させてきたSQL
実はSQLの場合、同じ「SQL」という名前で呼ばれていますが、その中では激しい新陳代謝が起こっているのです。たとえとして適切かどうかちょっと自信がないのですが、同じバンド名でありながら、構成メンバーはどんどん入れ替わっているような……それに似たところが、SQLにはあります。
その大きな理由は、かつてのSQLが実際に機能不足の言語だったことです。「 SQLは“ Scarcely Qualifies as a Language” [4] ( 言語としての資格に欠ける)の略だ」 ──これは、かつて言われていた皮肉混じりのジョークです。かなり昔のネタなので、知っている人はけっこうベテランなのではと思いますが、そういう悪口を言われるぐらい、昔のSQLは機能不足でした。
そのため、SQLの進化は、基本構文そのものを捨てる ことで進んできました。このことは、相関サブクエリがウィンドウ関数によって、サブクエリが共通表式によって、UPDATEとINSERTがMERGE文によって置き換えられてきたことを見ても明らかです。SQLの言語拡張は、古い構文を捨てていくことによって進むのです。
これは、手続き型言語の進化のしかたと大きく異なります。手続き型言語の場合、ひととおりの基本構文を覚えれば、そのあとの言語拡張はほぼ純粋な機能追加という形で行われるため、基本構文のかなりの部分は、継続的に使える知識になります。その代わり、まったく別の新しい言語によって取って代わられることが頻繁に起きます。
こうしたことから、エンジニアにとって、SQLは勉強のコストがかかる言語だと言えます。しかしそれは、古い非効率的なコードを新しいコードに置き換えられるチャンスがある、ということでもあります[5] 。
見た目上の「寿命」が長いように見えたとしても、その内実はなかなか複雑な人生(言語生?)を送っているのです。
次の患者も、SQLではシンプルさが正義であることがよくわかる。新機能の力も発揮できる。
カルテ2: 図4 のような製造業者のテーブルManufacturersを追加する。部品の要求をかけている製造業者と同じ地域(=都市および地区が同じ)に存在する供給業者を、先のSuppliersテーブル(図1)から選択したい。もちろん、出荷が可能な供給業者に限る。
図4 Manufacturersテーブル
冗長さはコードをわかりにくくする
部品を要求している製造業者は、a、b、c、fの4つ。うち、「 東京、江戸川区」のcと、「 大阪、福島区」のfについては、マッチする供給業者がいない、と。すると結果は、図5 のようになればいいですね。ようし(リスト4、図6 ) 。
図5 求める結果
sup | city | area
----+------+--------
A | 東京 | 北区
C | 東京 | 荒川区
リスト4 ワイリーの解(結果が正しくない) G | 大阪 | 北区
SELECT sup,
city,
area
FROM Suppliers
WHERE ship_flg = '可'
AND city IN (SELECT city
FROM Manufacturers
WHERE req_flg = '要')
AND area IN (SELECT area
FROM Manufacturers
WHERE req_flg = '要');
図6 リスト4の実行結果
sup | city | area
----+------+--------
A | 東京 | 北区
C | 東京 | 荒川区
G | 大阪 | 北区
お前の解は、効率以前に結果が違うぞ。
あれ……本当だ。余計な「大阪、北区」の供給業者Gが入ってしまっている。製造業者eは部品を要求していないのに。
あなたの解は、こういう動きをするの。最初のINのサブクエリで部品を要求している製造業者の都市として、東京と大阪が選ばれる。次のINのサブクエリでは、部品を要求している地区として、北区、荒川区、江戸川区、福島区が選ばれる。すると結局、条件に合致する地域の組み合わせはこうなるの。
ああっ、それで「大阪、北区」という条件に合致する供給業者Gが選ばれてしまったのか。偶然、東京と大阪に「北区」という同じ名前の区があったから。
比較できるのは列だけではない
そういうこと。いい? この問題では、「 都市、地区」はこれで一続きのキー として扱うべきなの。あなたみたいに分離したらダメ(リスト5 ) 。city もareaも文字列型の値だから、連結演算子で結合してしまえば、これで1列のキーとして扱えるようになるわ[6] 。
リスト5 ヘレンの解:複数の列を連結して一つのキーとして扱う
SELECT sup,
city,
area
FROM Suppliers
WHERE ship_flg = '可'
AND city || area IN (SELECT city || area
FROM Manufacturers
WHERE req_flg = '要');
うむ。そしてここからがようやく本題だ。リスト5はさらにリスト6 のように書き換えるのが正しい。これはSQL-92で標準化された行式(row expression)だ。列と列の比較を複数列の比較に一般化したもので、原理的には比較演算子全体に(=、、>)拡張できる。実装されているかどうかはまた別問題だが[7] 。
リスト6 ロバートの解:行式を利用
SELECT sup,
city,
area
FROM Suppliers
WHERE ship_flg = '可'
AND (city, area ) IN (SELECT city, area
FROM Manufacturers
WHERE req_flg = '要');
なるほど。しかしこれってヘレンさんの解(リスト5)から書き換えるメリットって何なんでしょう? 実質的に同じことをやっているように見えるのですが。
メリットは大きいぞ。まず左辺の列に連結演算子を使わなくてよいため、インデックスの利用が期待できる。そして列のデータ型が文字列型以外でも汎用的に使える。比較したい列が数値型や日付型の場合、連結子を使うには型変換を使わねばならない。
ああ、そういうことか!
SQLの標準化の中で最も画期的だったのは、SQL-92(1992年改訂)です。もう20年近く前の話ですが、ここで導入された機能には重要なものが多く含まれています。例を挙げれば、本稿で取り上げた行式やCASE式、ほかにも日付型やトランザクション分離レベル、一時表なども導入されました。「 ということはそれ以前には、そうした機能がなかったの?!」と驚く人もいると思います。昔のSQLが「欠陥言語」と悪口を叩かれていた理由がおわかりいただけるでしょう。これを言うと年輩の方から怒られてしまうかもしれませんが、SQLに悪いイメージを持っている人は、えてしてSQL-92以前のイメージで時が止まってしまっています 。
また、ロバートが言うように、SQLに追加される新機能を観察していると、そこには単純さの原則が存在していることが見て取れます。かつては難しい構文を使わなければ実現できなかった処理を、ずっと簡略的で、直観的にわかりやすく記述できるように進歩してきています。UNIONを連ねなければできなかった分岐はCASE式で、同じくUNIONを使っていた完全外部結合はFULL OUTER JOINで、複雑な相関サブクエリはウィンドウ関数で、簡単に書けるようになりました。かつ、SQLの長所は、そうした新機能のほうが概してパフォーマンスも優れた実行計画が立てられることです。利用しない手はありません。
ほかにも覚えておいたほうがいい新機能ってあります?
そうね、まずはこれまでも何度か治療に使ってきたウィンドウ関数[8] 。これは自己結合や相関サブクエリを省略できる最高のツールね。それに、GROUP BY句の集計機能を拡張したCUBE/ROLLUPオプション。これは小計や合計のレコード(超集合行)も出力するもので、UNIONを省略できてとても便利よ。CASE式と組み合わせたときの効果は絶大だわ。
あとは新機能というわけではないけど、結合の構文は可読性と保守性の観点から、実装依存の方言ではなく標準化されたものを使うべきね。DBMSベンダーもそれを推奨しているわ。
逆に、新機能だからというだけで飛びついてはいけないものもある。
えっ、そうなんですか。
たとえばSQL:1999で導入された非スカラ型(配列型) 。これを安易に使うのは危険だ。関係モデルの大原則である第1正規形[9] を崩すことになるうえ、DBとインタフェースを持つアプリケーション側も対応が必要で、実装のサポートもあまり進んでおらず移植性にも難がある[10] 。
標準化といえど完璧ではないんですね。
そうだな。SQLは言語の理論的基礎がかっちりしているから、あまり無軌道な拡張に向いたキャラではないのだろう。それに、標準といえど人間が決めることだ。政治もあれば間違いもある。完璧などありえんさ。……ところで、人間の決めることといえば、お前の専門についてだがな……。
あ、このあと飲み会なんで、今日はこれで失礼します。いやー今日も勉強になりました。僕も来年はお二人みたいにバリバリやるぞー。
お、おう。お疲れ。
普段は威張ってるくせに、ロバートはこういうとき頼りにならないんだから、もう。
【参考資料】
1.ミック WEB+DB PRESS Vol.63 連載「SQL緊急救命室」第2回「冗長性症候群」
冗長なSQLコーディングの代表格が、条件分岐をUNION(ALL)で表現してしまうことでした。これをCASE式で簡潔に記述する方法を紹介しています。gihyo.jp でも公開しております。
2.ミック『SQL ゼロからはじめるデータベース操作』翔泳社、2010年
外部結合の標準構文、CUBE/ROLLUP、ウィンドウ関数、CASE式といったSQLの重要な機能の使い方の基礎を解説した書籍です。SQLを基礎から正しく学びたい方にお勧めです。
3.Wikipedia - SQL - Standardization
SQLの標準化は数年単位で行われています。各改訂の内容は、上記Wikipediaのサイトからたどるのが調べやすいでしょう。
SQL周辺系機能の標準化について
標準SQLについて、本文では主にSQLそのものの構文についてスポットライトを当てましたが、実はISOの標準化では、そうした「ピュアSQL」だけではなく、周辺機能の新規追加や拡張も行われています。
代表的なところを挙げるならば、次のような種類があります。
SQL/PSM(OracleのPL/SQLやPostgreSQLのPL/pgSQLといった、いわゆるストアドプロシージャ)
SQLJ(Javaをホスト言語とした埋め込みSQL規格)
XML関連の機能
こうした機能は、純粋にSQLの中で利用するのではなく、SQLと連携しつつ、従来のRDB/SQLの枠内で実現の難しかった機能を実現するものです。これら周辺系機能の活用というのも、データベースを扱ううえでは重要なテーマの一つです。もっとも、本連載のスタンスとしては、こうした周辺系機能にすぐに頼るのではなく、ピュアSQLの機能を使うことで問題を解決する方向を選択しています。その理由は、こうした機能が周辺系ゆえに使いどころが限られるという理由もありますが、一般にSQLの力が十分に利用されているとは言えないからというのが最大の理由です。たとえば、「 第3回 ループ依存症 」( WEB+DB PRESS Vol.64)において、ストアドプロシージャのループに頼るのではなくSQLでループを代替する解を考えましたが、これは安易にプロシージャに処理を寄せると非効率になりがちだからです。
ただし、すべてをSQLで解けばよいというわけでないことは言うまでもありません(注a ) 。「 第4回 スーパーソルジャー病 」( WEB+DB PRESS Vol.65)で見たように選択肢を最初から狭めては、優秀なソルジャーにはなれても大局を見渡す指揮官にはなれません。エンジニアは、自分の使う道具について熟知していると同時に、複数の道具を使えなければならないのです。