Datastore-Indexe

App Engine gibt für jede Property einer Entität einen einfachen Index vor. Eine App Engine-Anwendung kann weitere benutzerdefinierte Indexe in der Indexkonfigurationsdatei datastore-indexes.xml definieren, die im Verzeichnis /war/WEB-INF/appengine-generated Ihrer Anwendung generiert wird. Der Entwicklungsserver fügt dieser Datei automatisch Vorschläge hinzu, wenn er Abfragen ermittelt, die mit den vorhandenen Indexen nicht ausgeführt werden können. Sie können die Indexe vor dem Hochladen der Anwendung von Hand optimieren.

Eine ausführliche Erläuterung von Indexen und Abfragen finden Sie unter Indexauswahl und erweiterte Suche.

Hinweis: Der indexbasierte Abfragemechanismus unterstützt ein breites Spektrum an Abfragen und ist für die meisten Anwendungen geeignet. Allerdings werden einige Arten von Abfragen, die in anderen Datenbanktechnologien üblich sind, vom Abfragemodul in Datastore nicht unterstützt, insbesondere Join- und Aggregatabfragen. Weitere Informationen zu Einschränkungen bei Datastore-Abfragen finden Sie auf der Seite Datastore-Abfragen.

Indexdefinition und -struktur

Für jedes Attribut ist in einer Attributliste eines bestimmten Entitätstyps ein Index in entsprechender Reihenfolge (aufsteigend oder absteigend) definiert. Für Ancestor-Abfragen kann der Index optional auch die Ancestors einer Entität umfassen.

Eine Indextabelle beinhaltet eine Spalte für jedes Attribut, das in der Definition des Indexes genannt wird. Jede Tabellenzeile stellt eine Entität in Datastore dar, die ein potenzielles Ergebnis für Abfragen auf Grundlage des Indexes ist. Eine Entität wird nur dann in den Index eingeschlossen, wenn jedes der verwendeten Attribute über einen indexierten Wert verfügt. Wenn sich die Indexdefinition auf ein Attribut ohne Entitätswert bezieht, wird die Entität nicht im Index aufgeführt und daher niemals als Ergebnis einer indexbasierten Abfrage angezeigt.

Hinweis: Datastore unterscheidet zwischen einer Entität, die kein Attribut hat, und einer Entität, die das Attribut mit einem Nullwert null umfasst. Wenn Sie dem Attribut einer Entität explizit einen Nullwert zuweisen, kann diese Entität in die Ergebnisse einer Abfrage aufgenommen werden, die sich auf dieses Attribut bezieht.

Hinweis: Bei Indexen, die sich aus mehreren Attributen zusammensetzen, darf keines der Attribute auf nicht indexiert gesetzt sein.

Die Zeilen einer Indextabelle werden in der definierten Reihenfolge zuerst nach Ancestor und dann nach Attributwert sortiert. Der perfekte Index für eine Abfrage, der die effizienteste Verarbeitung ermöglicht, ist nach den folgenden Attributen definiert (in dieser Reihenfolge):

  1. In Gleichheitsfiltern verwendete Attribute
  2. In Ungleichheitsfiltern verwendetes Attribut (von denen es höchstens eines geben kann)
  3. In Sortierreihenfolgen verwendete Attribute

Dadurch wird sichergestellt, dass alle Ergebnisse für jede mögliche Ausführung der Abfrage in aufeinanderfolgenden Tabellenzeilen angezeigt werden. Datastore führt eine Abfrage mit einem perfekten Index so aus:

  1. Identifizierung des zugehörigen Indexes, der Filterattribute, der Filteroperatoren und der Sortierreihenfolge eines Abfragetyps
  2. Scan des Indexes vom Anfang bis zur ersten Entität, die allen Filterbedingungen der Abfrage entspricht
  3. Fortsetzen des Index-Scans und Ausgabe jeder Entität, bis
    • Cloud Datastore auf eine Entität trifft, die den Filterbedingungen nicht entspricht, oder
    • das Index-Ende erreicht ist, oder
    • die maximale Anzahl an Ergebnissen, die von der Abfrage angefordert wurden, erreicht ist.

Betrachten Sie beispielsweise die folgende Abfrage:

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);

Der perfekte Index für diese Abfrage ist eine Schlüsseltabelle für Entitäten vom Typ Person mit Spalten für die Werte der Attribute lastName und height. Der Index wird zuerst in aufsteigender Reihenfolge nach lastName und dann in absteigender Reihenfolge nach height sortiert.

Für zwei Abfragen, die die gleiche Form, jedoch unterschiedliche Filterwerte haben, wird derselbe Index verwendet. Die folgende Abfrage verwendet den gleichen Index wie oben:

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);

Für die beiden nachfolgenden Abfragen wird trotz ihrer unterschiedlichen Form ebenfalls derselbe Index verwendet:

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);

und

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

Indexkonfiguration

Standardmäßig definiert Datastore für jedes Attribut jedes Entitätstyps automatisch einen Index. Diese vordefinierten Indexe reichen aus, um eine Vielzahl von einfachen Abfragen wie Gleichheitsabfragen oder einfache Ungleichheitsabfragen auszuführen. Für alle anderen Abfragen muss die Anwendung die erforderlichen Indexe in einer Indexkonfigurationsdatei mit dem Namen datastore-indexes.xml definieren. Wenn die Anwendung versucht, eine Abfrage auszuführen, die nicht mit den verfügbaren, d. h. mit den vordefinierten oder in der Indexkonfigurationsdatei definierten Indexen, realisiert werden kann, schlägt die Abfrage mit einem DatastoreNeedIndexException fehl.

Datastore erstellt für folgende Abfragetypen automatisch Indexe:

  • Typlose Abfragen, die nur Ancestors und Schlüsselfilter benötigen
  • Abfragen, die nur Ancestors und Gleichheitsfilter benötigen
  • Abfragen, in denen nur Ungleichheitsfilter verwendet werden, die auf ein einziges Attribut beschränkt sind
  • Abfragen, in denen nur Ancestor-Filter, Gleichheitsfilter für Attribute und Ungleichheitsfilter für Schlüssel verwendet werden
  • Abfragen ohne Filter und mit nur einer Sortierung nach einem Attribut, entweder auf- oder absteigend

Bei anderen Abfragetypen müssen die Indexe in der Indexkonfigurationsdatei angegeben werden, einschließlich:

  • Abfragen mit Ancestor- und Ungleichheitsfilter
  • Abfragen mit einem oder mehreren Ungleichheitsfiltern für ein Attribut sowie einem oder mehreren Gleichheitsfiltern für andere Attribute
  • Abfragen mit einer Sortierreihenfolge nach Schlüsseln in absteigender Reihenfolge
  • Abfragen mit mehreren Sortierreihenfolgen

Indexe und Attribute

Nachfolgend sind einige bei Indexen besonders zu beachtenden Aspekte und ihre Auswirkungen auf die Attribute von Entitäten in Datastore beschrieben:

Attribute mit gemischten Werttypen

Wenn zwei Entitäten Attribute mit dem gleichen Namen, aber mit unterschiedlichen Werttypen haben, sortiert ein Index für das Attribut die Entitäten zuerst nach dem Werttyp und dann nach einer für den jeweiligen Typ geeigneten Sekundärreihenfolge. Wird beispielsweise nach dem Attribut age sortiert und zwei verschiedene Entitäten haben jeweils ein Attribut mit diesem Namen age – eines mit ganzzahligem Wert und eines mit Stringwert – wird die Entität mit dem ganzzahligen Wert immer vor der Entität mit dem Stringwert gelistet, unabhängig von den jeweiligen Attributwerten.

Das ist besonders bei ganzzahligen und Gleitkommawerten zu beachten, die in Datastore wie separate Werttypen behandelt werden. Da Ganzzahlen immer vor den Gleitkommazahlen aufgeführt werden, wird ein Attribut mit dem ganzzahligen Wert 38 vor dem Gleitkommawert 37.5 gereiht.

Nicht indexierte Attribute

Wenn Sie keinen Filter oder keine Sortierreihenfolge für ein bestimmtes Attribut benötigen, können Sie Datastore anweisen, keine Indexierung für dieses Attribut durchzuführen. Deklarieren Sie dazu das Attribut als nicht indexiert. Dadurch verringern sich die Kosten für die Ausführung Ihrer Anwendung, da die Anzahl der auszuführenden Datastore-Schreibvorgänge verringert wird. Eine Entität mit einem nicht indexierten Attribut verhält sich so, als sei es nicht festgelegt worden. Bei Abfragen mit einem Filter oder einer Sortierfolge für das nicht indexierte Attribut wird diese Entität niemals angezeigt.

Hinweis: Wenn ein Attribut in einem Index aufgeführt ist, der sich aus mehreren Attributen zusammensetzt, wird durch ihre Festlegung auf „nicht indexiert“ verhindert, dass es in dem zusammengesetzten Index indexiert wird.

Angenommen, eine Entität hat die Attribute a und b und Sie möchten einen Index erstellen, der Abfragen wie WHERE a ="bike" and b="red" erfüllen kann. Gehen Sie außerdem davon aus, dass die Abfragen WHERE a="bike" und WHERE b="red" für Sie irrelevant sind. Wenn Sie a als nicht indexiert festgelegt haben und einen Index für a und b erstellt haben, erstellt Datastore keine Indexeinträge für den Index a und b und die Abfrage WHERE a="bike" and b="red" schlägt fehlt. Damit Datastore für die Indexe a und b Einträge erstellt, muss sowohl a als auch b indexiert werden.

In der Java Datastore API werden Attribute auf niedriger Ebene je nach Entität und abhängig von der zum Festlegen verwendeten Methode (setProperty() oder setUnindexedProperty()) als indexiert oder nicht indexiert festgelegt:

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());

Sie können ein indexiertes Attribut in nicht indexiert ändern, indem Sie seinen Wert mit setUnindexedProperty() zurücksetzen, oder ein nicht indexiertes Attribut in indexiert ändern, indem Sie den Wert mit setProperty() zurücksetzen.

Wenn Sie ein Attribut von nicht indexiert auf indexiert ändern, sind etwaige vor der Änderung erstellte Entitäten nicht von der Änderung betroffen. Abfragen, mit denen solche Attribute gefiltert werden, geben diese Entitäten nicht als Ergebnis aus, da diese bei Erstellung der Abfrage nicht im Abfrageindex enthalten waren. Damit die Entitäten für zukünftige Abfragen zur Verfügung stehen, müssen Sie sie neu in Datastore schreiben, damit sie in die entsprechenden Indexe aufgenommen werden. Sie müssen also die folgenden Schritte für jede bestehende Entität ausführen:

  1. Rufen Sie die Entität aus Datastore ab (get).
  2. Schreiben Sie die Entität zurück in Datastore (put).

Wenn Sie ein Attribut von „indexiert“ in „nicht indexiert“ ändern, sind entsprechend nur Entitäten betroffen, die nach der Änderung in Datastore geschrieben wurden. Die Indexeinträge für vorhandene Entitäten mit diesem Attribut bleiben bestehen, bis die Entitäten aktualisiert oder gelöscht werden. Um keine unerwünschten Ergebnisse zu erhalten, müssen Sie alle Abfragen, die nach diesem (nun nicht indexierten) Attribut filtern oder sortieren, aus Ihrem Code dauerhaft löschen.

Indexbeschränkungen

In Datastore gelten Beschränkungen für die Anzahl und Gesamtgröße der Indexeinträge, die mit einer einzelnen Entität verknüpft werden können. Diese Beschränkungen sind sehr großzügig bemessen und sind für die meisten Anwendungen nicht relevant. Unter bestimmten Umständen könnten sie jedoch für Sie relevant werden.

Wie oben beschrieben, erstellt Datastore einen Eintrag in einem vordefinierten Index für jedes Attribut jeder Entität mit Ausnahme von langen Textstrings (Text), langen Bytestrings (Blob) und eingebetteten Entitäten (EmbeddedEntity) sowie explizit als nicht indexiert deklarierten Entitäten. Das Attribut kann auch in zusätzlichen benutzerdefinierten Indexen enthalten sein, die Sie in der Konfigurationsdatei datastore-indexes.xml angegeben haben. Wenn eine Entität keine gelisteten Properties hat, verfügt sie maximal über einen Eintrag in einem solchen benutzerdefinierten Index (bei Nicht-Ancestor-Indexen) bzw. einen Eintrag für jeden Ancestor der Entität (bei Ancestor-Indexen). Jeder dieser Indexeinträge muss einzeln aktualisiert werden, wenn sich der Wert des Attributs ändert.

Bei einem Attribut, das für jede Entität über einen einzelnen Wert verfügt, muss jeder mögliche Wert nur einmal pro Entität im vordefinierten Index des Attributs gespeichert werden. Trotzdem ist es möglich, dass eine Entität mit einer großen Anzahl solcher Einzelwert-Attribute das Indexeintrags- oder Eintragsgrößenlimit überschreitet. Ebenso benötigt eine Entität, die mehrere Werte für dasselbe Attribut haben kann, für jeden Wert einen eigenen Indexeintrag. Wenn die Anzahl der möglichen Werte besonders groß ist, kann es sein, dass eine solche Entität das Eintragslimit überschreitet.

Bei Entitäten mit mehreren Attributen, die wiederum mehrere Werte annehmen können, ist die Situation noch schwieriger. Der Index muss einen Eintrag für jede mögliche Kombination der Attributwerte enthalten, um einer solchen Entität gerecht zu werden. Bei benutzerdefinierten Indexen, die sich auf mehrere Attribute beziehen, die wiederum mehrere mögliche Werte haben, kann die Anzahl von Kombinationen „explodieren“. Dadurch wird für eine Entität mit einer relativ geringen Anzahl möglicher Attributwerte eine große Anzahl Einträge erforderlich. Durch diese explodierenden Indexe können die Kosten für das Schreiben einer Entität in Datastore erheblich steigen, da eine hohe Anzahl Indexeinträge aktualisiert werden muss. Außerdem kann das leicht dazu führen, dass die Entität das Indexeintrags- oder Größenlimit überschreitet.

Betrachten Sie folgende Abfrage:

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);

Auf dieser Grundlage schlägt das SDK den folgenden Index vor:

<?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>
Dieser Index erfordert insgesamt |x| * |y| * |date| Einträge für jede Entität (wobei |x| die Anzahl der Werte angibt, die der Entität für Attribut x zugeordnet sind). Zum Beispiel der folgende Code
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);

erstellt eine Entität mit vier Werten für das Attribut x, drei Werte für das Attribut y und dem aktuellen Datum als Wert für das Attribut date. In diesem Fall werden 12 Indexeinträge benötigt – einer für jede mögliche Kombination von Attributwerten:

(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>)

Wenn dasselbe Attribut mehrere Male wiederholt wird, kann Datastore explodierende Indexe erkennen und alternative Indexe vorschlagen. In allen anderen Fällen (z. B. bei der in diesem Beispiel definierten Abfrage) generiert Datastore jedoch einen explodierenden Index. In diesem Fall können Sie den explodierenden Index umgehen, indem Sie mithilfe Ihrer Indexkonfigurationsdatei manuell einen Index definieren:

<?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>
Dadurch wird die Anzahl der erforderlichen Einträge auf nur (|x| * |date| + |y| * |date|) bzw. 7 statt 12 Einträgen reduziert:

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

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

Jeder Put-Vorgang, der dazu führt, dass ein Index das Eintrags- oder Größenlimit für Indexe überschreitet, schlägt mit der Ausnahme IllegalArgumentException fehl. Der Text der Fehlermeldung beschreibt, welches Limit überschritten wurde ("Too many indexed properties" oder "Index entries too large") und welcher benutzerdefinierte Index die Ursache war. Wenn Sie einen neuen Index erstellen, der die Limits für eine Entität überschreiten würde, schlagen Abfragen des Index fehl und der Index wird in der Cloud Console mit dem Status Error angezeigt. So können Sie Indexe mit dem Status Error reparieren:

  1. Entfernen Sie den Index mit dem Status Error aus der Datei datastore-indexes.xml.

  2. Führen Sie den folgenden Befehl aus dem Verzeichnis aus, in dem sich datastore-indexes.xml befindet, um diesen Index aus Datastore zu entfernen:

    gcloud datastore cleanup-indexes datastore-indexes.xml
    
  3. Beheben Sie die Fehlerursache. Beispiel:

    • Formulieren Sie die Indexdefinition und die entsprechenden Abfragen um.
    • Entfernen Sie die Entitäten, die dazu führen, dass ein Index explodiert.
  4. Fügen Sie den Index wieder in die Datei datastore-indexes.xml ein.

  5. Führen Sie den folgenden Befehl in dem Verzeichnis aus, das datastore-indexes.xml enthält, um den Index in Datastore zu erstellen:

    gcloud datastore create-indexes datastore-indexes.xml
    

Das Explodieren von Indexen können Sie verhindern, indem Sie Abfragen vermeiden, für die ein benutzerdefinierter Index erforderlich ist, der ein Listenattribut verwendet. Dazu gehören wie oben beschrieben Abfragen mit mehreren Sortierreihenfolgen und Abfragen mit einer Kombination aus Gleichheits- und Ungleichheitsfiltern.