インデックスの最適化

このページでは、アプリ用に Datastore モードの Firestore のインデックスを選択する場合の考慮事項について説明します。

Datastore モードの Firestore では、クエリ パフォーマンスを高めるために、すべてのクエリに対してインデックスが使用されます。ほとんどのクエリのパフォーマンスは、データベースの合計サイズではなく、結果セットのサイズによって決まります。

Datastore モードの Firestore では、エンティティ内の各プロパティに対して組み込みインデックスが定義されます。これらの単一プロパティのインデックスは、多くの単純なクエリに対応しています。Datastore モードの Firestore では、その他のクエリにも対応するために、インデックス マージ機能を使用して組み込みインデックスをマージできます。より複雑なクエリの場合は、事前に複合インデックスを定義する必要があります。

このページでは、次の 2 つの重要なインデックス最適化機会に関連するインデックス マージ機能に焦点を当てています。

  • クエリの高速化
  • 複合インデックスの数の削減

次の例では、インデックス マージ機能の使用方法を示します。

Photo エンティティのフィルタ

種類 Photo のエンティティを含む Datastore モードのデータベースについて考えます。

写真
プロパティ 値の型 説明
owner_id 文字列 ユーザー ID
tag 文字列の配列 トークン化されたキーワード
size 整数 列挙:
  • 1 icon
  • 2 medium
  • 3 large
coloration 整数 列挙:
  • 1 black & white
  • 2 color

次のものを論理 AND で結合して Photo エンティティに対するクエリを実行するためのアプリ機能が必要であるものとします。

  • 次のプロパティに基づく最大 3 つのフィルタ:

    • owner_id
    • size
    • coloration
  • tag 検索文字列。アプリでは、検索文字列がタグにトークン化され、タグごとにフィルタが追加されます。

    たとえば、検索文字列 outside, family はクエリフィルタ tag=outside および tag=family に変換されます。

組み込みインデックスと Datastore モードの Firestore のインデックス マージ機能を使用すると、複合インデックスを別途追加しなくても、この Photo フィルタ機能のインデックス要件を満たすことができます。

Photo エンティティの組み込みインデックスは、次のような単一フィルタクエリに対応しています。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_id = client.query(kind="Photo", filters=[("owner_id", "=", "user1234")])

query_size = client.query(kind="Photo", filters=[("size", "=", 2)])

query_coloration = client.query(kind="Photo", filters=[("coloration", "=", 2)])

また、Photo フィルタ機能では、複数の等式フィルタを論理 AND で結合したクエリを使用する必要があります。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Datastore モードの Firestore では、組み込みインデックスをマージすることで、これらのクエリにも対応できます。

インデックス マージ

Datastore モードの Firestore では、クエリとインデックスが次のすべての制約を満たす場合にインデックス マージを使用できます。

  • クエリで使用しているフィルタが等式(=)フィルタのみである
  • クエリのフィルタと順序に完全に一致する複合インデックスが存在しない
  • いずれの等式フィルタも、クエリと同じ順序で 1 つ以上の既存のインデックスと一致する

Datastore モードの Firestore では、この状況を満たせば、追加の複合インデックスを構成しなくても、既存のインデックスを使用してクエリに対応できます。

複数のインデックスを同じ条件で並べ替える場合、Datastore モードの Firestore では、複数のインデックス スキャンの結果をマージして、該当するすべてのインデックスに共通する結果を見つけることができます。組み込みインデックスでは常にエンティティ キーを基準に値が並べ替えられるため、Datastore モードの Firestore ではこれらのインデックスをマージできます。

Datastore モードの Firestore では、組み込みインデックスをマージすることによって、複数のプロパティに対して等式フィルタを使用したクエリに対応できます。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Datastore モードの Firestore では、同じインデックスの複数のセクションからのインデックス結果をマージすることもできます。Datastore モードの Firestore では、tag プロパティの組み込みインデックスの複数の異なるセクションをマージすることによって、複数の tag フィルタを論理 AND で結合したクエリに対応できます。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_tag = client.query(
    kind="Photo",
    filters=[
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

query_owner_size_color_tags = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

組み込みインデックスをマージすることによって対応したクエリには、Photo フィルタリング機能で必要な一連のクエリがすべて含まれています。なお、Photo フィルタリング機能に対応するうえで、追加の複合インデックスが必要になることはありません。

アプリに最適なインデックスを選択するには、インデックス マージ機能を理解することが重要です。インデックス マージにより、Datastore モードの Firestore におけるクエリの柔軟性が高まりますが、その代償としてパフォーマンスが低下する可能性があります。次のセクションでは、インデックス マージのパフォーマンス、および複合インデックスを追加することによってパフォーマンスを向上させる方法について説明します。

完全インデックスを見つける

インデックスは、インデックス定義で指定された順序で、まず祖先、次にプロパティ値で並べられます。クエリを最も効率的に実行できる、クエリに対する最適な複合インデックスは、以下の特性に基づいて定義され、順に:

  1. 等式フィルタで使用されるプロパティ
  2. 並べ替えの順序で使用されるプロパティ
  3. distinctOn フィルタで使用されるプロパティ
  4. 範囲フィルタと不等式フィルタで使用されるプロパティ(並べ替え順序にまだ含まれていないもの)
  5. 集計と射影で使用されるプロパティ(並べ替え順序、範囲フィルタと不等式フィルタにまだ含まれていないもの)

これにより、実行される可能性があるすべてのクエリの結果が、確実に考慮されます。Datastore モードの Firestore データベースは、次のステップで完全なインデックスを使用してクエリを実行します。

  1. クエリの種類、フィルタのプロパティ、フィルタの演算子、並べ替えの順序に対応するインデックスを特定します。
  2. インデックスの先頭から、クエリのフィルタ条件のすべてまたはサブセットを満たす最初のエンティティまでスキャンします。
  3. インデックスのスキャンを続行し、すべてのフィルタ条件を満たす各エンティティを返します。スキャンは、次の条件のいずれかが満たされるまで続けられます。
    • フィルタ条件を満たさないエンティティを検出する
    • インデックスの末尾に到達する
    • クエリでリクエストされた結果の最大数に達した

たとえば、次のクエリについて考えてみます。

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority < 3
ORDER BY priority DESC

このクエリにとっての最適な複合インデックスは、Task という種類のエンティティに対するキーのインデックスで、categorypriority のプロパティ値に対する列を持つものです。インデックスはまず category の昇順で、次に priority の降順で並べられます。

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: desc

形式が同じでフィルタの値が異なる 2 つのクエリは、同じインデックスを使用します。たとえば、次のクエリでは前のクエリと同じインデックスを使用しています。

SELECT * FROM Task
WHERE category = 'Work'
  AND priority < 5
ORDER BY priority DESC

次のインデックスについては、

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: asc
  - name: created
    direction: asc

前のインデックスは、次の両方のクエリを満たすことができます。

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority = 5
ORDER BY created ASC

SELECT * FROM Task
WHERE category = 'Work'
ORDER BY priority ASC, created ASC

インデックス選択の最適化

このセクションでは、インデックス マージのパフォーマンス特性、およびインデックス マージに関連する次の 2 つの最適化機会について説明します。

  • 複合インデックスを追加して、マージしたインデックスを利用するクエリを高速化する
  • マージしたインデックスを利用して、複合インデックスの数を削減する

インデックス マージのパフォーマンス

Datastore モードの Firestore でインデックス マージを行う際には、ジグザグマージ結合アルゴリズムを使用して効率的にインデックスがマージされます。Datastore モードでは、このアルゴリズムに基づいて複数のインデックス スキャンからの一致候補を結合することで、クエリに一致する結果セットが作成されます。インデックス マージでは、書き込み時ではなく読み取り時にフィルタ コンポーネントが結合されます。Datastore モードの Firestore のほとんどのクエリのパフォーマンスは、結果セットのサイズによってのみ決まりますが、インデックス マージを使用したクエリのパフォーマンスは、クエリ内のフィルタ、およびデータベースで考慮される一致候補の数によって決まります。

インデックス マージのパフォーマンスが最高となるのは、インデックスのすべての一致候補がクエリフィルタを満たす場合です。この場合、パフォーマンスは O(R * I) になります。ここで、R は結果セットのサイズで、I はスキャンされたインデックスの数です。

パフォーマンスが最悪となるのは、データベースで考慮される一致候補の数が多いにもかかわらず、そのほとんどがクエリフィルタを満たさない場合です。この場合、パフォーマンスは O(S) になります。ここで、S は単一のインデックス スキャンで候補とみなされた最小エンティティ セットのサイズです。

実際のパフォーマンスは、データの形状によって変わります。各結果が返される際に考慮される平均エンティティ数は O(S/(R * I)) です。多くのエンティティが各インデックス スキャンに一致したとしても、クエリに一致するエンティティが全体として少ない場合、つまり R が小さく、S が大きい場合、クエリのパフォーマンスは低下します。

このリスクは次の 4 つのことによって軽減されます。

  • クエリ プランナーは、クエリ全体と一致することがわかったエンティティしか検索しない。

  • ジグザグ アルゴリズムは、すべての結果を検索しなくても次の結果を返すことができる。最初の 10 件の結果をリクエストした場合、その 10 件の結果の検索に伴うレイテンシしか発生しない。

  • ジグザグ アルゴリズムは、偽陽性の結果の大部分をスキップする。パフォーマンスが最悪となるのは、偽陽性の結果がスキャン間で(並べ替え順に)完全に混ざり合っている場合のみです。

  • レイテンシは、各フィルタに一致するエンティティの数ではなく、各インデックス スキャンで見つかったエンティティの数によって決まります。次のセクションに示すように、複合インデックスを追加することによって、インデックス マージのパフォーマンスを向上させることができます。

インデックス マージを使用したクエリの高速化

Datastore モードの Firestore でインデックスをマージすると、多くの場合、各インデックス スキャンがクエリ内の単一のフィルタにマップされます。クエリ内の複数のフィルタに一致する複合インデックスを追加すると、クエリ パフォーマンスが向上します。

次のクエリについて考えます。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_size_tag = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "username"),
        ("size", "=", 2),
        ("tag", "=", "family"),
    ],
)

各フィルタは、次の組み込みインデックスの 1 つのインデックス スキャンにマップされます。

Index(Photo, owner_id)
Index(Photo, size)
Index(Photo, tag)

複合インデックス Index(Photo, owner_id, size) を追加すると、次に示すように、クエリは 3 つではなく 2 つのインデックス スキャンにマップされます。

#  Satisfies both 'owner_id=username' and 'size=2'
Index(Photo, owner_id, size)
Index(Photo, tag)

大きな画像と白黒画像は多数存在するものの、パノラマ画像は少ないというシナリオについて考えます。クエリフィルタがパノラマ画像と白黒画像の両方を対象としている場合、組み込みインデックスをマージすると、パフォーマンスが低下します。

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_size_coloration = client.query(
    kind="Photo", filters=[("size", "=", 2), ("coloration", "=", 1)]
)

クエリ パフォーマンスを向上させるには、次の複合インデックスを追加して、O(S/(R * I))S の値(単一のインデックス スキャンの最小エンティティ セット)を小さくします。

Index(Photo, size, coloration)

2 つのクエリフィルタが同じであっても、この複合インデックスを使用した場合と 2 つの組み込みインデックスを使用した場合では、前者の方が候補となる結果の数が減ります。このアプローチでは、追加した 1 つのインデックスにかかる費用を支払うだけで、パフォーマンスが大幅に向上します。

インデックス マージを利用することによる複合インデックスの数の削減

クエリ内のフィルタと完全に一致する複合インデックスは最高のパフォーマンスを発揮しますが、フィルタの組み合わせによっては、複合インデックスの追加が必ずしも最善策でなかったり、複合インデックスを追加できなかったりする場合もあります。次のことを考慮して複合インデックスのバランスを取る必要があります。

  • 複合インデックスの制限:

    上限
    データベース 1 つあたりの複合インデックスの最大数
    エンティティの複合インデックス エントリの最大合計サイズ 2 MiB
    エンティティの次の要素の最大合計数:
    • インデックス付けされたプロパティ値の数
    • 複合インデックスのエントリ数
    20,000
  • インデックスの追加に伴うストレージ費用の単価
  • 書き込みレイテンシへの影響

インデックスの問題の多くは、Photo エンティティの tag プロパティのような複数値フィールドに起因して起こります。

たとえば、Photo フィルタリング機能において、降順の並べ替え基準として次の 4 つのプロパティを追加する必要が生じた場合について考えます。

写真
プロパティ 値の型 説明
date_added 整数 日時
rating 浮動小数点数 総合ユーザー評価
comment_count 整数 コメント数
download_count 整数 ダウンロード数

tag フィールドを無視すれば、Photo フィルタのすべての組み合わせに一致する複合インデックスを選択できます。

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -comments)
Index(Photo, size, -date_added)
Index(Photo, size, -comments)
...
Index(Photo, owner_id, size, -date_added)
Index(Photo, owner_id, size, -comments)
...
Index(Photo, owner_id, size, coloration, -date_added)
Index(Photo, owner_id, size, coloration, -comments)

複合インデックスの合計数は次のとおりです。

2^(number of filters) * (number of different orders) = 2 ^ 3 * 4 = 32 composite indexes

最大 3 つの tag フィルタをサポートしようとすると、複合インデックスの合計数は次のようになります。

2 ^ (3 + 3 tag filters) * 4 = 256 indexes.

tag のような複数値プロパティを含むインデックスを使用すると、インデックス爆発の問題が発生し、ストレージ コストと書き込みレイテンシが増加します。

この機能で tag フィールドでのフィルタに対応するには、インデックスをマージして、インデックスの合計数を減らします。次の複合インデックスのセットは、Photo フィルタリング機能をこの並べ替えに対応させるために最低限必要です。

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -rating)
Index(Photo, owner_id, -comments)
Index(Photo, owner_id, -downloads)
Index(Photo, size, -date_added)
Index(Photo, size, -rating)
Index(Photo, size, -comments)
Index(Photo, size, -downloads)
...
Index(Photo, tag, -date_added)
Index(Photo, tag, -rating)
Index(Photo, tag, -comments)
Index(Photo, tag, -downloads)

定義される複合インデックスの数は次のとおりです。

(number of filters + 1) * (number of orders) = 7 * 4 = 28

インデックス マージを使用すると、次のようなメリットもあります。

  • Photo エンティティで最大 1,000 個のタグを扱うことができ、1 つのクエリで tag フィルタを無制限に使用できます。
  • インデックスの合計数が減り、結果としてストレージ費用と書き込みレイテンシを削減できます。

アプリ用のインデックスの選択

Datastore モードのデータベースに最適なインデックスを選択するには、次の 2 つのアプローチを使用できます。

  • インデックス マージを使用して追加のクエリに対応する

    • 必要な複合インデックスの数が減る
    • エンティティあたりのストレージ費用が減少する
    • 書き込みレイテンシを改善
    • インデックスの爆発を回避できる
    • パフォーマンスはデータの形状によって変わる
  • クエリ内の複数のフィルタに一致する複合インデックスを定義する

    • クエリ パフォーマンスが向上する
    • データの形状に依存しない一貫したクエリ パフォーマンスが保証される
    • 複合インデックスの制限以下に留める
    • エンティティあたりのストレージ費用が増加する
    • 書き込みレイテンシの増加

アプリに最適なインデックスは、データの形状に応じて変わります。クエリ パフォーマンスをサンプリングすると、アプリの一般的なクエリと低速のクエリを把握するのに役立ちます。この情報を基にインデックスを追加することで、一般的なクエリと低速のクエリの両方のパフォーマンスを向上させることができます。