2026年3月2日月曜日

すべての PXC ノードに XtraBackup をインストールする必要があるか?

Perconaフォーラムで定期的に浮上する質問:Percona XtraDB Cluster (PXC)のすべてのノードにXtraBackupをインストールする必要がありますか? これは混合環境を管理している場合や特定のノードのソフトウェアフットプリントを最小限に抑えようとする場合に特に妥当な質問です。実際の仕組みとテストが確認する内容を以下に示します。

簡潔な回答(ただし読み進めてください)

そのノードに何をさせたいかによります。 ここでのニュアンスがかなり重要なので、PXCにおけるState Snapshot Transfer (SST)の仕組みと、特定のノードにXtraBackupが存在する — または存在しない — ことがなぜ重要かを詳しく説明する価値があります。

PXCにおけるSSTの簡単なおさらい

新しいノードがPercona XtraDB Clusterに参加する場合、または既存のノードが十分に長時間ダウンしてIncremental State Transfer (IST)がもはや不可能になった場合、クラスタはState Snapshot Transfer (SST)を実行します。これは基本的に、ドナーノードからジョイナーノードへの完全なデータコピーです。

PXCはmy.cnfで設定される複数のSST方法をサポートしています:

[mysqld]
wsrep_sst_method = xtrabackup-v2

利用可能なSST方法には以下が含まれます:

  • xtrabackup-v2 — PXC向けの推奨方法で、Percona XtraBackupを使用;ドナーを長時間ロックせずにSSTを実行します
  • clone — PXC 8.0.22+で利用可能で、MySQLの組み込みClone Pluginを使用;SSTのためのXtraBackup依存を除去します
  • mysqldump — 転送中にドナーをロックし遅い;本番環境では推奨されません
  • rsync — 転送中にドナーを読み取り専用に要求し、書き込みをブロック;ライブクラスタでも推奨されません

xtrabackup-v2方法は歴史的にデフォルトのアプローチであり、転送中にドナーノードを書き込み可能に保つため、既存のデプロイメントで広く使用されています。他のレガシーメソッドはドナーの書き込みをブロックする可能性があり、本番クラスタでは一般的に受け入れられません。PerconaはPXC 8.0.22以降の新規インストールに対して、SST層での外部ツール依存を除去するため、clone方法をますます推奨しています。

XtraBackupはどこにインストールする必要がありますか?

xtrabackup-v2を使用したSSTがトリガーされると、ドナーとジョイナーの両方にXtraBackupがインストールされ、アクセス可能である必要があります。両方が関与する理由は以下の通りです:

``````html
  • donor(提供元)は XtraBackup を実行してスナップショットデータをアウトバウンドでストリーミングします
  • joiner(参加元)は XtraBackup — 具体的には xbstream および xbcrypt ユーティリティ — を実行して、そのストリーミングされたデータを受信し適用します

joiner ノードに XtraBackup がインストールされていない場合、xtrabackup-v2 を使用してクラスタに追加しようとすると、SST が失敗します。joiner のエラーログには通常、次のような内容が表示されます:

[ERROR] WSREP: Failed to read 'ready <addr>' from: wsrep_sst_xtrabackup-v2
...
wsrep_sst_xtrabackup-v2: line 522: xbstream: command not found
[ERROR] WSREP: SST failed: 2 (No such file or directory)

これは明確で曖昧さのない失敗モードです。xbstream が joiner に存在しない場合、SST は完了しません。

参加元になることのないノードはどうでしょうか?

技術的には、ノードが常に donor(提供元)として動作し、ゼロからクラスタに再参加する必要がない場合、donor としての機能のみで XtraBackup が必要だと主張できます。しかし実際には、クラッシュ後、計画メンテナンス後、またはネットワークパーティションからの回復後、どのノードでも joiner になる可能性があります。ノードが SST を受信する必要が決してないことを確実に保証する方法はありません。

ここでの実際的な指針はシンプルです:PXC ノードすべてに例外なく XtraBackup をインストールしてください。 インストールのオーバーヘッドは無視できるほど小さく、計画外障害時の SST 失敗のコストはそうではありません。

Clone プラグインの代替手段(PXC 8.0.22+)

PXC 8.0.22 以降、Percona は MySQL Clone Plugin を SST 方式としてサポートするようになりました。これは SST 目的での XtraBackup 依存関係を完全に排除するため、知っておく価値があります:

[mysqld]
wsrep_sst_method = clone

clone 方式を使用する場合、すべてのノードで Clone Plugin をロードする必要があります:

INSTALL PLUGIN clone SONAME 'mysql_clone.so';
SHOW PLUGINS WHERE Name = 'clone';
+-------+--------+-------+----------------+---------+
| Name  | Status | Type  | Library        | License |
+-------+--------+-------+----------------+---------+
| clone | ACTIVE | CLONE | mysql_clone.so | GPL     |
+-------+--------+-------+----------------+---------+

clone 方式は XtraBackup を SST 依存関係とせずに標準化するための堅実な選択肢です。ただし、選択する SST 方式に関わらず、XtraBackup は外部バックアップ戦略において依然として実用的な価値があります。SST はクラスタ同期メカニズムであり、バックアップではなく、決してバックアップとして扱ってはなりません。

現在の SST 設定の確認

現在の SST 方式と Galera 関連の設定を確認するには:

SHOW VARIABLES LIKE 'wsrep_sst_method';
+------------------+---------------+
| Variable_name    | Value         |
+------------------+---------------+
| wsrep_sst_method | xtrabackup-v2 |
+------------------+---------------+

クラスタ状態を確認し、どのノードが donor(提供元)として動作しているかを確認するには:

SHOW STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name             | Value  |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+
SHOW STATUS LIKE 'wsrep_connected';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| wsrep_connected | ON    |
+-----------------+-------+
SHOW STATUS LIKE 'wsrep_cluster_size';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+

実際の観察事項

PXC 環境を直接扱った経験から注目すべきいくつかの点:

``````html
  • XtraBackup のバージョンは PXC バージョンと一致する必要があります。XtraBackup 2.x を PXC 8.0 で使用すると SST が失敗します。PXC 8.0 には Percona XtraBackup 8.0 を使用し、アップグレード後はバージョン一致を確認してください。
  • <clone SST メソッドに切り替えても、スケジュールされたバックアップのために XtraBackup をインストールしたままにしてください。バックアップ戦略と SST メソッドは別々の懸念事項であり、それぞれ独立して扱うべきです。
  • <wsrep_sst_donor 変数は、好みのドナーノードを指定でき、最も忙しいかレイテンシに敏感なメンバーに SST を向けないようにするのに便利です。
  • PXC とともに Percona Toolkit を実行している場合、特定の PXC バージョンでの DDL レプリケーションの動作に注意してください — Total Order Isolation (TOI) と Rolling Schema Upgrade (RSU) の動作が異なり、本番環境でスキーマ変更を実行する前に専用に確認する価値があります。

まとめ

質問に直接答えると:<xtrabackup-v2 を SST メソッドとして使用している場合 — これは多くの既存の PXC デプロイメントでデフォルトのままです — はい、すべてのクラスターメンバーに XtraBackup をインストールする必要があります。状況によっては任意のノードがドナーまたはジョイナーとなり、このメソッドを使用する際は両方の役割で XtraBackup が必要です。

PXC 8.0.22 以降を使用しており、SST レイヤーでの依存関係を排除したい場合、Clone Plugin メソッドは実行可能な代替手段であり、新規デプロイメントに対する Percona の推奨選択肢となっています。新規インストールの場合、PXC 8.4 LTS が現在の長期サポートリリースであり、新規インストールの推奨ターゲットです。SST に clone を使用していても、実際のバックアップジョブには XtraBackup が最適なツールです。

選択したノードで XtraBackup をスキップして数メガバイトのディスク容量を節約しようとしないでください。その決定が最終的に引き起こす SST 失敗は、犠牲にする価値のないトレードオフです。

リソース

2026年2月22日日曜日

AI ワークロードのための MySQL + Neo4j:なぜリレーショナルデータベースが今なお重要か

This article was originally published in English at AnotherMySQLDBA.

AIエージェント用の永続メモリを構築する方法を、既にお使いのデータベースを使って文書化する時期が来たと考えました。ベクトルデータベースではありません - MySQLとNeo4jです。

これは理論ではありません。私はこのアーキテクチャを日常的に使用し、複数のプロジェクトでAIエージェントのメモリを管理しています。実際に機能するスキーマとクエリパターンを紹介します。

アーキテクチャ

AIエージェントには2種類のメモリが必要です:

  • 構造化メモリ - 何が起こったか、いつ、なぜ(MySQL)
  • パターンメモリ - 何が何に繋がっているか(Neo4j)

ベクトルデータベースは類似性検索用です。ワークフローの状態や決定履歴を追跡するには適していません。それにはACIDトランザクションと適切なリレーションシップが必要です。

MySQLスキーマ

AIエージェントの永続メモリのための実際のスキーマはこちらです:

-- Architecture decisions the AI made
CREATE TABLE architecture_decisions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    decision TEXT NOT NULL,
    rationale TEXT,
    alternatives_considered TEXT,
    status ENUM('accepted', 'rejected', 'pending') DEFAULT 'accepted',
    decided_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    tags JSON,
    INDEX idx_project_date (project_id, decided_at),
    INDEX idx_status (status)
) ENGINE=InnoDB;

-- Code patterns the AI learned
CREATE TABLE code_patterns (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    code_example TEXT,
    language VARCHAR(50),
    confidence_score FLOAT DEFAULT 0.5,
    usage_count INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_project_category (project_id, category),
    INDEX idx_confidence (confidence_score)
) ENGINE=InnoDB;

-- Work session tracking
CREATE TABLE work_sessions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(255) UNIQUE NOT NULL,
    project_id INT NOT NULL,
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    ended_at DATETIME,
    summary TEXT,
    context JSON,
    INDEX idx_project_session (project_id, started_at)
) ENGINE=InnoDB;

-- Pitfalls to avoid (learned from mistakes)
CREATE TABLE pitfalls (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    how_to_avoid TEXT,
    severity ENUM('critical', 'high', 'medium', 'low'),
    encountered_count INT DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_project_severity (project_id, severity)
) ENGINE=InnoDB;

外部キー。チェック制約。適切なインデックス。これはリレーショナルデータベースが得意とするところです。

クエリパターン

AIエージェントのメモリを実際にクエリする方法はこちらです:

-- Get recent decisions for context
SELECT title, decision, rationale, decided_at
FROM architecture_decisions
WHERE project_id = ?
  AND decided_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY decided_at DESC
LIMIT 10;

-- Find high-confidence patterns
SELECT category, name, description, code_example
FROM code_patterns
WHERE project_id = ?
  AND confidence_score >= 0.80
ORDER BY usage_count DESC, confidence_score DESC
LIMIT 20;

-- Check for known pitfalls before implementing
SELECT title, description, how_to_avoid
FROM pitfalls
WHERE project_id = ?
  AND category = ?
  AND severity IN ('critical', 'high')
ORDER BY encountered_count DESC;

-- Track session context across interactions
SELECT context
FROM work_sessions
WHERE session_id = ?
ORDER BY started_at DESC
LIMIT 1;

これらはストレートフォワードなSQLクエリです。EXPLAINで期待通りのインデックス使用が確認できます。サプライズはありません。

Neo4jレイヤー

MySQLが構造化データを扱い、Neo4jがリレーションシップを扱います:

// Create nodes for decisions
CREATE (d:Decision {
  id: 'dec_123',
  title: 'Use FastAPI',
  project_id: 1,
  embedding: [0.23, -0.45, ...]  // Vector for similarity
})

// Create relationships
CREATE (d1:Decision {id: 'dec_123', title: 'Use FastAPI'})
CREATE (d2:Decision {id: 'dec_45', title: 'Used Flask before'})
CREATE (d1)-[:SIMILAR_TO {score: 0.85}]->(d2)
CREATE (d1)-[:CONTRADICTS]->(d3:Decision {title: 'Avoid frameworks'})

// Query: Find similar past decisions
MATCH (current:Decision {id: $decision_id})
MATCH (current)-[r:SIMILAR_TO]-(similar:Decision)
WHERE r.score > 0.80
RETURN similar.title, r.score
ORDER BY r.score DESC

// Query: What outcomes followed this pattern?
MATCH (d:Decision)-[:LEADS_TO]->(o:Outcome)
WHERE d.title CONTAINS 'Redis'
RETURN d.title, o.type, o.success_rate

それらが連携して動作する仕組み

フローは以下のようになります:

  1. AIエージェントがコンテンツを生成するか決定を下す
  2. MySQLに構造化データを保存(何、いつ、なぜ、完全なコンテキスト)
  3. エンベディングを生成し、類似アイテムとのリレーションシップ付きでNeo4jに保存
  4. 次のセッション:Neo4jが関連する類似決定を見つけ出す
  5. MySQLがその決定の詳細情報を提供

MySQLが真実の源です。Neo4jがパターン発見者です。

なぜベクトルデータベースだけではダメなのか?

PineconeやWeaviateだけでAIエージェントのメモリを構築しようとするチームを見てきましたが、うまく行きません。理由は:

ベクトルDBが得意なこと:

  • クエリに似たドキュメントを見つける
  • セマンティック検索(RAG)
  • "これに似たもの"

ベクトルDBが苦手なこと:

  • "3月15日に何を決定したか?"
  • "障害を引き起こした決定を表示"
  • "このワークフローの現在の状態は?"
  • "confidence > 0.8 AND usage_count > 10のパターンはどれ?"

これらのクエリには構造化フィルタリング、結合、トランザクションが必要です。それはリレーショナルデータベースの領域です。

MCPと未来

Model Context Protocol (MCP)は、AIシステムがコンテキストを扱う方法を標準化しています。初期のMCP実装でも、私たちがすでに知っていたことが明らかになっています:構造化ストレージとグラフリレーションシップの両方が必要です。

``````html

MySQL は MCP の「resources」と「tools」カタログを扱います。Neo4j はコンテキスト項目間の「relationships」を扱います。ベクター埋め込みはパズルの一部に過ぎません。

Production Notes

このアーキテクチャを実行している現在のシステム:

  • MySQL 8.0, 48 tables, ~2GB data
  • Neo4j Community, ~50k nodes, ~200k relationships
  • Query latency: MySQL <10ms, Neo4j <50ms
  • Backup: Standard mysqldump + neo4j-admin dump
  • Monitoring: Same Percona tools I've used for years

運用上の複雑さは低いです。これらは成熟したデータベースで、よく理解された運用パターンを持っています。

When to Use What

Use CaseDatabase
Workflow state, decisions, audit trailMySQL/PostgreSQL
Pattern detection, similarity, relationshipsNeo4j
Semantic document search (RAG)Vector DB (optional)

状態には MySQL から始めましょう。パターン認識が必要になったら Neo4j を追加します。セマンティックドキュメント検索を実際にしている場合にのみ、ベクターデータベースを追加してください。

Summary

AI エージェントには永続的なメモリが必要です。単なるベクターデータベースの埋め込みではなく、構造化された、リレーショナルで、時間的なメモリにパターン認識を備えたものです。

MySQL は構造化された状態を扱います。Neo4j はグラフリレーションシップを扱います。これらを組み合わせることで、ベクターデータベース単独では提供できないものを提供します。

AI ワークロードのためにリレーショナルデータベースを放棄しないでください。それぞれの仕事に適したツールを使用し、それは両方を一緒に使うことです。

このアーキテクチャに関する AI エージェントの視点の詳細については、3k1o の関連投稿をご覧ください。

MySQL 8.0 JSON 関数:実践的な例とインデクシング

This article was originally published in English at AnotherMySQLDBA.

この投稿では、MySQL 8.0 の JSON 関数のハンズオン解説を扱います。JSON サポートは MySQL 5.7 から存在しますが、8.0 では重要な改善が追加されました — より優れたインデックス戦略、新しい関数、マルチバリューインデックス — これにより JSON データの取り扱いが大幅に実用的になりました。以下の内容では、最も一般的に必要なパターンのいくつかを文書化し、EXPLAIN の出力と知っておくべきパフォーマンス観察を含めています。

これは「JSON vs. リレーショナル」の議論投稿ではありません。MySQL に JSON を保存している場合、すでに理由をお持ちでしょう。ここでの目標は、利用可能なツールを効果的に使用していることを確認することです。

環境

mysql> SELECT @@version, @@version_comment\G
*************************** 1. row ***************************
        @@version: 8.0.36
@@version_comment: MySQL Community Server - GPL

テストは 8GB RAM の VM で行われ、innodb_buffer_pool_size を 4G に設定しました。言及する価値のあるメンテナンス上の注意点として:query_cache_type は 8.0 では無関係です。なぜなら query cache が完全に削除されたからです。5.7 インスタンスから移行し、my.cnf にその変数が残っている場合、削除してください — MySQL 8.0 は起動エラーを投げます。

テストテーブルの設定

テストテーブルは、かなり一般的なパターンをシミュレートしています — アプリケーションがユーザー profile データとイベントメタデータを JSON blob として保存するものです:

CREATE TABLE user_events (
  id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id     INT UNSIGNED NOT NULL,
  event_data  JSON NOT NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  INDEX idx_user (user_id)
) ENGINE=InnoDB;

INSERT INTO user_events (user_id, event_data) VALUES
(1, '{"action":"login","ip":"192.168.1.10","tags":["mobile","vpn"],"score":88}'),
(1, '{"action":"purchase","ip":"192.168.1.10","tags":["desktop"],"score":72,"amount":49.99}'),
(2, '{"action":"login","ip":"10.0.0.5","tags":["mobile"],"score":91}'),
(3, '{"action":"logout","ip":"10.0.0.9","tags":["desktop","vpn"],"score":65}'),
(2, '{"action":"purchase","ip":"10.0.0.5","tags":["mobile"],"score":84,"amount":129.00}');

基本的な抽出:JSON_VALUE vs. JSON_EXTRACT

JSON_VALUE() は MySQL 8.0.21 で導入され、スカラー値を抽出するよりクリーンな方法で、組み込みの型キャストが可能です。それ以前は JSON_EXTRACT()(または -> 省略形)を使用し、手動でキャストする必要があり、動作はしますがクエリにノイズを追加します。

-- Pre-8.0.21 approach
SELECT user_id,
       JSON_EXTRACT(event_data, '$.action') AS action,
       CAST(JSON_EXTRACT(event_data, '$.score') AS UNSIGNED) AS score
FROM user_events;

-- Cleaner 8.0.21+ approach
SELECT user_id,
       JSON_VALUE(event_data, '$.action') AS action,
       JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) AS score
FROM user_events;

2 番目のクエリの出力:

+---------+----------+-------+
| user_id | action   | score |
+---------+----------+-------+
|       1 | login    |    88 |
|       1 | purchase |    72 |
|       2 | login    |    91 |
|       3 | logout   |    65 |
|       2 | purchase |    84 |
+---------+----------+-------+
5 rows in set (0.00 sec)

RETURNING 句は本当に有用です。煩わしいダブルキャストパターンを排除し、後でクエリコードを読む際に意図を明確にします。

マルチバリューインデックス:真のゲームチェンジャー

ここが 8.0 で JSON ワークロードの本当の進化点です。MySQL 8.0.17 以降利用可能なマルチバリューインデックスにより、JSON カラム内の配列要素を直接インデックス付けできます。実際の動作は以下のようになります:

ALTER TABLE user_events
  ADD INDEX idx_tags ((CAST(event_data->'$.tags' AS CHAR(64) ARRAY)));

タグ値でフィルタリングするクエリに対する、EXPLAIN の前後を示します:

-- Without the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data->'$.tags', '"vpn"')\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 100.00
        Extra: Using where

-- After adding the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data->'$.tags', '"vpn"')\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: range
possible_keys: idx_tags
          key: idx_tags
      key_len: 67
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using where

フルテーブルスキャンからレンジスキャンへ。5 行では些細なことですが、数百万行のテーブルで頻繁なタグベースのフィルタリングを行う場合、その差は顕著です。改善効果はテーブルサイズとクエリ頻度に比例してスケールします。

重要な注意点として:MEMBER OF()JSON_OVERLAPS() もマルチバリューインデックスから恩恵を受けますが、JSON_SEARCH() は受けません。設計時にクエリパターンを選択する際に重要です:

-- This WILL use the multi-valued index:
SELECT * FROM user_events
WHERE 'vpn' MEMBER OF (event_data->'$.tags');

-- This will NOT use it:
SELECT * FROM user_events
WHERE JSON_SEARCH(event_data->'$.tags', 'one', 'vpn') IS NOT NULL;

JSON の集計と変換

よく知っておくべきいくつかの集計関数:

``````html
-- Build a JSON array of actions per user
SELECT user_id,
       JSON_ARRAYAGG(JSON_VALUE(event_data, '$.action')) AS actions
FROM user_events
GROUP BY user_id;

+---------+----------------------+
| user_id | actions              |
+---------+----------------------+
|       1 | ["login","purchase"] |
|       2 | ["login","purchase"] |
|       3 | ["logout"]           |
+---------+----------------------+
3 rows in set (0.01 sec)

-- Summarize into a JSON object keyed by action
SELECT user_id,
       JSON_OBJECTAGG(
         JSON_VALUE(event_data, '$.action'),
         JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)
       ) AS score_by_action
FROM user_events
GROUP BY user_id;

+---------+--------------------------------+
| user_id | score_by_action                |
+---------+--------------------------------+
|       1 | {"login": 88, "purchase": 72}  |
|       2 | {"login": 91, "purchase": 84}  |
|       3 | {"logout": 65}                 |
+---------+--------------------------------+
3 rows in set (0.00 sec)

JSON_OBJECTAGG() は、グループ内に重複するキーが存在する場合にエラーをスローします。これは、本番の ETL パイプラインで遭遇する前に知っておく価値があります。その場合、上流で重複を除去するか、データがこの集計ステップに到達する前にアプリケーション logic で処理する必要があります。

JSON を多用したクエリ実行後の SHOW STATUS の確認

クエリパターンを評価する際、ハンドラーメトリクスの確認は有用な習慣です:

FLUSH STATUS;

SELECT * FROM user_events
WHERE JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) > 80;

SHOW STATUS LIKE 'Handler_read%';

+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Handler_read_first         | 1     |
| Handler_read_key           | 0     |
| Handler_read_last          | 0     |
| Handler_read_next          | 4     |
| Handler_read_prev          | 0     |
| Handler_read_rnd           | 0     |
| Handler_read_rnd_next      | 6     |
+----------------------------+-------+
7 rows in set (0.00 sec)

Handler_read_rnd_next の値はフルスキャンを確認しています — score 値に機能的インデックスがないため驚くことではありません。スケールでの score ベースのフィルタリングには、インデックス付きの生成カラムが正しい解決策です:

ALTER TABLE user_events
  ADD COLUMN score_val TINYINT UNSIGNED
    GENERATED ALWAYS AS (JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)) VIRTUAL,
  ADD INDEX idx_score (score_val);

これを追加した後、同じクエリは適切なインデックス範囲スキャンに低下します。JSON フィールド上の生成カラムは MySQL 8.0 と Percona Server 8.0 の両方で利用可能であり、いかなる意味のあるスケールでもスカラー JSON フィールドのフィルタリングに対して最も信頼性の高い方法です。

Percona Server を使用している場合、Percona Toolkitpt-query-digest が、本番環境で実際に問題を引き起こしている JSON を多用したクエリを特定するための最も実践的な方法であり、推測的にインデックスを追加する前に使用することをお勧めします。

実践的な観察

  • マルチバリューインデックス (8.0.17+) は長らく待ち望まれた改善であり、クエリパターンが JSON_CONTAINS() または MEMBER OF() と一致する場合にうまく機能します
  • JSON_VALUE() with RETURNING (8.0.21+) は、古い抽出後のキャストパターンよりもクリーンで、一貫して採用する価値があります
  • 生成カラムとインデックスは、スケールでのスカラー JSON フィールドフィルタリングに対して最も信頼性の高い方法です
  • グループ化されたデータでの JSON_OBJECTAGG() の重複キーエラーを監視してください — ETL パイプラインでハードエラーとして表面化し、サンプルデータがたまたまクリーンであればテストで簡単に逃すことがあります
  • 常に EXPLAIN でインデックスの使用を確認してください — オプティマイザは複雑な WHERE 句でマルチバリューインデックスを常に選択するわけではなく、仮定するよりも確認する価値があります

まとめ

MySQL 8.0 の JSON 改善は本当有用であり、特にマルチバリューインデックスと型キャスト付きの JSON_VALUE() がそうです。これらは良好なスキーマ設計に代わるものではありませんが、JSON ストレージが適切または継承された場合、オプティマイザが勝手に解決してくれることを願うのではなく、本物のツールが利用可能になりました。特に、特定の JSON フィールドが WHERE 句で頻繁に使用されることがわかっている場合は、生成カラムパターンを早い段階で評価する価値があります。

有用な参考資料: