動的属性の保存 — Sparse Columns、EAV、JSONB の解説
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
データ管理の進化し続ける状況において、アプリケーションは、非常に変動的で予測不可能な属性を持つデータを処理する課題に直面することがよくあります。従来の正規化データベーススキーマは、固定された列構造により、このような要件に効率的に適応するのが難しい場合があります。たとえば、電子機器、アパレル、書籍など、それぞれ固有の特性を持つ製品カテゴリを持つ e コマース プラットフォームや、ユーザーがカスタム フィールドを定義できるユーザー プロファイル システムなどを想像してみてください。考えられるすべての属性を 1 つの広いテーブルに強制すると、多数の空の列と非効率的なストレージが発生しますが、属性セットごとに個別のテーブルを作成すると、スキーマ管理の悪夢になる可能性があります。この記事では、Sparse Columns、EAV (Entity-Attribute-Value)、JSONB の 3 つの主要なデータベース設計パターンを掘り下げ、動的属性を効果的に格納およびクエリするためのさまざまな戦略を提供します。それぞれのパターンには、トレードオフと理想的なユースケースがあります。
動的属性管理のコアコンセプト
パターンを分析する前に、動的属性の格納を理解するために不可欠なコアコンセプトを定義しましょう。
- 固定スキーマ: テーブルにあらかじめ定義された静的な列セットがあるデータベース設計。これは従来の正規化モデルです。
- 動的属性: 事前にわかっていない、または同じタイプのエンティティ間で大きく変動するエンティティのプロパティ。
- データ疎性: テーブルのデータのかなりの部分が
NULL
値で構成されている状況。これは、多くの列が特定の行に対して使用されていないことを示していることがよくあります。 - 柔軟性: スキーマの変更を必要とせずに、新しい属性を簡単に追加したり、既存の属性を変更したりできること。
- クエリパフォーマンス: 動的属性に基づくデータの取得とフィルタリングの速度と効率。
- ストレージ効率: データの格納にディスクスペースがどのように効果的に利用されるか、無駄を最小限に抑えます。
動的属性の設計パターン
1. Sparse Columns: 列セットの活用
Sparse columns は、主に SQL Server に見られる機能であり、物理ストレージスペースを消費せずに NULL
値を格納するように設計されています。COLUMN_SET
(テーブル内のすべての sparse column の型指定なし XML 表現) と組み合わせると、単一の広いテーブル内で変動する列セットを管理する方法を提供します。
原則: Sparse columns は、値の不在を示すメタデータを格納します。値が存在する場合、通常どおり格納されます。COLUMN_SET
を使用すると、アプリケーションは 1 つの XML ドキュメントとして行のすべての sparse column 値を読み書きできます。
実装 (SQL Server):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255), -- Common attributes Price DECIMAL(10, 2), -- Sparse columns for dynamic attributes ScreenSizeInches DECIMAL(3, 1) SPARSE NULL, -- For electronics ProcessorType VARCHAR(50) SPARSE NULL, -- For electronics Material VARCHAR(50) SPARSE NULL, -- For apparel Author VARCHAR(100) SPARSE NULL, -- For books -- A column set for easy access to all sparse columns AllDynamicAttributes XML COLUMN_SET FOR ALL_SPARSE_COLUMNS ); -- Inserting an electronic product INSERT INTO Products (ProductID, ProductName, Price, ScreenSizeInches, ProcessorType) VALUES (1, 'Laptop X', 1200.00, 15.6, 'Intel i7'); -- Inserting an apparel product INSERT INTO Products (ProductID, ProductName, Price, Material) VALUES (2, 'T-Shirt', 25.00, 'Cotton'); -- Querying the column set for a product SELECT ProductID, ProductName, AllDynamicAttributes FROM Products WHERE ProductID = 1;
アプリケーション シナリオ:
- 多数の列が一部の行にのみ適用される場合(データ疎性 > 20-40%)。
- 固定された既知の属性がデータの大部分を構成する場合。
- 潜在的な動的属性のセットが比較的安定しており、スキーマ作成時に定義できる場合。
- 主に SQL Server 環境での使用。
利点:
- スキーマの単純さ: すべての属性が 1 つのテーブルにあり、JOIN が簡素化されます。
- 型安全性: 各 sparse column は定義されたデータ型を保持します。
- パフォーマンス: コア属性および特定の sparse column に対するクエリでパフォーマンスを発揮できます。インデックスを直接作成できます。
- ストレージ最適化:
NULL
値はスペースを消費しません。
欠点:
- ベンダー ロックイン: 主に SQL Server の機能です。
- スキーマ進化: 動的属性の追加には、依然として
ALTER TABLE
が必要です。 - スケーラビリティ: 数百または数千の sparse column を持つテーブルは扱いにくくなり、列の制限に達する可能性があります。
COLUMN_SET
オーバーヘッド:COLUMN_SET
の読み書きには XML 解析が含まれます。
2. EAV (Entity-Attribute-Value): 柔軟なトリオ
EAV は、「オープン スキーマ」または「縦型テーブル」設計としても知られ、3 つの列(エンティティ ID、属性名、値)を使用して動的属性を格納します。
原則: 列が属性を表すのではなく、行がエンティティの属性値ペアを表します。各エンティティは、任意の数の属性値ペアを持つことができます。
実装 (汎用 SQL):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255) -- ... other common attributes ); CREATE TABLE ProductAttributes ( ProductAttributeID INT PRIMARY KEY IDENTITY(1,1), ProductID INT NOT NULL, AttributeName VARCHAR(100) NOT NULL, AttributeValue VARCHAR(MAX) NOT NULL, -- Storing all values as string type FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ); -- Inserting an electronic product's dynamic attributes INSERT INTO ProductAttributes (ProductID, AttributeName, AttributeValue) VALUES (1, 'ScreenSizeInches', '15.6'), (1, 'ProcessorType', 'Intel i7'); -- Inserting an apparel product's dynamic attributes INSERT INTO ProductAttributes (ProductID, AttributeName, AttributeValue) VALUES (2, 'Material', 'Cotton'); -- Querying product with its specific attribute SELECT p.ProductName, pa.AttributeName, pa.AttributeValue FROM Products p JOIN ProductAttributes pa ON p.ProductID = pa.ProductID WHERE p.ProductID = 1; -- To get attributes as columns (pivot-like) - more complex queries SELECT p.ProductName, MAX(CASE WHEN pa.AttributeName = 'ScreenSizeInches' THEN pa.AttributeValue END) AS ScreenSizeInches, MAX(CASE WHEN pa.AttributeName = 'ProcessorType' THEN pa.AttributeValue END) AS ProcessorType, MAX(CASE WHEN pa.AttributeName = 'Material' THEN pa.AttributeValue END) AS Material FROM Products p LEFT JOIN ProductAttributes pa ON p.ProductID = pa.ProductID GROUP BY p.ProductID, p.ProductName;
アプリケーション シナリオ:
- 非常に動的で予測不可能な属性セット。
- 属性のセットが頻繁に変更または増加する場合。
- レポート要件が、特定の列型を必要とする複雑な分析クエリではなく、キーと値のルックアップで満たされることが多い場合。
- スキーマレスまたは柔軟なデータ モデルが最重要である場合。
利点:
- 最大の柔軟性: 新しい属性のためのスキーマ変更は不要です。
- スケーラビリティ: エンティティあたりほぼ無制限の動的属性を処理できます。
- ストレージ効率: 存在する属性値ペアのみを格納し、
NULL
はありません。
欠点:
- 複雑なクエリ: 特定の属性を取得したり、複数の属性をクエリしたりするには、多くの場合、自己結合、ピボット、または複雑なサブクエリが必要になり、パフォーマンスが低下します。
- 型安全性なし: すべての値は通常、汎用文字列型 (
VARCHAR(MAX)
) として格納され、アプリケーションレベルの変換と検証が必要です。 - データ整合性の課題: 動的属性のデータ型、制約、およびリレーションシップの強制は困難です。
- パフォーマンスのオーバーヘッド: 大量の結合と巨大なテーブル スキャンは、特に多数の属性またはエンティティがある場合、クエリ パフォーマンスに深刻な影響を与える可能性があります。
3. JSONB: 正規化データベース内のネイティブ ドキュメント ストア
JSONB (JSON Binary) は、PostgreSQL で利用可能なネイティブ JSON データ型です (MySQL の JSON、SQL Server の JSON など、機能が異なる他のデータベースにも同様の実装があります)。JSON データを分解されたバイナリ形式で格納し、JSON ドキュメント内のキーと値の効率的なインデックス作成とクエリを可能にします。
原則: 別個の列または行の代わりに、エンティティのすべての動的属性は、専用の列内の単一の JSON ドキュメントに格納されます。データベース エンジンは、この構造化データと対話するための関数と演算子を提供します。
実装 (PostgreSQL):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255), Price DECIMAL(10, 2), DynamicAttributes JSONB ); -- Inserting an electronic product INSERT INTO Products (ProductID, ProductName, Price, DynamicAttributes) VALUES (1, 'Laptop X', 1200.00, '{"ScreenSizeInches": 15.6, "ProcessorType": "Intel i7"}'); -- Inserting an apparel product INSERT INTO Products (ProductID, ProductName, Price, DynamicAttributes) VALUES (2, 'T-Shirt', 25.00, '{"Material": "Cotton", "Size": "M"}'); -- Querying a specific attribute SELECT ProductID, ProductName, DynamicAttributes->>'ScreenSizeInches' AS ScreenSize FROM Products WHERE ProductID = 1; -- Filtering by an attribute SELECT ProductID, ProductName, DynamicAttributes FROM Products WHERE DynamicAttributes->>'Material' = 'Cotton'; -- Creating an index on a specific JSONB path for faster queries (Gin index) CREATE INDEX idx_products_material ON Products USING GIN ((DynamicAttributes->'Material')); -- Updating a specific attribute UPDATE Products SET DynamicAttributes = jsonb_set(DynamicAttributes, '{ScreenSizeInches}', '13.3'::jsonb) WHERE ProductID = 1;
アプリケーション シナリオ:
- 属性がブロックとして、または部分的に一緒にクエリされることが多い場合。
- スキーマの柔軟性とクエリ パフォーマンスの良好なバランスが望ましい場合。
- JSON データと自然に連携するアプリケーションとの統合。
- 動的属性に何らかの内部構造がある場合(例: ネストされたオブジェクトまたは配列)。
- PostgreSQL が選択されたデータベースである場合。
利点:
- スキーマの柔軟性: テーブル スキーマを変更せずに新しい属性を追加または削除できます。
- 良好なクエリ パフォーマンス: JSONB はキーと値のインデックス作成を可能にし、ネストされたデータに対するクエリを大幅に高速化します。
- 単一列: 動的データ用の単一列の単純さ。
- 豊富なデータ型: JSON はさまざまなデータ型(文字列、数値、ブール値、配列、オブジェクト)をサポートします。
- 半構造化データ: 複雑なネスト構造を格納できます。
欠点:
- データベース固有: 機能とパフォーマンスは、さまざまな SQL データベース間で大きく異なります。PostgreSQL の JSONB は特に堅牢です。
- ネイティブ正規化制約の欠如: データベースレベルの型強制および外部キー制約は、JSONB 内のデータには直接適用できません。検証は通常、アプリケーションレイヤーまたは高度なチェック制約で行われます。
- クエリの複雑さの増加: JSONB 演算子を含むクエリは、標準 SQL よりも最初は直観的でない場合があります。
- 任意キーのインデックス作成の制限: 特定のキーはインデックス作成できますが、すべての可能なキー(不明なセットの場合)をインデックス作成することは現実的ではありません。
結論
Sparse Columns、EAV、JSONB のいずれかを選択するのは、一般的に「最良」のいずれかという問題ではなく、アプリケーションの要件、データの特性、クエリパターン、および選択したデータベース システムに最も効果的に適合するパターンはどれかという問題です。Sparse Columns は、適度に疎で事前に定義された属性に対して、型安全性と SQL Server 固有のストレージの利点を提供します。EAV は、クエリ パフォーマンスとデータ整合性への多大なコストを伴う、非常に変動的で予測不可能な属性セットに究極の柔軟性を提供します。JSONB は、スキーマの柔軟性、効率的なクエリ(特にインデックス作成)、および半構造化データのサポートを提供する説得力のあるバランスをもたらし、PostgreSQL のようなデータベースの最新アプリケーションにとって強力なソリューションとなっています。柔軟性、パフォーマンス、型安全性、およびスキーマの複雑さのトレードオフを慎重に検討することにより、開発者は動的データを効率的に管理するための最も適切な戦略を選択できます。