📜

「アンチパターン」と呼ばれるEAVを、あえて採用した話 ── 数十万件の属性、数十億件の値と向き合って

はじめに

株式会社イエソドのCEO兼CTOの竹内(@chimerast)です。

データベース設計の書籍やブログを読むと、EAV(Entity-Attribute-Value)は「アンチパターン」として紹介されることが少なくありません。SQLアンチパターンの代表格として挙げられ、「やめておけ」と言われがちな設計パターンです。

しかし、筆者はこれまで2つのBtoB SaaSプロジェクトでEAVを採用してきました。1つ目は、前職ユーザベースの全世界の数百万社の企業および数十万件の勘定科目数十億の値を扱う「Speeda」という大規模な経済情報サービスの財務諸表機能。2つ目は、現職イエソドの人・組織・会社・オフィス・プロジェクトといった多様なEntityの関係性を管理する、「YESOD」という企業内の人・組織をマスタ管理した上でシステムのアカウント管理に繋げる大規模ID管理サービスです。どちらのプロジェクトでも、EAVは合理的な設計選択として機能しています。

本記事では、EAVがなぜアンチパターンとされるのかを整理した上で、2つのプロジェクトそれぞれでの設計判断・工夫・得た知見を時系列でお伝えします。「EAV=悪」という先入観を一度外して、適材適所で使うための考え方を共有できればと思います。

実際のテーブル設計やアプリケーション実装には、もっと色々工夫を入れていますが、ニュアンスが伝わりやすい形に変更しています。


EAVとは何か

EAVは Entity-Attribute-Value(エンティティ・属性・値)の略で、データを「何が(Entity)」「どんな属性を持ち(Attribute)」「その値は何か(Value)」という3つの要素で表現するデータモデルです。

通常のテーブル設計

通常のリレーショナルデータベース設計では、属性はカラムとして定義します。

CREATE TABLE accounts (
    id         BIGINT PRIMARY KEY,
    name       VARCHAR(255),
    category   VARCHAR(100),
    balance    DECIMAL(18,2)
);

属性が固定で、レコード数が増えても構造は変わりません。

EAVによる設計

EAVでは、属性をカラムではなく「行」として表現します。

CREATE TABLE entities (
    id    BIGINT PRIMARY KEY,
    name  VARCHAR(255)
);

CREATE TABLE attributes (
    id    BIGINT PRIMARY KEY,
    name  VARCHAR(255),
    type  VARCHAR(50)  -- データ型のメタ情報
);

CREATE TABLE entity_attribute_values (
    entity_id     BIGINT REFERENCES entities(id),
    attribute_id  BIGINT REFERENCES attributes(id),
    value         TEXT,
    PRIMARY KEY (entity_id, attribute_id)
);

属性の数がどれだけ増えても、テーブル構造の変更(ALTER TABLE)は不要です。新しい属性はattributesテーブルに1行追加するだけで済みます。


なぜEAVはアンチパターンとされるのか

EAVがアンチパターンと呼ばれるのには、それなりに正当な理由があります。ここを理解しないまま採用すると痛い目を見るので、まずはデメリットを正直に整理します。

1. SQLが複雑になる

EAVでは、通常なら SELECT name, balance FROM accounts で済むクエリが、JOINやPIVOT操作を伴う複雑なものになります。属性ごとにJOINが必要になるため、属性が増えるほどクエリは肥大化します。

-- EAVから「名前」と「残高」を取りたいだけでもこうなる
SELECT
    e.name,
    v1.value AS category,
    v2.value AS balance
FROM entities e
LEFT JOIN entity_attribute_values v1
    ON e.id = v1.entity_id AND v1.attribute_id = (SELECT id FROM attributes WHERE name = 'category')
LEFT JOIN entity_attribute_values v2
    ON e.id = v2.entity_id AND v2.attribute_id = (SELECT id FROM attributes WHERE name = 'balance')
;

2. データ型の制約が効かない

value列はTEXTやVARCHARになりがちで、RDBMSの型安全性が失われます。数値であるべきフィールドに文字列が入っても、データベース側では防げません。

3. 外部キー制約が使えない

通常のカラム設計なら FOREIGN KEY (category_id) REFERENCES categories(id) のような参照整合性を持たせられますが、EAVの汎用的なvalue列では難しくなります。特にEntity同士の関係性(親子関係など)を表現したい場合に、この制約は大きな痛手です。

4. パフォーマンスが劣化しやすい

行数が爆発的に増えます。エンティティ1件あたり属性の数だけ行が生まれるため、通常設計に比べてテーブルの行数が桁違いに多くなります。また、属性をまたいだ集計やフィルタリングは、複数回のJOINが避けられません。

こうした理由から、「属性が固定で事前に分かっているなら、普通にカラムを定義しなさい」というのがEAV批判の本質です。これは正しいアドバイスであり、ほとんどのケースでは従うべきです。


EAVを採用する際の判断基準

具体的なプロジェクトの話に入る前に、筆者が2つのプロジェクトの経験を通じて整理したEAV採用の判断基準を共有します。

EAVを検討すべき条件

属性の数が非常に多い、または事前に確定できない。 数十程度なら通常のカラム設計で十分です。数千〜数十万となると、カラム設計は破綻します。

属性の追加・変更が頻繁に発生する。 属性が固定で変わらないなら、EAVのメリットは薄いです。ビジネス要件として属性が動的に変化する場合にEAVが活きます。

データがスパースである。 全エンティティが全属性を持つなら、通常のテーブル設計のほうが効率的です。ほとんどのエンティティが属性の一部しか持たない場合に、EAVは格納効率で優位に立ちます。

EAVを避けるべきケース

属性が数十個以下で固定。 ユーザーテーブルに名前・メール・住所を持たせるような設計にEAVは過剰です。

複雑な集計やレポーティングが主要なユースケース。 EAVでのクロス集計は苦行です。BIツールとの相性も悪くなります。

チームにEAVの運用経験がない。 EAVはクエリの書き方やデータの扱い方が通常と大きく異なります。チーム全体がその特性を理解していないと、保守コストが跳ね上がります。

判断のフローチャート

属性の数は数百以上か?
  └── No → 通常のカラム設計を使う
  └── Yes → 属性は頻繁に追加・変更されるか?
              └── No → JSONBカラムやカラムストア型DBを検討
              └── Yes → データはスパースか?
                          └── No → ワイドテーブル+パーティショニングを検討
                          └── Yes → EAVが有力な選択肢

ただし、EAVに決める前に「JSONBカラム(PostgreSQL)」や「ドキュメントDB(MongoDB等)」といった代替案も必ず検討してください。EAVが最適になるのは、RDBMSの他の機能(トランザクション、JOINによるリレーション、既存システムとの統合)も同時に必要な場合です。

ここからは、筆者が実際にEAVを採用した2つのプロジェクトについて、時系列順にお伝えします。


前職の事例: 財務諸表プロジェクト(Speeda) ── 数十万件の属性と数十億件のデータ

プロジェクトの背景

筆者が最初にEAVを採用したのは、大規模な会計システムのプロジェクトでした。このシステムでは、勘定科目(Chart of Accounts)の数が数十万件に上りました。

「数十万件の勘定科目」と聞くとピンと来ないかもしれませんが、これは複数の企業グループ・複数の会計基準・複数の国をまたいで統合的に管理する必要があったためです。各企業が独自の勘定科目体系を持ち、それらを連結・マッピングする必要がありました。

なぜカラム設計では対応できなかったのか

仮に勘定科目をカラムとして設計した場合を考えてみてください。

数十万のカラムを持つテーブルは、そもそもRDBMSの制約に引っかかります(PostgreSQLのカラム上限は1,600、MySQLはストレージエンジンによりますが数千程度)。仮に技術的に可能だったとしても、勘定科目は頻繁に追加・変更されます。新しい子会社が加わるたびにALTER TABLEでカラムを追加するのは運用として現実的ではありません。

また、勘定科目の「属性」自体も多様です。科目コード、科目名、科目区分、集約先科目、通貨、税区分など、各科目が持つメタ情報も企業ごとに異なります。

EAVが自然にフィットした理由

このプロジェクトでは、勘定科目そのものをAttribute(属性)として扱いました。つまり、Entity(企業)に対して、どの勘定科目にいくらの金額が計上されたかをAttribute-Valueのペアで表現したのです。

Entity(企業ID: 1001)
  ├── Attribute: 売上高(科目ID: 40100)  → Value: 1,000,000
  ├── Attribute: 営業利益(科目ID: 11000)  → Value: 1,000,000
  └── Attribute: 経常利益(科目ID: 21500)  → Value: 100,000

このモデルにはいくつかの利点がありました。

科目の追加がデータ操作だけで完結する。 新しい勘定科目が必要になっても、attributesテーブルにINSERTするだけです。DDLの変更もアプリケーションの改修も不要です。

科目体系が企業ごとに異なることを自然に表現できる。 A社は科目コード40100を「売上高」、B社は「Revenue」と呼ぶかもしれません。EAVならattributesテーブルのメタ情報で柔軟に対応できます。

スパースなデータを効率的に格納できる。 全エンティティが全属性を持つわけではありません。ある企業はごく少数の勘定科目にしか関係しないため、カラム設計では大量のNULLが生まれますが、EAVなら該当する行だけを持てばよいのです。

時間軸の組み込み ── period_idの追加

財務諸表データには、もう一つ避けて通れない要素があります。年度や四半期といった期間の概念です。

同じ企業・同じ勘定科目であっても、「2024年度Q1の売上高」と「2024年度Q2の売上高」は別のデータです。財務諸表システムでは、すべてのデータが何らかの会計期間に紐づいています。

この時間軸をどうEAVに組み込むかは重要な設計判断でした。選択肢としては、期間をAttributeとして扱う方法、Entityに期間情報を持たせる方法などが考えられましたが、筆者のプロジェクトではEAVテーブルに period_id カラムを追加するアプローチを取りました。

CREATE TABLE periods (
    id         BIGINT PRIMARY KEY,
    fiscal_year INT,           -- 会計年度
    quarter    INT,            -- 四半期(1〜4、NULLなら年度単位)
    start_date DATE,           -- 期初 
    end_date   DATE            -- 期末
);

CREATE TABLE entity_attribute_values (
    entity_id     BIGINT REFERENCES entities(id),
    attribute_id  BIGINT REFERENCES attributes(id),
    period_id     BIGINT REFERENCES periods(id),  -- 会計期間への参照
    value         DECIMAL,
    PRIMARY KEY (entity_id, attribute_id, period_id)
);

注目すべきは、主キーが (entity_id, attribute_id) の2カラムから (entity_id, attribute_id, period_id) の3カラムに変わった点です。これにより、同じEntityの同じAttributeに対して、期間ごとに異なるValueを持たせることが可能になりました。

Entity(企業ID: 2001・A商事)
  ├── Attribute: 営業利益(科目ID: 11000)
  │     ├── 2024年度Q1 → Value: 5,000,000
  │     ├── 2024年度Q2 → Value: 3,200,000
  │     └── 2024年度Q3 → Value: 7,800,000
  └── Attribute: 売上高(科目ID: 40100)
        ├── 2024年度Q1 → Value: 12,000,000
        └── 2024年度Q2 → Value: 15,000,000

この設計が効果的だった理由はいくつかあります。

期間をまたいだ比較が容易になる。 period_id でフィルタリングするだけで、特定の四半期のデータを取得できます。前年同期比や四半期推移の集計も、period_idをJOINの条件に加えるだけで実現できます。

期間のマスタ管理がシンプルになる。 periodsテーブルに会計年度・四半期・開始日・終了日を持たせることで、「3月決算の企業」と「12月決算の企業」が混在しても、期間の定義をperiodsテーブルで吸収できます。

EAVモデルとの親和性が高い。 period_idを追加したことで、EAVテーブルは実質的に「誰の(Entity)・何が(Attribute)・いつの(Period)・いくら(Value)」という4次元のデータキューブになりました。財務諸表データの本質的な構造と非常に相性が良く、クエリの見通しも良くなりました。

期間をAttributeとして扱うのではなくカラムとして追加した理由は、期間は勘定科目のような「動的に増える属性」ではなく、すべてのデータに共通する軸だからです。EAVの柔軟性を活かす部分と、固定的な構造で守る部分を分けることが、使いやすいモデルにするポイントでした。

財務諸表プロジェクトでの実践知見

Attributeテーブルをスキーマ定義として活用する

EAVの弱点である型安全性を補うため、attributesテーブルにメタ情報を充実させました。

CREATE TABLE attributes (
    id         BIGINT PRIMARY KEY,
    name       VARCHAR(255),
    currency   VARCHAR(50),   -- 'USD', 'JPY', 'EUR' など
    group_id   BIGINT          -- 科目グループでの分類
);

currency はアプリケーション層での通貨の出し分けに使っています。attributesテーブルが単なる名前の一覧ではなく、スキーマ定義の役割を担うように設計することが、EAVをうまく運用するコツだと感じています。

数十億件でも戦えたインデックス設計

EAVに対する最も多い懸念は「パフォーマンスが出ないのではないか」という点でしょう。この懸念は理解できますが、このプロジェクトでは、entity_attribute_valuesテーブルが数十億件の規模に達しても、実用上問題のないパフォーマンスを維持できていました。

数十万件の勘定科目 × 大量の企業 × 複数の会計期間が掛け合わさると、行数は容易に数十億のオーダーに膨らみます。「EAVは行数が爆発する」というアンチパターンの指摘はまさにその通りで、実際に爆発しました。しかし、行数が多いことと、パフォーマンスが出ないことはイコールではありません。

これを支えたのは、アクセスパターンに基づいた複合インデックスの設計です。

-- 「あるエンティティの全属性を取得」パターン用
CREATE INDEX idx_eav_entity ON entity_attribute_values (entity_id, attribute_id);

-- 「ある属性を持つ全エンティティを検索」パターン用
CREATE INDEX idx_eav_attribute ON entity_attribute_values (attribute_id, entity_id);

-- 「特定期間のデータを取得」パターン用(period_id導入後)
CREATE INDEX idx_eav_period ON entity_attribute_values (period_id, attribute_id, entity_id);

ポイントは、実際のクエリパターンから逆算してインデックスを設計したことです。

このプロジェクトの主要なアクセスパターンは、「特定の企業の、複数の勘定科目に関するデータを取得する」というものでした。つまり、ほぼすべてのクエリで entity_idattribute_id による絞り込みが先に入ります。数十億件のテーブルであっても、複合インデックスで entity_idattribute_id と絞り込めば、実際にスキャンする行数はごく一部に限定されます。

逆に言えば、インデックスなしでフルスキャンが走るクエリを書いてしまえば、数十億件のEAVテーブルは確かに破綻します。EAVのパフォーマンス問題の本質は行数そのものではなく、アクセスパターンを無視したインデックス設計(あるいはインデックスの欠如) にあると筆者は考えています。

「とりあえず全カラムにインデックス」でもなく、「インデックスなしでまず動かす」でもなく、クエリの WHERE 句と JOIN 条件を事前に洗い出し、それに合致する複合インデックスを張る。この当たり前のことを丁寧にやるだけで、数十億件規模のEAVテーブルでも実用的なレスポンスタイムを実現できました。

考慮した点

マイグレーションの概念が変わる。 通常のDB設計では、スキーマ変更はマイグレーションファイルで管理します。しかしEAVでは属性の追加がDMLになるため、「スキーマ変更」と「データ変更」の境界が曖昧になります。これに対しては、属性の追加・変更もマイグレーションの一部として管理するルールをチーム内で定めました。属性マスタの変更をシードデータとしてバージョン管理に含めることで、環境間の一貫性を保つことができました。

ORMとの相性。 多くのORMはEAVを前提としていないため、素直にモデル定義できません。EAVの読み書きを抽象化するリポジトリ層を独自に実装し、アプリケーションコードからはEAVの存在を意識しなくて済むようにしました。これにより、ビジネスロジック側のコードはシンプルに保てましたが、リポジトリ層の実装と保守にはそれなりのコストがかかりました。


現職の事例: ID管理プロジェクト(YESOD) ── 多様なEntityの関係性管理とKotlinによる型安全な設計

財務諸表プロジェクト(Speeda)でEAVの有効性を実感した筆者は、現在開発を続けているのID管理プロジェクト(YESOD)でもEAVを採用しています。こちらのプロジェクトでは、人(Person)、組織(Organization)、会社(Company)、オフィス(Office)、プロジェクト(Project) という複数種類のEntityを扱っており、それらの関係性に基づく値を管理しています。

勘定科目プロジェクトでは「大量の属性を柔軟に扱う」ことがEAV採用の主な動機でしたが、現在のプロジェクトではそれに加えて、Entity間の関係性の表現型安全性の確保という新たな課題に取り組んでいます。

ReferenceIdの導入 ── Entity間の関係性を表現する

標準的なEAVの限界

基本的なEAVモデルでは、あるEntityが持つ「値」はテキストや数値といったプリミティブな値しか扱えません。しかし現実のデータには、Entity同士が親子関係や参照関係で結びついているケースが頻繁に存在します。

このプロジェクトでは、Entity間の関係性がいくつものパターンで存在します。

人 → 組織     (所属関係: 田中さんは開発部に所属)
人 → プロジェクト (アサイン関係: 田中さんはプロジェクトAに参画)
人 → オフィス   (勤務地: 田中さんは東京オフィス勤務)
組織 → 会社    (帰属関係: 開発部はX社に帰属)

通常のRDB設計でこれを素直にモデリングすると、関係性の数だけ中間テーブルが増えていきます。person_organizationsperson_projectsperson_officesorganization_companies……と、テーブルが増殖し、それぞれにマイグレーションとリポジトリが必要になります。さらに、ビジネス要件の変化で新しい関係性(「人→メンター」「組織→拠点」など)が求められるたびに、DDL変更とアプリケーション改修が発生します。

これらの関係性をEAVの value 列(TEXT型)にEntity IDを文字列として格納する方法も考えられますが、それでは参照整合性が一切保証されません。参照先のEntityが削除されてもvalueはそのまま残り、孤児データが生まれます。EAVのアンチパターンとして挙げられる「外部キー制約が使えない」問題がまさにここで顕在化します。

reference_idカラムの追加

そこで、EAVテーブルに reference_id というカラムを追加しました。

CREATE TABLE entity_attribute_values (
    entity_id     BIGINT REFERENCES entities(id),
    attribute_id  BIGINT REFERENCES attributes(id),
    reference_id  BIGINT REFERENCES entities(id),  -- 他のEntityへの参照
    value         BYTEA,
    PRIMARY KEY (entity_id, attribute_id, reference_id)
);

値がプリミティブなデータ(金額、名前など)であれば value を使い、別のEntityとの関係性を表現する場合は reference_id を使う、という使い分けです。

EAV+ReferenceIdのモデルでは、先ほどの関係性がすべて同じテーブルで表現できます。

-- Attributeとして関係性を定義
INSERT INTO attributes (id, name, data_type) VALUES
    (101, '所属組織', 'reference'),
    (102, 'アサインプロジェクト', 'reference'),
    (103, '勤務オフィス', 'reference');

-- 田中さん(Entity ID: 1) の関係性
INSERT INTO entity_attribute_values (entity_id, attribute_id, reference_id, value) VALUES
    (1, 101, 501, NULL),   -- 所属: 開発部(ID: 501)
    (1, 102, 801, NULL),   -- アサイン: プロジェクトA(ID: 801)
    (1, 103, 601, NULL);   -- 勤務地: 東京オフィス(ID: 601)

外部キー制約が機能する。 reference_id はentitiesテーブルへの外部キーなので、参照先のEntityが存在しなければINSERTは失敗します。EAVの弱点である「参照整合性が保証できない」問題を、RDBMSの仕組みの中で解決できました。

新しい関係性の追加がデータ操作だけで完結する。 新しい関係性が必要になっても、attributesテーブルに1行INSERTするだけです。テーブル構造の変更は不要で、アプリケーションコードの改修も最小限で済みます。

関係性に基づく値の管理

さらに興味深いのは、「関係性そのもの」だけでなく「関係性に基づく値」も管理できる点です。例えば、田中さんがプロジェクトAにアサインされているという関係性に対して、アサイン開始日やアサイン比率(稼働率)といった属性を紐づけたいケースがあります。

このような場合、Entity-Attribute-Valueの行自体を一つの「関係性Entity」として捉え、その関係性にさらに属性を持たせる設計にすることで、柔軟に対応できます。詳細は省きますが、ポイントはEAVモデルの構造を再帰的に応用することで、関係性のメタデータまで同じ枠組みで扱えるということです。

Entityの種類が増えてもモデルが変わらない強み

このプロジェクトでは、立ち上げ当初は「人」と「組織」の2種類のEntityだけを扱う予定でした。しかし開発が進むにつれ、「会社」「オフィス」「プロジェクト」と管理対象のEntityが増えていきました。

通常のRDB設計であれば、Entityの種類が増えるたびにテーブルを追加し、既存テーブルとのリレーションを設計し直す必要があります。しかしEAVモデルでは、entitiesテーブルに種類を示す entity_type カラムを持たせるだけで、テーブル構造の変更なくEntityの種類を拡張できました。これは、要件が流動的なプロジェクトにおいて非常に大きなアドバンテージでした。

Valueのバイナリ格納とKotlinによる型安全なシリアライズ

勘定科目プロジェクトでは、EAVの「データ型の制約が効かない」問題に対して、attributesテーブルの data_type とアプリケーション層のバリデーションで対処していました。現在のプロジェクトでは、この問題に対してより踏み込んだアプローチを取っています。

valueをバイナリデータとして格納する

現在のプロジェクトでは、value列の型を BYTEA(バイナリ)としています。TEXT型ではなくバイナリにしている理由は、格納する値の型が多様だからです。このプロジェクトでは、数値型、文字列型に加えてJSON型の構造化データも扱っています。これらを統一的に扱うために、すべてのvalueをシリアライズしたバイナリとして格納しています。

Kotlinのシリアライズ・デシリアライズで型安全性を確保する

「バイナリで格納する」と聞くと、型安全性がさらに失われるように思えるかもしれません。しかし実際には逆です。Kotlin側でシリアライズ・デシリアライズの仕組みをしっかり作ることで、アプリケーションコードの型安全性はむしろ高く保てています

具体的には、attributesテーブルの data_type に基づいて、Kotlin側で型ごとのシリアライザを用意しています。

// Valueの型を表現するsealed interface
sealed interface AttributeValue {
    data class NumberValue(val value: BigDecimal) : AttributeValue
    data class StringValue(val value: String) : AttributeValue
    data class JsonValue(val value: JsonNode) : AttributeValue
}

// data_typeに基づくシリアライズ・デシリアライズ
object AttributeValueSerializer {
    fun serialize(value: AttributeValue): ByteArray = when (value) {
        is AttributeValue.NumberValue -> encodeDecimal(value.value)
        is AttributeValue.StringValue -> value.value.toByteArray(Charsets.UTF_8)
        is AttributeValue.JsonValue   -> objectMapper.writeValueAsBytes(value.value)
    }

    fun deserialize(bytes: ByteArray, dataType: String): AttributeValue =
        when (dataType) {
            "decimal" -> AttributeValue.NumberValue(decodeDecimal(bytes))
            "varchar" -> AttributeValue.StringValue(String(bytes, Charsets.UTF_8))
            "json"    -> AttributeValue.JsonValue(objectMapper.readTree(bytes))
            else      -> throw IllegalArgumentException(
                             "Unknown data type: $dataType"
                         )
        }
}

この設計により、以下のようなメリットが得られています。

コンパイル時に型の不整合を検出できる。 Kotlinのsealed interfaceとwhen式の網羅性チェックにより、すべてのデータ型に対するハンドリングが強制されます。新しいデータ型を追加した場合、対応するデシリアライズ処理を書かなければコンパイルエラーになります。

データベースに格納される値のフォーマットが厳密に制御される。 シリアライザを一箇所に集約しているため、「数値なのに不正なバイト列が入っている」といった不整合が起きません。TEXT型で格納した場合の「数値であるべきカラムに空文字が入っている」問題とは無縁です。

JSON型で構造化データを柔軟に格納できる。 単純なKey-Valueに収まらない複雑な属性値(例えば、住所の構造化データや、プロジェクトのメタ情報など)をJSON型として格納し、Kotlin側で型安全にパースできます。

TEXT型ではなくバイナリを選んだ理由

TEXT型でも数値や日付を文字列化して格納することは可能です。しかしバイナリ格納には、TEXT型にはない利点があります。数値の精度が文字列変換で劣化しない点、JSONなどの構造化データを圧縮しつつ効率的に格納できる点、そしてシリアライザを通す設計を強制できるため「とりあえず文字列で入れておく」という運用上の怠慢を防げる点です。特に最後の点は、チーム開発において地味に効いています。


まとめ ── EAVは「禁じ手」ではなく「選択肢」

EAVがアンチパターンとされるのは、多くのケースで不適切に使われてきた歴史があるからです。属性が固定で少数なのにEAVを採用するのは、確かにアンチパターンです。

しかし、属性が動的で大量、データがスパース、スキーマ変更を最小限にしたいという条件が揃ったとき、EAVは合理的な設計選択になります。

前職の財務諸表プロジェクト(Speeda)では、数十万件の勘定科目をAttributeとして扱い、period_idで時間軸を組み込むことで財務諸表データの構造を自然に表現しました。数十億件に達したentity_attribute_valuesテーブルでも、適切なインデックス設計によってパフォーマンスを維持できたことは、「EAVはスケールしない」という通説への実践的な反証です。

現職のID管理プロジェクト(YESOD)では、そこで得た知見をさらに発展させています。ReferenceIdによるEntity間の関係性表現で外部キー制約の問題を解決し、バイナリ格納+Kotlinのシリアライズ・デシリアライズで型安全性を確保する。EAVの弱点とされてきた問題に対して、一つずつ実践的な解を見つけてきた過程でもあります。

大事なのは、EAVの弱点を正しく理解した上で、それを補う工夫とセットで採用することです。どんな設計パターンにも言えることですが、「アンチパターン」というラベルに思考停止せず、自分のプロジェクトの要件に照らして判断する姿勢が、良い設計への近道ではないでしょうか。


この記事が、EAVの採用を迷っている方の判断材料になれば幸いです。ご質問やフィードバックがあれば、ぜひコメントで教えてください。

イエソド テックブログ

Discussion

ctrlzrctrlzr

ある期間の仕訳を取得するのは時間が掛かりそうに思いました。
実際のクエリパターンから逆算~とのことなので、期間のみが指定されるケースは稀ということでしょうか?

たけうちさんは縮退しました🌀たけうちさんは縮退しました🌀

会計システムではなく、主に投資家向けに、企業の決算における財務諸表を複数企業・複数勘定科目・複数年度の軸で比較するためのシステムですので、多くて年度・半期・四半期の単位でしか値を持ちません。

10年分比較するとしても1勘定科目当たり70件で、一般的なユースケースには耐えられます。仕訳データのように1年に大量に作られるものではありません。

言葉が足りなくて、会計システムの文脈で読まれている方も結構いそうなので、序文をちょっと修正します。