🔨

スキーマを放棄したデータベース設計 - 『SQLアンチパターン』のEAVについて考察する

2024/02/15に公開

これはなに

ども、レバテック開発部のもりたです。花粉症の薬を飲み始めました。

今回はデータベース設計のアンチパターンとして有名ながら、わりとやりがちな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の正しい歩き方

Web記事

SQLアンチパターンのEAV(Entity-Attribute-Value)について - Zenn

レバテック開発部

Discussion