データストア インデックス

App Engine は、エンティティの各プロパティに単純なインデックスを事前定義します。App Engine アプリケーションでは、アプリケーションの /war/WEB-INF/appengine-generated ディレクトリに生成される datastore-indexes.xml というインデックス構成ファイルで、さらにカスタム インデックスを定義できます。開発用サーバーは、既存のインデックスでは実行できないクエリが出現したときに、このファイルに自動的に候補を追加します。アプリケーションをアップロードする前にこのファイルを編集して、インデックスを手動で調整できます。

注: インデックス ベースのクエリ メカニズムはさまざまなクエリをサポートしているため、ほとんどのアプリケーションに適しています。ただし、他のデータベース テクノロジーで一般にサポートされているいくつかの種類のクエリがサポートされていません。具体的には、結合クエリおよび集計クエリは Datastore のクエリエンジン内でサポートされていません。Datastore クエリの制限については、Datastore クエリのページをご覧ください。

インデックスの定義と構造

インデックスは、特定の種類のエンティティのプロパティを、各プロパティの対応する順序(昇順または降順)で並べたリストに対して定義されます。また、祖先クエリで使用できるように、オプションでエンティティの祖先を含めることもできます。

インデックス テーブルには、インデックス定義で指定したすべてのプロパティに対応する列が含まれています。テーブルの各行は、インデックス ベースのクエリの結果になる Datastore 内のエンティティを表します。インデックスには、インデックスで使用されているすべてのプロパティに対応するインデックス付けされた値セットを持つエンティティのみが含められます。エンティティが値を持たないプロパティがインデックス定義で参照されている場合、そのエンティティはインデックスに含まれないため、インデックスに基づくクエリで結果として返されることはありません。

注: Datastore では、プロパティを持たないエンティティと、プロパティに null 値(null)が指定されているエンティティが区別されます。エンティティのプロパティに明示的に null 値を割り当てた場合、そのエンティティは、そのプロパティを参照するクエリの結果に含められる可能性があります。

注: 複数のプロパティで構成されているインデックスがある場合、個々のプロパティを「インデックスなし」に設定しないでください。

インデックス テーブルの行は祖先の順序で並べられた後に、インデックス定義で指定されたプロパティ値の順序で並べられます。クエリを最も効率的に実行できる「完璧なインデックス」とは、次のプロパティをこの順序で定義したものです。

  1. 等式フィルタで使用されるプロパティ
  2. 不等式フィルタ(プロパティを 1 つしか使用しないもの)で使用されるプロパティ
  3. 並べ替えの順序で使用されるプロパティ

これにより、実行される可能性があるすべてのクエリの結果が、テーブルの連続した行に表れます。Datastore は、次のステップで完璧なインデックスを使用してクエリを実行します。

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

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

Query q1 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
                new FilterPredicate("height", FilterOperator.EQUAL, 72)))
        .addSort("height", Query.SortDirection.DESCENDING);

このクエリにとっての完全なインデックスは、Person という種類のエンティティに対するキーのテーブルで、lastNameheight のプロパティ値に対する列を持つものです。インデックスはまず lastName の昇順で、次に height の降順で並べられます。

これらのインデックスを生成するには、インデックスを次のように構成します。

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Person" ancestor="false" source="manual">
    <property name="lastName" direction="asc"/>
    <property name="height" direction="desc"/>
  </datastore-index>
</datastore-indexes>

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

Query q2 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
                new FilterPredicate("height", FilterOperator.EQUAL, 63)))
        .addSort("height", Query.SortDirection.DESCENDING);

形式が異なるにもかかわらず、次の 2 つのクエリでも同じインデックスを使用します。

Query q3 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
                new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
        .addSort("height", Query.SortDirection.ASCENDING);

Query q4 =
    new Query("Person")
        .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
        .addSort("firstName", Query.SortDirection.ASCENDING)
        .addSort("height", Query.SortDirection.ASCENDING);

インデックスの構成

デフォルトでは、Datastore により、エンティティの種類ごとに各プロパティのインデックスが自動で事前定義されます。等式だけのクエリや単純な不等式クエリなどの多くの単純なクエリは、これらの事前定義されたインデックスで十分に実行できます。事前定義のインデックスで実行できないクエリについては、アプリケーションが必要とするインデックスを、datastore-indexes.xml という名前のインデックス構成ファイルに定義する必要があります。使用可能なインデックス(事前定義されたインデックスまたはインデックス構成ファイルで指定されたインデックス)では実行できないクエリを実行しようとすると、そのクエリは DatastoreNeedIndexException で失敗します。

Datastore は、次の形式のクエリに対するインデックスを自動的に作成します。

  • 祖先フィルタとキーフィルタだけを使用した、種類を指定しないクエリ
  • 祖先フィルタと等式フィルタだけを使用したクエリ
  • 不等式フィルタ(プロパティを 1 つしか使用しないもの)だけを使用したクエリ
  • 祖先フィルタ、プロパティに対する等式フィルタ、キーに対する不等式フィルタだけを使用したクエリ
  • フィルタが指定されておらず、プロパティに対する並べ替え順序(昇順か降順)だけが指定されたクエリ

上記以外の形式のクエリには、インデックス構成ファイルでインデックスを指定する必要があります。これには次のようなクエリが該当します。

  • 祖先フィルタと不等式フィルタを使用したクエリ
  • 1 つのプロパティに対して 1 つ以上の不等式フィルタを使用し、他のプロパティに対して 1 つ以上の等式フィルタを使用したクエリ
  • キーの降順という並べ替え順序を使用したクエリ
  • 複数の並べ替え順序を持つクエリ

インデックスとプロパティ

ここでは、インデックスについて特に考慮すべき点と、Datastore 内でのインデックスとエンティティのプロパティとの関連について留意しなければならないことを説明します。

値の型が混在するプロパティ

2 つのエンティティに名前が同じで値の型が異なるプロパティが存在する場合、エンティティはそのプロパティのインデックスにより、まず値の型で並べ替えられ、次にそれぞれの型に適した第 2 の順序付けで並べ替えられます。たとえば 2 つのエンティティそれぞれに age という名前のプロパティが存在し、一方が整数値を持つプロパティで、もう一方が文字列値を持つプロパティの場合、age プロパティを基準とした並べ替えを行うと、プロパティの値自体には関係なく常に整数値を持つエンティティのほうが文字列値を持つエンティティよりも順序が先になります。

Datastore では整数値と浮動小数点型の数値が別の型として区別されるため、このような動作について特に注意する必要があります。すべての整数値はすべての浮動小数点型の数値よりも順序が前になるため、整数値 38 を持つプロパティは浮動小数点値 37.5 を持つプロパティよりも前になります。

インデックスなしのプロパティ

フィルタリングや並べ替えを行わないことが明確なプロパティについては、インデックスなしとして宣言することで、そのプロパティのインデックス エントリを保持しないように Datastore に命令できます。これにより、Datastore 書き込みの実行数が削減され、アプリケーションの実行コストが削減されます。インデックスなしのプロパティを持つエンティティは、そのプロパティが設定されていないかのように振る舞います。インデックスなしのプロパティに対するフィルタまたは並べ替え順を指定したクエリが、そのエンティティに一致することはありません。

注: 複数のプロパティで構成されるインデックスに含まれるプロパティの 1 つをインデックスなしに設定すると、複合インデックスにインデックス付けされなくなります。

たとえば、エンティティにプロパティ ab があり、WHERE a ="bike" and b="red" のようなクエリを満たすインデックスを作成するとします。また、WHERE a="bike"WHERE b="red" というクエリには配慮しないものとします。この場合、a を「インデックスなし」に設定し、「a および b」に対してインデックスを作成すると、「a および b」インデックスに対するインデックス エントリは作成されず、WHERE a="bike" and b="red" というクエリは機能しません。Datastore に「a および b」インデックスのエントリを作成させるには、ab の両方が「インデックスあり」でなければなりません。

低レベルの Java Datastore API では、プロパティはそのプロパティを設定するために使用するメソッド(setProperty() または setUnindexedProperty())に応じて、エンティティ単位で「インデックスあり」または「インデックスなし」として定義されます。

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Key acmeKey = KeyFactory.createKey("Company", "Acme");

Entity tom = new Entity("Person", "Tom", acmeKey);
tom.setProperty("name", "Tom");
tom.setProperty("age", 32);
datastore.put(tom);

Entity lucy = new Entity("Person", "Lucy", acmeKey);
lucy.setProperty("name", "Lucy");
lucy.setUnindexedProperty("age", 29);
datastore.put(lucy);

Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);

Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);

// Returns tom but not lucy, because her age is unindexed
List<Entity> results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());

インデックス付けされるプロパティをインデックスなしに変更するには、値を setUnindexedProperty() でリセットし、インデックスなしからインデックスありに変更するには値を setProperty() でリセットします。

ただし、プロパティをインデックスなしからインデックスありに変更しても、変更前に作成されていたエンティティには適用されない点に注意してください。 そのようなプロパティに対してフィルタリングを行うクエリは、そうした既存のエンティティを返しません。それらはエンティティの作成時にクエリのインデックスに書き込まれていなかったためです。このようなエンティティに今後実行するクエリでアクセスできるようにするには、Datastore にエンティティを書き込み直して、適切なインデックスに含まれるようにする必要があります。つまり、既存のエンティティについては次の手順を行う必要があります。

  1. データストアからエンティティを取得します(get)。
  2. データストアにエンティティを書き戻します(put)。

同様に、プロパティをインデックスありからインデックスなしに変更した場合も、この変更が適用されるのは、変更後に Datastore に書き込まれたエンティティだけです。そのプロパティを持つ既存のエンティティに対応するインデックス エントリは、エンティティが更新または削除されない限り存在し続けます。望ましくない結果を回避するには、そのような(インデックスなしに変更した)プロパティでフィルタリングや並べ替えを行うすべてのクエリのコードを除去する必要があります。

インデックスの上限

Datastore では、単一のエンティティに関連付けられるインデックス エントリの数と合計サイズに上限があります。これらの上限は十分な余裕があるため、ほとんどのアプリケーションは影響を受けません。ただし、場合によっては制限に達することもあります。

前述のとおり、Datastore では、長いテキスト文字列(Text)、長いバイト文字列(Blob)、埋め込みエンティティ(EmbeddedEntity)、「インデックスなし」と明示的に宣言したエンティティを除く、すべてのエンティティのすべてのプロパティに対して、事前定義されたインデックスにエントリが作成されます。また、datastore-indexes.xml 構成ファイル内で宣言した追加のカスタム インデックスにプロパティを含めることもできます。エンティティにリスト プロパティが存在しない場合、そのようなエンティティはカスタム インデックスごとに最大で 1 つのエントリ(非祖先インデックスの場合)、またはエンティティの祖先ごとに 1 つのエントリ(祖先インデックスの場合)を持つことになります。このようなインデックス エントリは、プロパティの値が変化するたびに更新しなければなりません。

エンティティごとに 1 つの値を持つプロパティについては、そのプロパティの事前定義インデックスにエンティティごとに値を 1 回保存するだけで済みます。ただし、そのような単一値のプロパティを大量に持つエンティティの場合は、インデックスのエントリ数またはサイズの制限を超過する可能性があります。同様に、同じプロパティに対して複数の値を持つエンティティは値ごとに個別のインデックス エントリが必要になるため、値の数が大量に存在する場合は、やはりエントリの制限を超過する可能性があります。

複数のプロパティが存在するエンティティで、それぞれのプロパティが複数の値を取る場合、状況はさらに悪化します。そのようなエンティティに対処するには、可能性のあるすべてのプロパティ値の組み合わせに対応するエントリをインデックスに含める必要があります。カスタム インデックスが複数のプロパティを参照し、それぞれのプロパティが複数の値を持つ場合、組み合わせ数が爆発的に増加する可能性があり、比較的少数のプロパティ値しか持たないエンティティでもエントリが大量に必要になります。このようなインデックス爆発により大量のインデックス エントリを更新しなければならなくなるため、Datastore へのエンティティの書き込みコストが大幅に増えます。さらに、エンティティのインデックス エントリ数またはサイズ制限を簡単に超えてしまう可能性もあります。

次のクエリを検討します。

Query q =
    new Query("Widget")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("x", FilterOperator.EQUAL, 1),
                new FilterPredicate("y", FilterOperator.EQUAL, 2)))
        .addSort("date", Query.SortDirection.ASCENDING);

この場合、SDK は次のインデックスを提案します。

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget" ancestor="false" source="manual">
    <property name="x" direction="asc"/>
    <property name="y" direction="asc"/>
    <property name="date" direction="asc"/>
  </datastore-index>
</datastore-indexes>
このインデックスには、エンティティごとに合計 |x| * |y| * |date| 個のエントリが必要になります(ここでの |x| はプロパティ x のエンティティに関連付けられた値の数を示します)。たとえば、次のコードをご覧ください。
Entity widget = new Entity("Widget");
widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
widget.setProperty("y", Arrays.asList("red", "green", "blue"));
widget.setProperty("date", new Date());
datastore.put(widget);

上記のコードが作成するエンティティには、プロパティ x の値が 4 個、プロパティ y の値が 3 個あり、date に現在の日付が設定されます。この場合、全とおりの組み合わせのプロパティ値に対応するには、12 個のインデックス エントリが必要になります。

(1, "red", <now>) (1, "green", <now>) (1, "blue", <now>)

(2, "red", <now>) (2, "green", <now>) (2, "blue", <now>)

(3, "red", <now>) (3, "green", <now>) (3, "blue", <now>)

(4, "red", <now>) (4, "green", <now>) (4, "blue", <now>)

同じプロパティが何度も繰り返し使われている場合、Datastore がインデックス爆発を検出し、代替インデックスを提案することがあります。ただし、そうでない状況(この例で定義されているクエリなど)ではインデックス爆発が発生することになります。この場合、インデックス構成ファイルのインデックスを手動で次のように構成することにより、インデックス爆発を回避できます。

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget">
    <property name="x" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
  <datastore-index kind="Widget">
    <property name="y" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
</datastore-indexes>
これにより、必要なエントリの数は、12 ではなく、(|x| * |date| + |y| * |date|)、つまり 7 つに削減されます。

(1, <now>) (2, <now>) (3, <now>) (4, <now>)

("red", <now>) ("green", <now>) ("blue", <now>)

put オペレーションでインデックスのエントリ数またはサイズの上限を超過した場合、そのオペレーションは IllegalArgumentException で失敗します。例外のテキストには、どの上限を超えたか("Too many indexed properties" または "Index entries too large")と、原因となったカスタム インデックスが示されます。構築時にエンティティの上限を超える新しいインデックスを作成すると、そのインデックスに対するクエリは失敗し、Google Cloud コンソールでは Error 状態で表示されます。Error 状態のインデックスを解決する手順は次のとおりです。

  1. datastore-indexes.xml ファイルから Error 状態のインデックスを削除します。

  2. datastore-indexes.xml が置かれているディレクトリから次のコマンドを実行して、そのインデックスを Datastore から削除します。

    gcloud datastore indexes cleanup datastore-indexes.xml
    
  3. エラーの原因を解決します。次に例を示します。

    • インデックスの定義および対応するクエリを再構成する。
    • インデックス爆発の原因となっているエンティティを削除する。
  4. インデックスを datastore-indexes.xml ファイルに追加し直します。

  5. datastore-indexes.xml が置かれているディレクトリから次のコマンドを実行して、Datastore にインデックスを作成します。

    gcloud datastore indexes create datastore-indexes.xml
    

インデックス爆発は、リスト プロパティを使用するカスタム インデックスが必要なクエリを避けることで回避できます。すでに説明したように、これには複数の並べ替え順序を持つクエリや、等式フィルタと不等式フィルタが混在するクエリなどが該当します。