Elasticsearch は MySQL のような多テーブル JOIN クエリを直接サポートしません。これは反正規化設計、分散アーキテクチャ、近似実時間処理の本質に起因します。JOIN を強制的に実装するとパフォーマンス急落、データ不整合、メンテナンスコスト上昇を招きます。
しかし適切なデータモデル設計により、90% の JOIN 要件を ES のネイティブ機能で効率的に処理可能です。
1. JOIN の本質: 関係型データベースの基盤
MySQL JOIN の例
SELECT a.author_name, p.title
FROM authors a
JOIN posts p ON a.id = p.author_id
WHERE a.status = 'verified';
- 依存要素:
- 正規化設計(データを複数テーブルに分割)
- ACID トランザクション(関連データの一貫性保証)
- ネストループ/ハッシュ JOIN アルゴリズム(実行時計算)
本質の違い: JOIN は「実行時関連付け」、ES は「書き込み時関連付け」
2. ES が JOIN をサポートしない理由
1. 分散アーキテクチャの制約
- データシャーディング:
authorsとpostsが異なるノードに分散 - JOIN にはノード間通信が必要 → ネットワークオーバーヘッドが指数関数的に増加(O(n²))
2. パフォーマンスモデルの不一致
- ES は高スループット書き込み/低遅延検索を最適化
- JOIN はランダム I/O + 複雑計算を必要 → ES のパフォーマンスモデルを破壊
3. データモデルの根本的違い
- ES = ドキュメント指向データベース
- 核心思想: 反正規化(関連データを1ドキュメントに埋め込む)
ES の哲学: 「計算を削減するためのストレージ活用」を基本とする
3. 代替策: 4つのデータモデル変換戦略
戦略1: 反正規化(Denormalization)— 推奨
- 適用ケース: 1:N かつ次元テーブルが小さい(例: 著者情報)
- 実装:
postsドキュメントにauthorsフィールドを埋め込む - ES ドキュメント:
{ "post_id": 205, "title": "JavaScript チュートリアル", "author": { "id": 301, "name": "Alice", "status": "verified" } } - クエリ:
{ "query": { "bool": { "must": [ { "match": { "title": "javascript" } }, { "term": { "author.status": "verified" } } ] } } } - メリット: 単一ドキュメントでの検索 → 最高パフォーマンス
- コスト: データ重複、更新時の同期処理が必要
戦略2: 埋め込みオブジェクト(Nested Objects)
- 適用ケース: 1:N かつサブオブジェクトを独立して検索する必要あり(例: 投稿 + コメント)
- ES マッピング:
{ "mappings": { "properties": { "comments": { "type": "nested", "properties": { "user_id": { "type": "keyword" }, "text": { "type": "text" } } } } } } - クエリ:
{ "query": { "nested": { "path": "comments", "query": { "match": { "comments.user_id": "user_123" } } } } } - コスト: 扁平ドキュメントに比べて書き込み/検索パフォーマンスが低下
戦略3: アプリケーション層 JOIN(Client-Side Join)
- 適用ケース: N:M かつ反正規化が不可能
- フロー:
1. ES で
postsを検索 2.author_idリストを抽出 3. MySQL でauthorsを取得 4. アプリケーション層で結果をマージ - コード例:
// 1. ES で投稿検索 $posts = $esClient->search('posts', 'react'); // 2. author_id リストを抽出 $authorIds = array_column($posts, 'author_id'); // 3. MySQL で著者取得 $authors = $pdo->query("SELECT id, name FROM authors WHERE id IN (" . implode(',', $authorIds) . ")")->fetchAll(); // 4. マッピングと結合 $authorMap = array_column($authors, null, 'id'); foreach ($posts as &$post) { $post['author'] = $authorMap[$post['author_id']]; } - メリット: 非常に柔軟
- コスト: N+1 クエリのリスク、遅延増加
戦略4: 親子ドキュメント(Parent-Child)— 慎用
- 適用ケース: データが頻繁に更新され反正規化が不可能
- ES マッピング:
{ "mappings": { "properties": { "doc_type": { "type": "join", "relations": { "author": "post" } } } } } - クエリ(子ドキュメント検索):
{ "query": { "has_child": { "type": "post", "query": { "match": { "title": "react" } } } } } - コスト: パフォーマンスが非常に悪く、小規模データに限定
4. 実践ガイド: モデル選択の基準
| シナリオ | 推奨戦略 | 理由 |
|---|---|---|
| 著者 + 投稿(1:N) | 反正規化 | 著者データが小さく更新頻度が低い |
| 投稿 + コメント(1:N) | 埋め込みオブジェクト | コメントを独立して検索する必要あり |
| タグ + 投稿(N:M) | アプリケーション層 JOIN | タグが動的に変化する |
| 注文 + 商品(リアルタイム) | 反正規化 + CDC | Debezium を活用した MySQL から ES への同期 |
絶対に避けるべきパターン
- ES で Parent-Child を頻繁に使用(パフォーマンス急落の原因)
- アプリケーション層 JOIN にキャッシュを適用しない(MySQL への負荷増大)
5. 高リスクの誤解
誤解1: "ES 7+ は JOIN をサポート"
- 事実:
joinデータ型は SQL JOIN とは異なる。Parent-Child の新実装であり、パフォーマンスは依然として低い - 対策: 反正規化を優先的に検討
誤解2: "Terms Lookup で JOIN を模倣可能"
- 例:
{ "query": { "terms": { "author_id": { "index": "authors", "id": "1", "path": "verified_posts" } } } } - 事実: 静的リストにのみ適用可能、リアルタイム JOIN には不適
- 対策: ダイナミックな関連クエリには使用しない
誤解3: "Elasticsearch は MySQL に置き換え可能"
- 事実: ES は検索エンジン、MySQL はトランザクションデータベース
- 対策: ES + MySQL の共存(CQRS アーキテクチャ)
6. デザインの本質: ストレージを活用して計算を削減
"ES で JOIN を実装しようとするのではなく、JOIN が不要なデータモデルを設計する"
- 脆弱な設計: Parent-Child を頻繁に使用 → パフォーマンス急落
- 頑健な設計: 反正規化 + CDC 同期 → ミリ秒単位の検索
- 結果: 前者はデータ量増加で崩壊、後者はデータ量増加に伴い拡張可能
7. 実践的アクションリスト
## 2025-10-15 データモデル最適化
### 1. 現在の JOIN を分析
- [ ] MySQL での JOIN クエリを特定
### 2. 代替戦略を決定
- [ ] 1:N → 反正規化
- [ ] N:M → アプリケーション層 JOIN
### 3. ES マッピングを実装
- [ ] posts インデックスに author フィールドを追加
### 4. クエリを検証
- [ ] JOIN を単一 ES クエリで置き換え