MySQLで外部キー制約を課すべきか

外部キー制約を設けておくと、存在しないエンティティ(例えばユーザIDなど)にリレーションをシステム的に持てないようにすることができる。これは、データの不整合を防ぐためには非常に有効な手段であると思う。

SQLアンチパターンには「キーレスエントリ(外部キー嫌い)」というタイトルで章が割かれている。基本的に制約を設けた方が色々とよいという結論である模様。

私が所属しているチームでも同様な議論がされたため、論点になった箇所と考察を書いておく(普段はこういうところにあんまり書かないんだけど、なんか勿体無いなと思って)。最終的な結論としては、外部キー制約は設けない方向で行くという決断をした。

外部キー制約に関する論点

データの整合性

外部キー制約を設けると、最初にも書いたが存在しない要素のIDにはリレーションを持つことができないため、データの生合成を保証することができる。これは、制約を設けることの大きなメリット。人間が考えることを少なくし、システムを強固にしてくれる。

テストについて

テストデータを作成する場合、リレーション元のテーブルから順番に作成しないといけないため、テストデータに工夫が必要でコストが高いという意見が出た。この件に関しては、どちらのケースでも対応する方法はある模様。

少し乱暴だがテストデータをインサートする前に SET FOREIGN_KEY_CHECKS=0; を発行して外部キー制約のチェックを無効にする方法がある。そして、テストが終わった時に SET FOREIGN_KEY_CHECKS=1; チェックを有効にする。本番のDBではあり得ないが、テストDBなら問題はないだろう(そもそもモックしろとかそういう話も色々あるが)。

しかし、これはこれで「外部キー制約を踏まえたテストができないので全てが丸く収まるというわけにはいかなそう」という問題もでてきた。

テストデータを作成するのが面倒くさいというが、何も制約がない状況下で制約を担保するようなテストを書く方が手間がかかり、考え事が増えるという話もある(テストfixtureを準備するのがめんどくさい(?))。

とはいえ、安全側に倒すことを考えるとこの件に関しても外部キー制約を設けた方が良さそうな気がする。

パーティショニングされたテーブル

InnoDBはパーティショニングされたテーブルについて、外部キー制約をサポートしないという悲しい事実がある。これはどうにもできない。親だろうが、子だろうがどちらか一方がパーティショニングされたテーブルだった場合、制約を課すことができない。 また、シャーディングされたテーブルにおいても同様に、外部キー制約を課すことができない。

InnoDB ストレージエンジンを使用するパーティション化されたテーブルでは、外部キーはサポートされません。これは具体的には、次の 2 つの記述が true であることを意味します。 1. ユーザー定義パーティショニングを使用する InnoDB テーブルの定義には、外部キー参照を含めることはできません。定義に外部キー参照が含まれる InnoDB テーブルはパーティション化できません。 2. InnoDB テーブル定義に、ユーザーパーティション化されたテーブルへの外部キー参照を含めることはできません。ユーザー定義パーティショニングを持つ InnoDB テーブルに、外部キーによって参照されるカラムを含めることはできません。 https://dev.mysql.com/doc/refman/5.6/ja/partitioning-limitations.html

今回のプロジェクトはパーティショニングされたテーブルがそれなりの個数存在し、当初はSpiderでのシャーディングも検討していた。そのため、外部キー制約は全てのテーブルに設けない方向で今まで進んでいた。

導入コスト

パーティションの件があるので、すでに外部キー制約を設けない方向でプロジェクトを進めている。とはいえ、制約を課せるところだけ課そうとする意見もあったが、どちらかに統一したいという意見もあった。それに加え、今から導入するとそれなりの作業が発生し導入コストがかかる(これは外部キー制約のデメリットではないが…)。

暗黙のインデックス

このスライドにも書いてあるが、暗黙のインデックスというのが貼られ、インデックスサイズの肥大化につながる。デメリットだけどあまり気にしない。

意図しない箇所でのテーブルロック

リレーション先の行にshared lockがかかる」で指摘されていることで、こっちは逆に外部キー制約を課すデメリットなのかもしれない。

リレーションの親テーブルをうっかりshared lockしていることがある http://songmu.github.io/slides/fk-night/#15

削除対象のレコードがある場合

レコードの削除が想定されるテーブルに外部キー制約を課すと、レコード削除が難しくなるケースがありうるという意見。整合性を保つゆえ仕方ないが、何も考えずにカスケード削除を行うと事故を起こす可能性は否定できないとも。これは、整合性を保ってはくれるがDBの構造を理解しない・気にしなくていいというわけではないのではない。これに関しては、構造は理解していることに越したことはないのだが、「親テーブルの行を消したときに子テーブルの行は残したい」という状況が発生した時点で、設計を見直すべきである

考察

外部キー制約を課すことによって得られるメリットである、「データの整合性」は非常に魅力的ではあるが、パーティショニングをしているテーブルが多いこと、また現在の状況と対応工数を踏まえて検討すると、外部キー制約を課さない方向で行こうと思う。ただ、注意すべきは整合性が取れないということを前提にシステムを設計・構築しなければならない。例えば、どういうことを意味するかというと、APIなどで投稿されるあるキー(例えば商品IDなど)をそのまま信じてDBにインサートしてはいけない(当たり前だけど)。必ずそのIDがデータとして有効なものであるかを確認する必要がある(整合性は取れなかったとしても)。このことを考えると一部だけ外部キー制約を課すという案もある。これは必要に応じで外部キー制約を課したほうが少しはマシになるのではないか。

今回は採用しない方針で進めるが、外部キー制約を課さない方がよいというわけではない。当然だが、全ての事柄はケースバイケースである。外部キー制約を課さないとしても、エンティティ同士のリレーションを表現する図(つまりER図)は人間が見るときに欲しい。

Foreign Key Nightの「我々(主語が大きい)は何故MySQLで外部キーを使わないのか」がだいぶ参考になった。🤔

References