LASSIC Media らしくメディア
カーディナリティとは|値のばらつきと索引設計
開発会社から「この列にインデックスを追加しても、あまり速くなりません」と説明を受けて、なぜそうなるのか掴みにくいと感じる発注担当者もいるでしょう。同じ「索引を張る」という作業でも、対象になる列によって効果には大きな差が出ます。その差を生む代表的な指標が、カーディナリティです。
この記事では、発注者やプロジェクトマネージャーに向けて、カーディナリティとは何か、なぜ索引設計や実行計画に関わるのかを整理します。索引の内部構造であるB木の仕組みや、正規化・非正規化の設計判断そのものは、それぞれ別記事で扱うテーマです。本稿は、列の値のばらつきという指標が索引の効果や実行計画にどう影響するか、という観点に絞って解説します。
この記事のポイント
- カーディナリティとは、列に格納された値の種類がどれだけ多いかを表す指標です。
- 値のばらつきが大きい列ほど索引で行を絞り込みやすく、種類が少ない列は絞り込みにくくなります。
- データベースは統計情報からカーディナリティを見積もり、索引を使うか全件走査するかを実行計画で判断します。
カーディナリティとは
カーディナリティとは、ある列に格納された値の種類がどれだけ多いか(値のばらつきの度合い)を表す指標で、データベースの索引設計の判断材料になります。
Wikipedia英語版の説明では、ユーザーIDのように行ごとにほぼ一意な値が入る列を「高カーディナリティ」と呼びます。性別や会員区分のように値の種類が数個しかない列は「低カーディナリティ」に分類されるとされています*1。同じ説明の中では、姓名のように一部の値(多い姓)に集まる箇所がありつつも種類自体は多い列を「通常カーディナリティ」と呼ぶ例も挙げられています*1。この分類は、値がどれだけ散らばっているかという性質を、実務で扱いやすい形に整理したものと捉えるとよいでしょう。
会員テーブルを例に考えてみます。会員ID列は行数に近い数の値が並ぶため、高カーディナリティに当たります。一方、性別列や退会フラグのような列は、値の種類が数個に限られるため、低カーディナリティに分類されるのです。
本稿で扱うのは、この「値のばらつき」という指標が索引の効果や実行計画にどう関わるかという観点です。索引そのものの内部構造(B木の仕組み)や、テーブル設計における正規化・非正規化の判断は、それぞれ別記事のテーマとして扱い、本稿では取り上げません。
この「値のばらつき」を割合で捉える考え方として、選択性(セレクティビティ)という言葉も使われます。ある条件で絞り込んだときに、全体の何割の行が残るかを表す割合で、カーディナリティが高い列ほど選択性は小さくなり、絞り込みが利きやすい関係にあります。選択性は、次章で扱う実行計画の見積もりでも使われる考え方です。
都道府県コードのように、値の種類が数十程度にとどまる列は、会員IDほど高くも性別ほど低くもない、中間的なカーディナリティに位置づけられます。索引の効果は列ごとに濃淡があり、「索引を張れば一律に速くなる」とは言えない理由が、この中間層の存在にも表れているのです。
カーディナリティが問われるのは、検索条件に使う列だけではありません。テーブル同士を結びつけるJOIN(結合)の条件に使う列でも、値のばらつきは無視できない要素です。結合条件の列が高カーディナリティであれば、片方のテーブルの1行に対応する相手側の行数が少なく済みます。結合処理そのものの負荷を抑えやすくなるでしょう。逆に結合条件の列が低カーディナリティだと、1件に対して多数の行が対応する組み合わせが生じます。結合結果の件数が想定以上にふくらむことがあるのです。
なぜ索引設計に関わるのか
索引は、目的の行を素早く見つけるための道しるべです。値のばらつきが大きい列に索引を張ると、その道しるべをたどるだけで対象行をわずかな件数まで絞り込めます。逆に値の種類が数個しかない列では、索引をたどっても該当行が大量に残ってしまい、絞り込みの効果は薄くなりがちです。
会員テーブルで「会員ID = 12345」を検索する場合を考えてみましょう。会員ID列は高カーディナリティなので、索引をたどれば該当する行はほぼ1件に絞られます。一方「性別 = 女性」で検索すると、性別列は低カーディナリティのため、該当する行は全体の半分近くに及ぶこともあるでしょう。索引をたどっても大量の行を読み込む必要が生じ、全件走査とさほど変わらない負荷になる場合があるのです。
管理画面の検索フォームを設計する場面でも、この考え方は役立ちます。注文番号や会員IDのような高カーディナリティの列を条件に指定できる画面は、少数の行だけを読み込んで済むため、応答が安定しやすいでしょう。一方、承認ステータスのような低カーディナリティの列だけで絞り込む画面は、条件を指定しても大量の行が対象に残りやすいものです。他の条件と組み合わせる設計や、表示件数の制御をあわせて検討する必要が出てきます。
この違いを、次の表に整理しました。
| 観点 | 高カーディナリティ列 | 低カーディナリティ列 |
|---|---|---|
| 列の例 | 会員ID・メールアドレス | 性別・会員ステータス |
| 値の種類の目安 | 行数に近い件数 | 数個から十数個程度 |
| 単独索引の絞り込み | 少数の行に絞りやすい | 該当行が多く残りやすい |
| 複合索引での位置 | 前方に置くと絞り込みへの寄与が大きい | 単独では後方か他条件と組み合わせる |
具体的な数の感覚もつかんでおきましょう。たとえば100万件のテーブルを想定すると、カーディナリティが80万件に近い列では、条件を一つ指定するだけで対象行がわずかな件数に絞られます。一方、カーディナリティが2程度の列(真偽値など)では、条件を指定しても平均して行の半数近くが残る計算になり、索引をたどる手間に見合う絞り込みが得にくいのです。この差が、索引を「効果的な列」と「効果が薄い列」に分ける実務上の目安になっています。同じ100万件のテーブルでも、カーディナリティが10程度の中間的な列であれば、条件を一つ指定すると平均で10万件前後まで絞られる計算になります。極端な高低の間に位置づけられる、中間的な効果と捉えるとよいでしょう。
低カーディナリティの列に索引を張る判断が、そのまま誤りというわけではありません。値の分布に偏りがある場合や、他の列と組み合わせた複合索引の一部として使う場合には、低カーディナリティの列にも役割が生じるのです。「種類が少ない列には索引が要らない」と単純に割り切るのではなく、実際のクエリでどう使われるかをあわせて見る姿勢が求められます。
実行計画とカーディナリティの見積もり
データベースは、クエリを実行する前に「実行計画」を組み立てます。索引を使って絞り込むか、テーブル全体を読む全件走査にするかは、この実行計画の中でプランナ(オプティマイザ)が判断する事項です。判断材料の中心になるのが、列ごとの値の分布を記録した統計情報になります。
PostgreSQLでは、ANALYZE実行時に集めた統計情報をpg_statsビューで確認できます。列ごとの推定値の種類数を示すn_distinctという項目があります。正の値ならその列に含まれる値の種類の推定数を、負の値なら行数に対する割合を表すとされています*2。値が-1であれば、行数と同じ数の値が並ぶ一意な列と判断されていることになります*2。
MySQLでも考え方は同様です。SHOW INDEXコマンドで確認できるCardinality列は、索引に含まれる列の一意な値の推定数を表します。値が高いほど、そのインデックスが結合(JOIN)で使われる見込みが高くなる、と説明されているのです*3。カーディナリティは実測値ではなく統計に基づく推定値のため、テーブルの規模によっては正確な値と一致しない場合がある、という注記も添えられています*3。
プランナは、この統計情報をもとに「その条件で絞り込んだときに何行が残るか」を見積もり、索引を使う経路と全件走査する経路のどちらが軽いかを比較します*4。統計情報には、頻出する値とその出現割合をまとめた最頻値のリストや、値の分布を区分けしたヒストグラムも含まれます。これらは、見積もりの精度を高める材料として使われているのです*4。
ここで注意したいのが、統計情報の鮮度です。大量のデータ登録や削除の直後は、実際の値の分布と統計情報がずれることがあります。見積もりが実態からずれると、本来なら索引を使うべき場面で全件走査が選ばれるなど、想定より重い実行計画が選ばれることもあるのです。PostgreSQLもMySQLも、統計情報を更新するコマンド(ANALYZE、ANALYZE TABLE)を備えています*2*3。まとまったデータ変更のあとには、この更新を運用に組み込んでおくことが望ましいでしょう。
統計情報は、テーブル全体を毎回すべて読み直して作るのではなく、行の一部を抽出するサンプリングをもとに計算される場合があります。抽出した範囲に偏りがあると、実際の分布との差が生まれやすくなるのです。見積もりのずれは、結合(JOIN)の順序やアルゴリズムの選び方にも波及し、想定より処理時間が伸びる要因になり得ます。開発会社に実行計画のレビューを依頼する際は、EXPLAINの推定行数と実際の処理行数を突き合わせてもらうとよいでしょう。見積もりのずれを早期に見つける手がかりになります。とくに「以上」「以下」といった範囲検索を伴うクエリでは、ヒストグラムの精度が見積もりの正確さを左右しやすい点も覚えておきたいところです。
複数のテーブルを結合するクエリでは、どちらのテーブルから読み始めるか、どの結合アルゴリズムを使うかも、プランナがカーディナリティの見積もりをもとに選びます。見積もりが実態に近いほど、無駄の少ない結合順序が選ばれやすくなり、逆に見積もりが大きく外れると、非効率な組み合わせが選ばれて処理時間が伸びる要因になるのです。統計情報の鮮度を保つことは、単独のクエリだけでなく、複数テーブルにまたがる処理の効率にも関わっています。
複合索引での並び順
複数の列をまとめた複合索引では、列を並べる順序が絞り込みの効果を左右します。基本の考え方は、カーディナリティが高く絞り込みへの寄与が大きい列を前方に置き、低カーディナリティの列を後方に添える、というものです。
「会員ID」と「性別」の2列で複合索引を組む場合を考えてみます。会員IDを前方に置けば、索引をたどった時点でほぼ1件まで絞り込まれ、性別の条件はその後の確認に使われる程度で済みます。逆に性別を前方に置くと、索引をたどっても該当する行が大量に残った状態からしか絞り込みが始まらず、索引を張った意味が薄れてしまうのです。
もっとも、この並び順は「カーディナリティの高い列をつねに先頭にすればよい」という単純な話でもありません。実際に発行されるクエリの条件(WHERE句でどの列を組み合わせて使うか、範囲検索に使う列はどれか)とあわせて決める必要があります。カーディナリティは、あくまで判断材料の一つと位置づけるのが実務的な進め方でしょう。
複合索引の設計では、カーディナリティの高さだけでなく、索引全体の大きさや保守コストとのバランスも見ておく必要があります。列を増やすほど索引のデータ量は膨らみ、行を追加・更新するたびの書き込み負荷も上がっていくためです。絞り込みへの寄与が小さい列まで欲張って組み込むのではなく、実際のクエリで使われる組み合わせに絞って設計する姿勢が、運用後の負担を抑える近道になるでしょう。開発会社に索引設計のたたき台を出してもらう際は、想定するクエリごとに、どの索引が使われる見込みかをあわせて示してもらうと、過不足の判断がしやすくなります。
なお、「カーディナリティ」という言葉は、監視やログ分析の領域でも使われます。この場合は、メトリクスに付与するラベルの組み合わせ数が増え続ける状態を指すことが多いのです。ラベルの種類が増えるほど、保存コストや検索性能への負担が増していく、という文脈で語られるのです。索引設計における列のカーディナリティとは指し示す対象が異なるため、会話の中でどちらの意味で使われているかを見極めておく必要があります。たとえば監視基盤にユーザーIDをそのままラベルとして付与すると、ラベルの組み合わせが利用者数に比例して増え続け、監視システム側の負荷が高まる要因になり得ます。索引設計とは別の文脈の話題ですが、「値の種類が増えるほど扱いが重くなる」という感覚そのものは共通していると捉えてよいでしょう。
発注・レビューで押さえる点
カーディナリティの仕組みが分かると、発注者やプロジェクトマネージャーが開発会社との会話で確認すべき点も具体的になります。ここでは3つの観点を整理します。
想定するデータ分布を共有する
索引設計は、対象の列にどんな値がどれくらいの種類・比率で入るかによって、適切な形が変わってきます。会員ステータスのように種類が数個の列でも、特定の値に利用者が集まる見込みがあるなら、その分布を開発会社に伝えておくとよいでしょう。複合索引の列順や絞り込み条件の設計に、その情報が反映されやすくなります。
とくに、受注管理や在庫管理のように、特定のステータス(処理待ち・保留中など)に件数が偏りやすい業務では、平常時と繁忙期とで分布が変わることも珍しくありません。ピーク時を想定した分布も含めて共有しておくと、設計段階での見落としを減らしやすくなるでしょう。
リリース直後はテーブルの行数が少なく、カーディナリティが低い列でも索引の有無による差を体感しにくいものです。運用が進んでデータが積み上がってから、分布の偏りが表面化する場合もあります。初期の想定だけで設計を固定せず、増加ペースの見込みもあわせて共有しておくと、後々の見直しがしやすくなるでしょう。
統計情報の更新運用を確認する
統計情報が古いままだと、実行計画の見積もりが実態からずれ、想定より重い経路が選ばれることがあります。大量のデータ登録・削除が発生する処理のあとに統計情報を更新する運用(ANALYZEの実行タイミングなど)が組み込まれているかを確認しましょう。レビューの段階で押さえておきたいところです。
とくに、夜間バッチや月次の一括登録のように、まとまった件数のデータが一度に動く処理がある場合は注意が必要です。その直後に統計情報を更新するタイミングを運用手順に組み込んでおくと、判断のよりどころになります。更新のタイミングが抜け落ちたまま長期間運用されている案件は、少なくないため、既存システムを引き継ぐ場合は現状の運用を確認しておくとよいでしょう。
EXPLAINで実行計画の確認を依頼する
PostgreSQLやMySQLでは、EXPLAINコマンドでクエリが索引を使っているか、何行を見積もっているかを確認できます。想定していた索引が実際の実行計画で使われているかどうかを、開発会社に確認してもらうよう依頼すると、設計と実装のずれを早い段階で見つけられるはずです。想定と異なる実行計画が選ばれている場合は、索引の列順や統計情報の更新運用を見直す余地があると考えてよいでしょう。
レビューのたびに口頭でやり取りするのではなく、確認事項を一覧にしておくと、担当者が変わっても同じ観点で点検を続けられます。ここまでの内容を、次の表にまとめました。
| 確認項目 | 目的 |
|---|---|
| 想定データ分布・増加ペースの共有 | 列ごとのカーディナリティを踏まえた索引設計の判断材料にする |
| 統計情報の更新運用(ANALYZE等)の確認 | 実行計画の見積もりが実態からずれるのを防ぐ*2*3 |
| EXPLAINでの実行計画確認の依頼 | 想定した索引・見積もり行数が実態と合っているかを検証する |
まとめ
本稿では、カーディナリティという指標が索引設計や実行計画にどう関わるかを整理しました。索引の内部構造そのものではなく、値のばらつきという観点から見たときの勘所に絞って振り返ると、要点は次の通りです。開発会社との会話の中でこの指標を共通言語にできれば、提案内容を評価する土台が整います。索引の内部構造や正規化の設計判断まで踏み込みたい場合は、それぞれの専門記事もあわせて参照するとよいでしょう。
- カーディナリティは、列に格納された値の種類の多さを表す指標である。
- 値のばらつきが大きい列ほど索引で行を絞り込みやすく、種類が少ない列は絞り込みの効果が薄くなりやすい。
- データベースは統計情報からカーディナリティを見積もり、索引を使うか全件走査するかを実行計画で判断する*2*3*4。
- 複合索引では、絞り込みへの寄与が大きい高カーディナリティの列を前方に置く並び順が基本となる。
- 発注・レビューでは、想定データ分布の共有、統計情報の更新運用、EXPLAINでの実行計画確認の3点を押さえておくとよい。
よくある質問
統計情報の更新は、どのくらいの頻度で行うとよいですか。
まとまったデータの登録・削除・更新が発生したタイミングで実行するのが基本の考え方です。統計情報は自動更新の仕組みを備える製品もありますが、大量のバッチ処理の直後などは実態とのずれが生じやすいものです。処理の後に手動でANALYZEを実行する運用も、あわせて検討するとよいでしょう*2*3。頻度は一律に決まるものではなく、データの更新パターンに合わせて調整する対応が現実的です。テーブルごとに更新の起きやすさが異なる場合は、頻度の高いテーブルから優先して運用ルールを整えておくと、抜け漏れを防ぎやすくなります。
カーディナリティが高い列に索引を追加すれば、処理は速くなりますか。
速くなりやすい傾向はありますが、それだけで判断できるわけではありません。実際にその列がWHERE句や結合条件で使われるか、複合索引の中での列順が適切かによっても効果は変わります*3。索引を増やすと書き込み時の負荷も増えるため、絞り込みに使われる見込みが高い列を選んで設計することが大切です。使われる見込みが薄い索引まで積み増していくと、検索の速さと引き換えに更新処理が重くなる場面も出てきます。
監視・ログでいう「カーディナリティ」も、同じ意味ですか。
指し示す対象は異なります。索引設計でのカーディナリティは、列に入る値の種類の多さを指す言葉です。一方、監視・ログ分析の領域では、メトリクスに付与するラベルの組み合わせ数が増え続ける状態を指して使われることが多くあります。どちらも「値・ラベルの種類が増えるほど負担が増す」という点は共通していますが、対象がデータベースの列かメトリクスのラベルかで区別して理解しておく必要があります。開発会社との会話で言葉の意味がかみ合わないと感じたら、どちらの文脈での話かを確認するとよいでしょう。
複合索引の列順は、あとから変更できますか。
既存の索引を削除して、列順を変えた索引を作り直す形で対応します。運用中のクエリの傾向が当初の想定と変わってきた場合は、列順の見直しが有効な選択肢になるでしょう。ただし索引の作り直しには時間と処理負荷がかかるため、本番環境での実行タイミングは開発会社とすり合わせておくことをおすすめします。作り直す前にEXPLAINで現状の実行計画を確認し、想定通りの効果が見込めるかを試算しておくと、手戻りを減らせます。テーブルの規模が大きいほど作り直しの負荷も上がるため、余裕を持ったスケジュールで計画しておきたいところです。
著者:テレリモ総研編集部 鈴木 亮佑
LASSICでは、国内ニアショア開発体制を活かしています。データベースの索引設計やクエリの実行計画の確認から実装、公開済みシステムの見直しまでを一貫して支援する体制です。要件定義の段階から実装、テスト、リリース後の運用・保守まで、工程を分けずに任せられる点も強みでしょう。索引設計やパフォーマンス面でお困りの際も、ご相談いただけます。
ご不明な点はお問い合わせフォームからもご連絡いただけます。
出典
- Wikipedia「Cardinality (SQL statements)」
- PostgreSQL公式ドキュメント「53.53. pg_stats」(n_distinct列の説明)
- MySQL 8.4 リファレンスマニュアル「15.7.7.22 SHOW INDEX Statement」(Cardinality列の説明)
- PostgreSQL公式ドキュメント「70.1. Row Estimation Examples」