💬

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

2023/08/05に公開

EAV とは?

EAV(Entity-Attribute-Value)は、属性と値の対応関係を表すテーブルです。EAVは、属性が動的かつ多様であるようなデータを効果的に保存する方法として使用されます。特に、属性の数や種類が固定されていない場合に有用です。ただし、EAVモデルはいくつかのメリットとデメリットを持っているため、適切な使用場面を選択する必要があります。

EAV では、エンティティ(データベース内で区別される対象やオブジェクト)、属性(エンティティに関連する特性や情報の名前)、値(エンティティの属性に対応する実際のデータ)の3つの要素を持つテーブルを使用してデータを保存します。具体的には、以下のような構造を持ちます:

表1:電化製品を表すEAVのテーブル

ここでは、 製品名、価格、サイズ、在庫数、人気度の5つの属性を持つデータをいくつか示しています。各エンティティ(EntityID)に対して、それぞれの属性(Attribute)と値(Value)が関連付けられています。

このように、データベース内でのエンティティと属性の数が多様であり、柔軟性が求められる場合に EAV は有用です。

https://note.com/standenglish/n/n4bdb1d5f2a80

https://shiro-secret-base.com/sqlアンチパターン:eavエンティティ・アトリビュー/

https://qiita.com/skyc_lin/items/37365a36416d0dc42431

ただし、EAV にはいくつか問題点があり、次はその問題点について解説します。

 

EAVの問題点

EAVの問題点として以下のものが挙げられます。

  1. パフォーマンスの低下:データの取得に複雑なJOINが必要で、大規模なデータベースだとデータ量が増加しクエリのパフォーマンスが低下し、問題が発生する可能性があります。
  2. データの一貫性:EAVは属性の動的追加が可能ですが、データの一貫性を保つのが難しいという欠点があります。同じエンティティに対して同じ属性名で複数の値が存在することがあり、それがデータの混乱や正確性の低下を引き起こす可能性があります。
  3. クエリの複雑化:EAVでは、一つのエンティティの情報を取得するために複数のJOINが必要になり、クエリが非常に複雑になります。特に、多くの属性が存在する場合や関連するエンティティを結合する場合には、クエリを作成することが難しく、保守性が低下します。
  4. インデックスの効率:EAVでは、複数の属性が1つの列(Value列)に格納されます。これにより、効率的なインデックスの作成が難しくなり、データベースのパフォーマンスが低下する可能性があります。
  5. データ型の制限:EAVでは、Valueカラムにすべての属性の値が格納されるため、データ型の制限があります。すべての値を文字列として保存する必要があるため、数値や日付などのデータ型を正確に表現することができません。

したがって、EAVは柔軟性がある一方で、データの取得、一貫性の保持、クエリの複雑さ、インデックスの効率などの問題を抱えており、SQLのアンチパターンとされています。

データモデリングの際には、これらの問題をよく理解し、EAVが適切な使用場面かどうかを検討することが重要です。

EAVの問題点の解決策としてサブタイプ(特有の属性(カラム)を持つ子クラス)のモデリングを行うことが重要です。

下記に記載した方法(単一テーブル継承、具象テーブル継承、クラステーブル継承など)がEAVの問題点の解決方法となります。

 

単一テーブル継承(STI)

STI(Single Table Inheritance)は、オブジェクト指向プログラミングの継承概念をデータベースに適用した設計パターンで、複数の関連するエンティティ(テーブル)を1つのテーブルに統合し、共通の属性(カラム)を持つ親クラス(スーパータイプ)とそれに特有の属性を持つ子クラス(サブタイプ)を含む階層的なデータモデルを作成すること を言います。

https://qiita.com/niwa1903/items/218713c076fb0075712f

https://qiita.com/yebihara/items/9ecb838893ad99be0561

https://qiita.com/kidach1/items/789c2e7aebbcfbd2583e

https://sakaishun.com/2021/08/08/stisingle-table-inheritance/#:~:text=参考-,STI(単一テーブル継承)とは,しまうことを言います。

先ほど紹介した表1の EAVのテーブルを STI すると、下記のような形になります。

ただし、STIにはメリット・デメリットがあります。
STIのメリットは、下記の通りです。

①異なるタイプのオブジェクトを1つのテーブルにまとめることができ、テーブル数が減り、データベースの構造がシンプルになる。

②複数のモデルのデータを1つのテーブルに格納しているので、1つのテーブルを参照するだけでデータを取得することができる。

③STIを使用すると共通の属性(カラム)やメソッドを1つの親モデルで定義することができるので、重複するコードを減らし、保守性を高めることができる。

 
デメリットは以下の通りです。

①テーブルのカラム数の増加し、テーブル構造が複雑化する。

②カラムの値がNULLになる可能性があるため、その可能性があるカラムには、NULL制約を設定しなければいけない。

③親モデルのテーブルに変更を加えると、全てのサブクラスに影響が出る可能性がある。

 
また、railsでは STI をデフォルトでサポートしており、テーブル定義とクラスの継承により簡単に使用することができます。

例えば、下記のようなSpeciesクラス、そしてそのSpeciesクラスを継承しているDog,Cat,Birdクラスがあるとします。

イメージ図

上記の内容を STI にすると下記のようになります。

イメージ図

また、上記のクラスに仮にデータを入れてみると下記のような感じになります。

テーブルがどの型(今回の場合、Dog,Cat,Birdの3つのこと)なのかを判断するために type というカラムを作成することで識別しています。

(ただし、typeカラムを作成するときにエラーが発生する可能性あり。こちらを参照。)

上記のように rails で STI を使用するためのコマンドや流れは以下の通りです。

# Animalモデルとanimalsテーブルの作成
rails generate model Animal type:string name:string age:integer

# 2023×××××××_create_animals.rb(マイグレーションファイルの中身)
class CreateAnimals < ActiveRecord::Migration[6.1]
  def change
    create_table :animals do |t|
      t.string :type
      t.string :name
      t.integer :age

      t.timestamps
    end
  end
end

# Animalモデルに self.inheritance_column = :type を追加。app/models/animals.rb
class Animal < ApplicationRecord
  self.inheritance_column = :type
end

# Dogモデルの作成。app/models/dog.rb
class Dog < Animal
end

# Catモデルの作成。app/models/cat.rb
class Cat < Animal
end

# マイグレーションコマンドを実行
rails db:migrate

上記のようにすることで、animals テーブルのみを作成し、dogやcatの情報を管理することができます。

最後にデータを作成し、dogs,catsテーブルを作成せず、animalsテーブルで管理することができるのか検証してみます。

# Dogデータの作成
Dog.create(name: "Fido", age: 3)

# Catデータの作成
Cat.create(name: "Whiskers", age: 2)

# Animalデータの取得
animals = Animal.all

animals.each do |animal|
  puts "#{animal.type}: #{animal.name}, #{animal.age} years old"
end

=>
Dog: Fido, 3 years old
Cat: Whiskers, 2 years old
[#<Dog:0x00007fd1abf8bdb0
  id: 3,
  type: "Dog",
  name: "Fido",
  age: 3,
  created_at: Sat, 05 Aug 2022 17:10:36.846539000 JST +09:00,
  updated_at: Sat, 05 Aug 2022 17:10:36.846539000 JST +09:00>,
 #<Cat:0x00007fd1a942ba08
  id: 4,
  type: "Cat",
  name: "Whiskers",
  age: 2,
  created_at: Sat, 05 Aug 2022 17:10:42.548594000 JST +09:00,
  updated_at: Sat, 05 Aug 2022 17:10:42.548594000 JST +09:00>]

 

具象テーブル継承

具象テーブル継承は、単一テーブル継承(STI)からサブタイプごとのテーブルに分けたものです。

具象テーブルでは、サブタイプは、スーパータイプと同じ属性(カラム)を持ちます。

表1をSTIしたものを具象テーブルに書き換えたものが下記の通りです。

また、先ほどのAnimalクラス、そしてそのAnimalクラスを継承しているDog,Cat,Birdクラスを具象テーブルに書き換えると下記のようになります。

 

クラステーブル継承

クラステーブル継承は、具象テーブル継承からさらに共通の属性(カラム)を抽出しその共通の属性をまとめたテーブルを作成したものを指します。

表1を具象テーブルに書き換えたものをクラステーブルに書き換えると下記のような形になります。

また、先ほどのAnimalクラス、そしてそのAnimalクラスを継承しているDog,Cat,Birdクラスをクラステーブルに書き換えると下記のようになります。

 

半構造化データ

半構造化データは、データの一部が構造化されているが、他の部分は構造化されていない、つまりルールやテーブルに完全に従っていないデータ形式のことを指します。このようなデータは、一部の属性(カラム)は特定の形式に従っているが、他の属性(カラム)は自由な形式で入力されることがあります。

表1を半構造化データに書き換えたものが下記になります。

また、Animalクラス、そしてそのAnimalクラスを継承しているDog,Cat,Birdクラスを半構造化データに書き換えたものが下記になります。

 

参考

https://www.amazon.co.jp/SQLアンチパターン-Bill-Karwin/dp/4873115892

Discussion