序文
全国1千万の DB エンジニアの皆様、こんにちは。NULL撲滅委員会極東支部長のミックです。皆様におかれましては日々、DB の構築、SQL 作成、パフォーマンス・チューニング、本番データの入ったテーブルをいともあっさり DROP した新入社員の尻拭いと、獅子奮迅の働きにてチームを支えておられるであろうと存じます。さて、本日私が一筆啓上しましたのは、NULL撲滅基本宣言への皆様の参加を募りたく思ったからです。
NULL というこの面妖な怪物の質の悪いところは、最初は私たちの感覚に心地よく合致すると感じられるため、ごく自然にするっとシステム設計の中に忍び込んできて、気が付いたときにはシステムをどうしようもなく複雑で、非効率的で、直観に反する動作をするに至らしめ、開発も運用も困難なものにしてしまうところにあります。ゆえに、NULL のもたらす脅威から身を守るには、まず第一にその正体をよく知り、どのようなメカニズムによってシステムに猛威を振るうのかを理解することです。私は、既にこの怪物の生態をある程度を明らかにする一文を書きました。しかしそこでは、この敵から身を守る具体的な方法――といっても、大したものではなく、設計の際の心構え程度のものですが、それを詳らかにしませんでした。この文章ではこれを明らかにし、できるなら皆様にも NULL 撲滅運動への参加を強く促したいと思う所存です。
なぜNULLがそんなに悪いのか
NULL が悪いとされる理由は、一般的に以下の5点によります。
- 1. SQL の作成にあたり、人間の直観に反する3値論理を考慮せねばならない。
- 2. IS NULL、IS NOT NULL を指定する場合、インデックスが参照されないためパフォーマンスが悪い。
- 3. 四則演算または SQL 関数の引数に NULL が含まれると「NULL の伝播」が起こる
- 4. SQL の結果を受け取るホスト言語において、NULL の組み込み方が標準化されていない。
- 5. 通常の列の値と違って、NULL は行のどこかに余分なビットを持つことで実装されている。そのため記憶領域を圧迫したり、検索パフォーマンスを悪化させる。
1.の理由は、私が思うに NULL を排除すべき最大の理由ですが、既に「3値論理」で述べたのでここでは繰り返しません。(時折、NULL をガンガンに許可しながら SQL が2値論理で動作すると盲信している迷惑な輩の存在を耳にしますが、言語道断です。) また2.は、パフォーマンス・チューニングの際に気をつけるポイントして良く知られているものです。3.について、例えば四則演算の対象に NULL が含まれた場合、
1 + NULL = NULL
2 - NULL = NULL
3 * NULL = NULL
4 / NULL = NULL
NULL / 0 = NULL
というように、演算結果も問答無用で NULL に化けてしまいます。最後の例から分かるように、0除算の場合ですらエラーになりません。 SQL 関数の多くも、NULL に対しては NULL を返す仕様になっています。この現象を「NULL は伝播する(NULLs propagate)」と言います。propagate という単語は、「(雑草が)はびこる」のように負のニュアンスを持って使われることもあり、NULL の厄介者ぶりを表すにはぴったりの表現です。
意外と知られていない、あるいは注意を払われていないのが、4.と5.の理由ではないでしょうか。正直、この2点については、ホスト言語やDBMSの実装次第というところもあり、今後解消されていく可能性は大いにあります。しかしホスト言語が関係モデルにおける NULL をサポートしていない現時点では、やはり大きな問題です。Oracle では空文字と NULL は区別されないが他のDBMSでは区別されるという現状がまかり通っている以上、NULL 排除の十分な理由となります。DBMS間のコードの移植性を大きく下げる要因になるからです。
しかし NULL を完全に排除することはできない
支部長がいきなり何を言い出すんだと言われるかもしれませんが、いや実際、リレーショナル・データベースの世界から NULL を永久追放するのは難しいのです。また、さして重要でない列に NULL が入っているぐらいは眼をつむるのが、現場の SE としての感覚でもあります。
NULL の永久追放が難しいことの理由は、それがあまりにもリレーショナル・データベースの奥底に根を張る存在だからです。単純にテーブルの全列に NOT NULL制約を付加すれば済む話ではないのです。たとえそうしたとしても、外部結合や、SQL-99 で追加された CUBE や ROLLUP 付きの GROUP BY句を使うことで、NULL は簡単に入り込んできます。だから、私たちにできることはせいぜい、「極力」 NULL を排除することだけなのです。そしてまた、うまく使えば NULL が大変便利な概念であることは間違いありません。問題は、その「うまく使う」ことが NULL に関してはとても大変なことなのです。NULL の怖さは、うまく御していると思って油断していると背後から一突きされることの恐怖です。
そのようなわけで、NULL の扱い方は、識者の間でも議論の絶えないテーマです。創始者のコッドは、NULL が関係モデルにとって不可欠な要素であると確信していました。コッドの盟友にして現在最も指導的な論者であるデイトは、NULL撲滅運動の最右翼です。私の心情としてはデイトの味方をしたいのですが、場末のエンジニアの現実感覚に最も合致するのは、セルコの穏健的な人生処方です。そこで以後、極東支部としては、次の言葉を公式方針としたいと思います。
-
NULL は薬のようなものと考えてください。NULL のほどよい使用は、あなたのためになります。しかし乱用は害を与えます。最もよい考え方は、使う必要があるときに適正に使用し、後はなるべく使わないというものです[1]。
コードの場合:未コード化用コードを割り振る
きっと皆さんが使うデータベースのテーブルには、色々な種類のコードが格納されていることでしょう。例えば、企業コード、顧客コード、県コード、性別コード、等々。性別のように、一般的に「フラグ」と呼ばれることの多い属性も、大きく見ればコードに含めてよいでしょう。フラグとは要するに二つの値しか保持しないコードのことです。こうしたコード類は、システムにとって重要な列であることが多いものです。検索や結合のキーとなることも多いでしょう。従って当然、NULL 排除の第一標的となります。
解決策は簡単で、未コード化用コードを割り振ることです。例えば、ISO の性別コードでは、1:男性、2:女性の他に、0:未知、9:適用不能という二つの未コード化用コードが体系に組み込まれています。コード9は法人に使われます。これは素晴らしい解決です。図らずもコッドが分類した二つの NULL、未知と適用不能に対応するコードが採用されています。
常に二つのコードを用意する必要はありません。一つで十分な場合も多いと思います。例えば不明な顧客コードを持つ顧客をそれでも DB に登録せねばならない場合、不明を表すコードとして「XXXXX」等を用意すればよいでしょう。ここで「99999」のようなコードを使うことは避けた方が良いと思います。何故なら、コードには多くの場合、数字が使われるため、ありえないと思って未コード化用コードに採用したコードを持つ顧客が現実に出現してしまう可能性があるからです。そのため、コード列は必ず文字型で宣言すべきです。時々、無頓着にコード列を数値型で宣言しているテーブルを見かけますが、感心しません[2]。
名前の場合:「名無しの権兵衛」を割り振る
きっと皆さんが使うデータベースのテーブルには、コードに劣らず多種多様な種類の名前が格納されているはずです。名前の場合も、方針はコードの場合と同じです。すなわち、不明を表す値を与えることです。「不明」でも「UNKNOWN」でも、開発チーム内で共通了解の得られた適当な名前を与えましょう。
一般に、名前はコードに比べてキーに使われる頻度は少なく、付加的な意味しか持たない場合が多いので[3]、あまり撲滅に目くじらを立てる必要もないのですが、やっておくにこしたことはありません。
数値の場合:0で代替する
数値型の列の場合、私が最も良いと考えるのは、最初から NULL を 0 に変換してデータベースへ登録することです。NULL を許可しておいて集計時に NULLIF関数や IS NOT NULL述語で排除する、という方法は薦めません。私の経験上、NULL を 0 に吸収させて問題化したことは、あまりありません。しかも、NULL を排除したことによる恩恵を受けられます。
もっとも、厳密に考えればこのやり方が乱暴であることは否めません。セルコも言うように、「ガソリンタンクを持っていない車と、空のガソリンタンク」は、概念的に異なるものです[4]。従って現実的な案としては、
- 0 に変換する
- どうしても 0 と NULL を区別したい場合だけ、NULL を許可する
ということです。願わくば 0 に変換することで全てうまくいくことを祈っています。
日付の場合:最大値・最小値で代替する
日付の場合、NULL の持つ意味合いが多岐にわたるので、その場その場でデフォルト値を使うか、NULL を許可するかの判断が必要になります。
日付が開始日や終了日など「期限」を指示する場合は、「0001-01-01」や「9999-12-31」のように可能な最大値・最小値を使うことで対応できます。例えば社員の入社日やカードの有効期限を示す用途での日付が、これに該当します。この方法は、昔からよく使われています。
一方、デフォルト値がそもそも分からない場合、例えば歴史上の事件が起きた年月や誰かの誕生日など、コッドの分類における「未知」のNULL に相当する場合は、先のように意味ある値を入れることはできません。この場合は、NULL を許可しても良いでしょうし、「UNKNOWN」という文字列を入れても良いかもしれません。その場合、日付の列は文字型で宣言しておくということも有効でしょう。(確実に NOT NULL制約を付けられる場合なら、日付型でもかまいません。) 文字型と日付型は簡単に型変換ができるため、暦日計算の場合だけキャストするという選択も、性能的なオーバーヘッドが問題にならなければ考慮に値する選択肢です。
終わりに
さて、四つのデータ種類に分けて NULL 撲滅の具体的手段を述べてきました。まとめるならば、
(1)まずデフォルト値を入れられないか検討する
(2)どうしようもない場合だけ NULL を許可する
ということです。これだけでも、かなり NULL のもたらす厄介事からシステム開発を解放できるのではないかと思います。あるいは「そのやり方ではうまくいかない」とか「もっと良い方法がある」ということもあると思います。その場合は、メールまたはゲストブックへご一報ください。
注
[1] J.セルコ『プログラマのためのSQL 第2版』p.102
[2] コード列を文字型で宣言した方が良い理由は、他にも二つあります。
まず一つは、コードは多くの場合桁数が固定で、前ゼロが入るからです。例えば3桁のコードであれば「008」、「012」のように。数値型では前ゼロが削られてただの「8」や「12」になってしまいます。これではソートもうまく並びません。
第二に、一度データの入ってしまったテーブルに対して後から型を変えようとするのは大変です。私の使用する Oracle9i では、文字型と数値型の変換のためには、一度列を NULL クリアするか、名前を変えて新しい列を追加する必要があります。何事もそうですが、最初が肝心です。
[3] 逆に、名前をキーに使用しているテーブルがある場合、設計に何か間違いがあると疑ってください。
[4] セルコ、p.111
Copyright (C) ミック
作成日:2005/08/05
最終更新日:2017/06/22 Tweet