スキーマを放棄したデータベース設計 - 『SQLアンチパターン』のEAVについて考察する
これはなに
ども、レバテック開発部のもりたです。花粉症の薬を飲み始めました。
今回はデータベース設計のアンチパターンとして有名ながら、わりとやりがちなEAV(Entity-Attribute-Value)について解説します。また、その対処法として挙げられる半構造化データについてさらに掘り下げ、どのようなケースでスキーマレスな設計が許容されうるのか? という点について考えてみたいと思います。
タネ本は『SQLアンチパターン』と『失敗から学ぶRDBの正しい歩き方』です。
EAV(Entity-Attribute-Value)とは
スキーマ定義を捨てたデータベースの設計
EAV(Entity-Attribute-Value)とは、その名前の通りEntityID、属性名(Attribute)、値(Value)の三つで構成されたデータベース設計のことを呼びます。この設計をとることで、属性にさまざまなものをとることができ、拡張性に富んでいます。具体的には下記のような設計です。
issue_id | attr_name | attr_value |
---|---|---|
1234 | product | 1 |
1234 | date_reported | 2009-06-01 |
1234 | status | NEW |
1234 | description | 保存処理に失敗する |
1234 | reported_by | Bill |
1234 | version_affected | 1.0 |
1234 | severity | 機能の損失 |
1234 | priority | HIGH |
1235 | product | 1 |
1235 | date_reported | 2009-06-01 |
1235 | status | NEW |
1235 | description | 編集機能の追加 |
1235 | reported_by | Tom |
1235 | priority | MIDIUM |
1235 | sponsor | ヒガシマル |
(テーブルの構成は『SQLアンチパターン』p53を引用)
このテーブルは要望とバグ報告が可能なチケット発行システムで利用されます。バグ報告の場合はseverity(重大さ)とversion_affected(影響のあるバージョン)の属性を含み、要望の場合はsponsor(要望元スポンサー)を含みます。そのほかの属性(date_reported, reporter, riority, status)は共通のものです。(以降、共通の属性を基底、issue_type別の属性をサブタイプなどと呼びます。)
このような設計が取られる背景には、リレーショナルデータベースがそもそも拡張性に乏しく、その反面で開発者には少ない工数でシステムを拡張する柔軟性が求められるという点が挙げられます。ただ、RDB本来の責務である「データを守ること」に立ち返ったとき、この拡張性の乏しさは堅牢性の裏返しであり、基本的にメリットです。RDBの「スキーマによってデータを守る」という思想とアプリケーションの「柔軟に素早く変化する」という思想のぶつかったところで、本来の責務を超えた対処法が編み出されてしまったケースと言えそうです。
では以下で、EAVのメリットとデメリットを整理してみましょう。
EAVのメリット
1. スキーマがないため、拡張性を確保できる
属性名カラムに任意の属性名を、値カラムに対応する値を入れることでどんなデータでも格納することが可能です。スキーマを変更することなくデータを保存することができるというのはリレーショナルデータベースには本来備わっていない柔軟さです。
EVAのデメリット
1. クエリが冗長で複雑、パフォーマンスに乏しい
特定の属性を取得しようとした際、どの属性を取得したいのかWhere句で指定する必要が出てきます。
また、データの取得ができても、その後アプリケーション側で余計な編集をする必要が出てくることもあります。
2. スキーマ情報がわからない
スキーマの定義を放棄しているため、どんな属性が含まれるのかは取得してみないと分かりません。たとえば、本当に取得したい属性が存在しているのか、取得できてもぞの属性と値の組み合わせが正しいのかということも分かりません。もっと悪いことには、属性名が揺れるということさえ起きえます。
3. データの整合性が保たれず、一貫性に乏しい
RDBの提供するさまざまな制約も使えません。たとえばNotNull制約や外部キー制約、データ型や検査制約といったものが使えません。
『SQLアンチパターン』上での対策
『SQLアンチパターン』ではいくつかの対策が紹介されています。
1. シングルテーブル継承
もっとも単純な対応として、全ての取りうるattr_nameを本来の属性にしましょう。
issue_id | reported_by | product_id | priority | status | description | issue_type | severity | version_affected | sponsor |
---|---|---|---|---|---|---|---|---|---|
1234 | Bill | 1 | HIGH | NEW | 保存処理に失敗する | BUG | 機能の損失 | 1.0 | |
1235 | Tom | 1 | MIDIUM | NEW | 編集機能の追加 | FEATURE | ヒガシマル |
どのissue_typeのときにどの属性が参照できるか? という情報をアプリケーション側で持たなければならないという欠点がありますが、ActiveRecordのように特定のテーブルに対してのアクセスパターンを定義できる場合には有用です。
2. 具象テーブル継承
次の対策は具象テーブル継承です。issue_typeごとにテーブルを作成します。
■BUGテーブル
issue_id | reported_by | product_id | priority | status | description | severity | version_affected |
---|---|---|---|---|---|---|---|
1234 | Bill | 1 | HIGH | NEW | 保存処理に失敗する | 機能の損失 | 1.0 |
■FEATUREテーブル
issue_id | reported_by | product_id | priority | status | description | sponsor |
---|---|---|---|---|---|---|
1235 | Tom | 1 | MIDIUM | NEW | 編集機能の追加 | ヒガシマル |
メリットはissue_type属性を省略可能になった点です。
ただ、基底属性の冗長性が否めない点や、どの属性が基底タイプでどの属性がサブタイプなのかを見分けるメタ情報がないため、属性を追加したいときにうっかり追加漏れや誤追加があるかもしれません。また、基底タイプを横断的に調査したいときにSQLが若干めんどうくさいことになります。
3. クラステーブル継承
みっつ目の対策はクラステーブル継承です。基底となるテーブルを作り、差分を別テーブルに切り出します。
■基底テーブル
issue_id | reported_by | product_id | priority | status | description |
---|---|---|---|---|---|
1234 | Bill | 1 | HIGH | NEW | 保存処理に失敗する |
■BUGテーブル
issue_id | severity | version_affected |
---|---|---|
1234 | 機能の損失 | 1.0 |
■FEATUREテーブル
issue_id | sponsor |
---|---|
1235 | ヒガシマル |
このパターンであれば基底属性への一括操作も簡単です。またJOINすることで結果的にシングルテーブル継承と同じリレーションを得ることができます。
4. 半構造化データ
そして最後が半構造化データです。これは基底タイプのテーブルにTEXTやJSONの形でサブタイプの属性を持つというものになります。
issue_id | reported_by | product_id | priority | status | description | issue_type | attributes |
---|---|---|---|---|---|---|---|
1234 | Bill | 1 | HIGH | NEW | 保存処理に失敗する | BUG | {severity: “機能の損失”, version_affected: 1.0} |
1235 | Tom | 1 | MIDIUM | NEW | 編集機能の追加 | FEATURE | {sponsor: “ヒガシマル”} |
この対策はスキーマレスという点でEAVと同じです。拡張性を持つ反面、スキーマ定義の恩恵を捨てることになります。
ここまでのまとめ
ここまでではEAVの意図とそのスキーマレスな設計がRDBにどのようなデメリットを引き込むのかについて見てきました。本記事では、ここからさらに「半構造化データ」について深く見ていきます。
(半構造化データ以外の対策についてはサクッと書くにとどめているので、こちらの記事を読むか、もしくは『SQLアンチパターン』を参考にしてください。)
半構造化データについて
半構造化データで本当に良いのか?
EAVの対策として挙げられている半構造化データですが、これは基本的にスキーマレスな設計であり、スキーマ定義を放棄した点でEAVと同じです。
『失敗から学ぶRDBの正しい歩き方』第8章、「JSONの甘い罠」ではその問題に言及した上で、メリットデメリット、そして利用できる場面について紹介しています。
半構造化データのメリット・デメリット
メリットは基本的にEAVと同じく、RDBには本来ない柔軟性を持つ点です。それに加えて、attr_name、attr_valueというふたつの属性のペアでサブタイプの部分を扱うのではなく、JSONの中にattr_name、attr_valueを入れ込んで扱っているため、RDBのJSON属性をそのまま利用可能という点があります。
デメリットについてはEAVと同じです。
半構造化データを利用する目安
このようなスキーマレス設計を利用できる条件として、いくつか挙げることができます。
JSONをそのまま扱える
まずはJSONを加工することなく保存したり、アプリケーションに渡したりすることです。JSONの中身に手を入れた時点で複雑さが勝ります。
保存したレコードに対して更新をかけない
繰り返しになりますが、JSONの中身に手を入れることが少ないケースになります。
レコード数が少なく、数千程度
データが小さいことも利用の目安になります。大量になり、また結合の必要などが出るようであれば半構造化データには向いていません。
システムがそもそも発展途上
発展途上であり、柔軟性を担保することのメリットが得やすい場合も利用に向いています。
ただこれは言い訳になりやすく、また計測もしにくいので十分注意した方がよさそうと個人的には思います。
具体例
『失敗から学ぶRDBの正しい歩き方』にある3つの利用ケースと、もりたの考えた利用ケースをご紹介します。
WebAPIの戻り値
WebAPIの戻り値をそのまま入れるようなケースです。サードパーティのWebAPIの場合、予告なく変更が入ることもあるため、とりあえずJSONをそのままデータとして受け取り、必須のもののみ別のテーブルに移し直す的な対応が可能です。
OS情報
OS情報は固有のものが多く、また更新もすくないため、半構造化データのメリットを活かすことが可能です。
ユーザが任意で登録するプラグインの情報
固有の属性を持つsettingテーブルをプラグインごとに作成するよりも、JSONでもつメリットの方が勝ったケースです。レコード数も多くなりにくいため導入されました。
メッセージブローカー
一時的な利用にとどまり、利用後に破棄されるデータであれば半構造化データの利用が可能と考えています。これは例えばイベントソーシングで使われるようなものを想定しています。
おわりに
EAVなどのスキーマレスな設計は柔軟である反面、RDBの責務である「データを守る」ということに真っ向からぶつかっていくアンチパターンです。ただ、半構造化した上でJSONなどを利用することで、特定のケースではデメリットを避けてその拡張性を享受することも可能と分かりました。設計時にはかなり注意と議論が必要になるとは思いますが、考えてみるのもいいかもなと思いました。
参考書籍
『SQLアンチパターン』
『失敗から学ぶRDBの正しい歩き方』
Discussion
EAVのデメリットとしてよく言われていますが、冗長ではなく単に「文字数が多い」だけなのでは?
必要な記述は多少長かろうが無駄ではないので、決して「冗長」ではないと思います。
WHERE句に条件が増えるのも特別なテクニックが必要な訳ではなく、シンプルな条件の列記が増えるだけだと思うけれど、長いSQLは全て複雑ですか?
コメントありがとうございます!
ちょっともりたがアホなので論点を整理させてもらうと、
論点整理
なので、いま喋った方がいいのは以下の2点だと思います。
①通常のテーブル構成と比較して冗長・複雑であるか
そういう意味ですと、
長いSQLは、同じデータ操作を目的とするより短いSQLと比較して複雑であり、冗長だろうなと思います。
(自己相関するクエリは短いけど複雑じゃんとかは今回の論点から外れるので考慮しないこととしてます)
②そういう記述に手間暇かける価値があるか
これはケースバイケースだと思います。EAVが生きるケースは記事の中にも挙げてますが、そのケースだったり、他にも気がついていないケースがあるのではないかと思っています。
(なにかそういうケースがあったら教えて欲しいです)
以上です!
この記事の元ネタになってる書籍が記事末尾にあるので、そちらも読まれてはいかがかと思います。おそらくtezさんの疑問に対していろんな観点を与えてくれる記述があるのではないかと思います。
素早いお返事に対応できなくて申し訳ありませんでした。
僕の個人的な感覚は多くの方に共感していただけないのだろうとは思いますが、長かろうが複雑だろうが、必要であるなら冗長ではないと思うのです。言い換えると、不要なモノを含んでいる場合に「冗長」と呼ぶのだと思うのです。
比較する際に、通常のテーブル構成にする為にテーブルやカラムを増やすコストを無視してしまうのは、フェアじゃないなぁと思うのです。(テーブルやカラムが増えれば CREATE TABLE 〜、ALTER TABLE 〜 等を1回実行するだけで済むなんてヌルい事はないですよね?)単純に(アレコレが済んで)取得するSQLが長い(≠冗長)か短いかだけを比較するのは片手落ちなんだろうなとも思うのです。
もちろん最終的な実行時の事だけ比較したいと言うニーズもあるでしょう。(大方の場合がそうであるのかも知れませんが)
成長途中のシステムだと、仕様がドンドン追加変更されるのが常です。
EAVが採用されるべきケースだと思いますので、気にせずに進めれば良いだけなんですけどね。世の中でアンチパターンと呼ぶ理由が何となく薄いなぁと感じたので手近なところで(ごめんなさい🙏)ご意見を聞ければと思って書き込みました。