セカンダリインデックスの基本原理
HBaseは辞書順にソートされた行キーに基づく主索引のみをサポートしているため、特定のフィルタ条件によるデータ抽出時はフルスキャンやクライアントサイドフィルタリングが発生しがちです。Apache Phoenixが提供するセカンダリインデックス機能は、このフルテーブルスキャンに起因する性能ボトルネックを解消し、ポイントロックアップを含む各種クエリのレスポンス時間を劇的に改善します。本稿では、Phoenixで利用可能な索引の種類、適用条件、環境設定要件、および運用時のパフォーマンスチューニング手法について詳述します。
カバリングインデックス (Covering Index)
カバリングインデックスは、インデックス表の中にSELECT句およびWHERE句で指定した列の値をすべて格納する構成です。これにより、HBase本体のデータリージョンへアクセスすることなく、インデックス表のみから完結してクエリを実行可能となります(Index-Only Scan)。対象とする検索列をインデックス定義時にINCLUDE節で明示する必要があります。
-- 元データテーブル: DATA_STORE_MAIN
-- 行キー: ROW_ID
-- カラム: ACCOUNT_UUID, QUERY_TOKEN, TARGET_URI
-- クエリ対象列(QUERY_TOKEN)をインデックスに含める定義
CREATE INDEX IDX_STORE_COV_ACC ON DATA_STORE_MAIN(ACCOUNT_UUID) INCLUDE(QUERY_TOKEN);
上記インデックスが設定されている場合、アカウントUUIDを指定してクエリトークンを取得するクエリはインデックス表への直接読取りのみで完了します。EXPLAINコマンドで実行計画を確認すると、FULL SCANではなく特定レンジのスキャン(INDEX RANGE SCAN)が実行されていることが確認でき、測定環境では数ミリ秒台の応答遅延を実現しています。なお、SELECT句に含まれるすべての列がインデックス範囲内で確保されていない場合は、このインデックスは使用されません。
関数インデックス (Functional Index)
Phoenix 4.3以降から導入された関数インデックスは、単一のカラムだけでなく任意の式や関数をインデックスキーとして登録できる機能です。アプリケーション側で文字列の大文字変換や連結処理を行った後に照合する場合、その演算ロジック自体をインデックスに事前計算させて保管することで、実行時の計算コストとインデックスヒット率の向上を図れます。
-- 顧客IDとページURIを連結し大文字化して格納
CREATE INDEX IDX_STORE_FUN_UPR ON DATA_STORE_MAIN(UPPER(ACCOUNT_UUID || '|' || TARGET_URI));
-- 関数インデックスと一致する形式で参照
SELECT TARGET_URI
FROM DATA_STORE_MAIN
WHERE UPPER(ACCOUNT_UUID || '|' || TARGET_URI) = 'A1B2C3D4-TEST-HASH';
関数インデックスを定義した後、該当する関数表現を持つWHERE節を用いた検索は自動的にインデックスを使用します。複雑な正規表現やカスタム関数よりも、標準SQL関数を使用したパターンマッチングに適しています。
グローバルインデックス (Global Index)
グローバルインデックスは「読み取り頻度が高く、書き込み頻度が低い」ワークロードに最適化されています。ただし、UPSERT(INSERT/UPDATE)、DELETE、UPSERT SELECTなどの書き込み操作が発生すると、対応するインデックス表全体へ分散更新処理が走るため、書き込みレイテンシが増加する傾向があります。インデックス表は主テーブルとは独立した別のHBaseテーブルとして維持されます。
必須環境設定
RegionServer側のhbase-site.xmlに以下のプロパティ追加が必要です。
<property>
<name>hbase.regionserver.wal.codec</name>
<value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>
インデックス作成と利用可否
CREATE INDEX IDX_STORE_GLB_ACC ON DATA_STORE_MAIN(ACCOUNT_UUID);
- インデックスを使用するケース: 検索条件および選択列がインデックスキーと同じカラム、または行キー(ROWKEY)のみの場合。
- インデックスがスキップされるケース: インデックス対象外のカラム(例: QUERY_TOKEN)を同時にSELECTした場合。インデックスからは必要なデータが取れないため、オプティマイザーはフルスキャンを選択します。
インデックス対象外のカラムも同時に取得したい場合、以下の3つのアプローチがあります。
- カバリングインデックスへ変更: INCLUDE節を追加して必要な列をインデックスに統合する。
- 強制利用ヒント: クエリ直前にhintを付与してオプティマイザーの判断を上書きする。ただし、選択列がインデックスに含まれていない場合は依然としてフルスキャンが発生するため、データ分布が偏っており条件マッチ件数が極めて少ない場合にのみ有効です。
SELECT /*+ INDEX(DATA_STORE_MAIN, IDX_STORE_GLB_ACC) */ QUERY_TOKEN
FROM DATA_STORE_MAIN
WHERE ACCOUNT_UUID = 'd8e7f6...';
ローカルインデックス (Local Index)
ローカルインデックスは「書き込み負荷が高く、ストレージ制約がある」環境向けに設計されています。グローバルインデックスとの決定的な違いは、インデックスデータが関連づけられたデータと同じリージョンサーバー上に共存する点です。これにより、書き込み時のネットワークラウンドトリップが排除され、高い書き込みスループットを維持できます。一方で、すべてのローカルインデックスは内部で共有テーブル(SYSTEM.INDEXのようなシステム管理テーブルとは別に、物理的な一元管理表)として扱われるため、異なるリージョンに跨るデータ取得時には領域横断的なチェックが入り、読み取りレイテンシがやや上昇する可能性があります。また、Phoenix 4.3以降ではメインリージョンのマージ操作に合わせてインデックス側も自動的にコンパクト化されます。
必須環境設定
マスターノードおよびリージョンサーバーに対して以下の設定を適用します。
<!-- マスターノード (hbase-site.xml) -->
<property>
<name>hbase.master.loadbalancer.class</name>
<value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancer</value>
</property>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.phoenix.hbase.index.master.IndexMasterObserver</value>
</property>
<!-- リージョンサーバー (hbase-site.xml) -->
<property>
<name>hbase.coprocessor.regionserver.classes</name>
<value>org.apache.hadoop.hbase.regionserver.LocalIndexMerger</value>
</property>
作成と動作確認
CREATE LOCAL INDEX IDX_STORE_LCL_QRY ON DATA_STORE_MAIN(QUERY_TOKEN);
SELECT TARGET_URI
FROM DATA_STORE_MAIN
WHERE QUERY_TOKEN = 'phoenix_tuning';
主テーブルのリージョン配置に関係なくインデックスの更新が同期されるため、ハイブリッドワークロードでも安定したパフォーマンスが発揮されます。
インデックスの管理と運用最適化
不要になった索引の削除は通常のリレーションと同じ構文で行えます。
DROP INDEX IDX_STORE_LCL_QRY ON DATA_STORE_MAIN;
親テーブルからカラムがdropされると、それに関連するインデックスは自動検出・削除されます。同様に、カバリングインデックスが保有していたINCUDE対象カラムが親テーブルから削除されると、カバリング構造内から自動的に除外されて整合性が保たれます。
インデックス更新スレッドのチューニング
書き込み負荷が高い環境において、インデックス表への反映処理を追従させるためには、プロセス間のスレッドプールサイズ調整が効果的です。以下のパラメータは各HBaseノードのhbase-site.xmlで設定してください。
- index.builder.threads.max (既定: 10): 主テーブルの更新イベントをインデックス更新タスクへ割り当てるワーカー数の上限。
- index.builder.threads.keepalivetime (既定: 60): アイドル状態のビルダー线程が維持される秒数。
- index.write.threads.max (既定: 10): 複数のインデックス表へ並列書き込みを行うスレッド数。インデックス表の総数と同等またはそれに準拠させるのが理想的です。
- index.write.threads.keepalivetime (既定: 60): 書き込みスレッドの生存時間。
- hbase.htable.threads.max (既定: 2147483647): 個別インデックス表に対する同時書き込み許可数。インデックス更新の並列度を高めたい場合、増加させます。メモリ消費に留意してください。
- hbase.htable.threads.keepalivetime (既定: 60): 個別インデックス更新用スレッドのタイムアウト。
- index.tablefactory.cache.size (既定: 10): ヒープメモリにキャッシュされるHTableインスタンスの数。増やすとTable生成コストを削減できますが、RAM使用量が増加します。