Cloud Datastore 索引

應用程式引擎會針對實體的每個屬性預先定義簡單的索引。App Engine 應用程式可在名為 datastore-indexes.xml 的「索引設定檔」中進一步定義自訂索引,而此設定檔會產生於應用程式的 /war/WEB-INF/appengine-generated 目錄中。開發伺服器遇到無法使用現有索引執行的查詢時,即會自動在這個檔案中新增建議。您可在上傳應用程式之前編輯該檔案,手動調整索引。

如需有關索引和查詢的深入說明,請參閱索引選取與進階搜尋一文。

附註:基於索引的查詢機制支援各種查詢,適用於大多數應用程式。不過這種機制並不支援其他資料庫技術常見的某些查詢種類,Cloud Datastore 查詢引擎尤其不支援彙整與匯總查詢。如要瞭解 Cloud Datastore 查詢的限制,請參閱 Datastore 查詢頁面。

索引定義和結構

索引的定義列於指定實體種類的屬性清單中,每個屬性會有對應的排序順序 (遞增或遞減)。如需與祖系查詢一同使用,索引也可以選擇是否包含實體的祖系。

索引表格中,該索引定義中提及的屬性都各自有一欄。表格中的每一列代表 Cloud Datastore 中的一個實體,這些實體可能是以索引為基礎的查詢結果。只有在索引使用的每個屬性均已設定編入索引的值時,該實體才會加入索引中;如果索引定義參照實體未設有值的屬性,該實體將不會出現在索引中,且因此不會在任何基於該索引的查詢結果中傳回。

附註:Cloud Datastore 會區分不含屬性值的實體與屬性值含空值 (null) 的實體。如果您明確將實體的屬性指定為空值,該實體便可能列入參照該屬性的查詢結果中。

附註:如果索引含有多個屬性,則每個屬性不得設為未編入索引

索引表格的列會依照索引定義的指定順序,先按祖系排序,再按屬性值排序。查詢的「完美索引」可以讓查詢以最有效率的方式執行,此索引由下列屬性依序定義:

  1. 等式篩選器使用的屬性
  2. 不等式篩選器使用的屬性 (可以不超過一個)
  3. 排序順序使用的屬性

這可確保每個潛在查詢執行作業的所有結果,都會出現在表格的連續列中。Cloud Datastore 會依照下列步驟使用完美索引來執行查詢:

  1. 找出查詢種類、篩選屬性、篩選運算子以及排序順序所對應的索引。
  2. 從索引的開頭開始掃描,直至找出符合該查詢所有篩選條件的第一個實體。
  3. 連續掃描索引,依次傳回每個實體,直到:
    • 遇到不符合篩選條件的實體,或
    • 抵達索引尾端,或
    • 已收集到查詢所要求的結果數上限。

例如,請思考以下查詢:

Java 8

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

Java 7

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 遞減排序。

屬於相同表單但具有不同篩選器值的兩個查詢,將使用相同的索引。例如,以下查詢使用與前述查詢相同的索引:

Java 8

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

Java 7

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

以下兩個查詢雖然形式不同,但也使用相同索引:

Java 8

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

Java 7

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

Java 8

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

Java 7

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

索引設定

根據預設,Cloud Datastore 會針對每種實體種類的每個屬性,自動預先定義索引。這些預先定義的索引便足以執行許多簡易查詢,例如只使用等式的查詢以及簡易的不等式查詢。對於所有其他查詢,應用程式必須在名為 datastore-indexes.xml 的「索引設定檔」中定義所需的索引。若應用程式嘗試執行不能使用可用索引執行的查詢 (無論是預先定義的索引,或是索引設定檔中指定的索引),查詢便會失敗並顯示 DatastoreNeedIndexException

Datastore 會為下列形式的查詢建構自動索引:

  • 僅使用祖系和金鑰篩選器的無種類查詢
  • 僅使用祖系和等式篩選條件的查詢
  • 僅使用不等式篩選條件的查詢 (只限單一屬性)
  • 只使用祖系篩選條件、等式篩選條件 (查詢屬性),以及不等式篩選條件 (查詢金鑰) 的查詢
  • 沒有篩選條件,且只有一個按遞增或遞減順序查詢屬性的排序順序

其他形式的查詢必須在索引設定檔中指定索引,包含:

  • 使用祖系和不等式篩選條件的查詢
  • 使用一或多個不等式篩選條件查詢某個屬性,並使用一或多個等式查詢其他屬性的查詢
  • 按遞減順序排序金鑰的查詢
  • 使用多個排序順序的查詢

索引與屬性

有關索引以及索引與 Cloud Datastore 中實體屬性的關係,以下是一些特別注意事項:

具有混合值類型的屬性

如果兩個實體所含的屬性名稱相同但值類型不同,屬性索引會先依據值類型排序實體,再按照適合各個類型的次要順序排序。舉例來說,如果兩個實體都含有名為 age 的屬性,其中一個屬性含有整數值,另一個含有字串值,則在按 age 屬性排序時,不管屬性值本身的值大小為何,含有整數值的實體皆會一律位於含有字串值的實體之前。

在整數值和浮點數的情況下尤其需要注意,因為 Cloud Datastore 將這兩種數值視為不同類型。因為整數會排在浮點數之前,所以含有整數值 38 的屬性會排在含有浮點值 37.5 的屬性之前。

未編入索引的屬性

如果您確定永遠都不需要篩選或排序特定屬性,則可以將該屬性宣告為「未編入索引」,藉以告知 Cloud Datastore 不用維護該屬性的索引項目。這樣可以降低必須執行的 Cloud Datastore 寫入次數,進而減少應用程式的執行費用。含有未編入索引屬性的實體,行為方式與未設定該屬性的實體一樣:該實體將永遠不符合篩選或排序該屬性的查詢。

附註:如果屬性出現在由多個屬性組成的索引中,將該屬性設為未編入索引,便會防止該屬性編入多屬性的索引中。

舉例來說,假設某實體含有「a」和「b」兩種屬性,且您想要建立一個能夠符合像是 WHERE a ="bike" and b="red" 查詢的索引。另外假設您不在乎 WHERE a="bike"WHERE b="red" 查詢。若您將「a」設為「不編入索引」,並為「a」和「b」建立索引,則 Cloud Datastore 不會為「a」和「b」索引建立索引項目,因此 WHERE a="bike" and b="red" 查詢無法發揮作用。如要讓 Cloud Datastore 為「a」和「b」索引建立項目,「a」和「b」皆必須編入索引。

在低階 Java Datastore API 中,系統會將每一個實體的屬性定義為編入索引或未編入索引 (視您設定屬性時使用的是 setProperty()setUnindexedProperty() 方法而定):

Java 8

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

Java 7

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() 重設值。

但請注意,將屬性從未編入索引變更為編入索引,並不會影響改變之前建立的任何現有實體。查詢使用該屬性進行篩選時,將不會傳回這類現有實體,因為這些實體在建立時並未寫入查詢的索引。為使實體可供未來的查詢使用,您必須將實體重新寫入 Cloud Datastore,才能讓實體加入適當的索引中。亦即,您必須為每個現有實體執行以下動作:

  1. 從 Cloud Datastore 擷取 (get) 實體。
  2. 將實體寫入 (put) 回 Cloud Datastore。

同樣地,將屬性從已編入索引變更為未編入索引,只會影響後續寫入 Cloud Datastore 的實體。在更新或刪除實體之前,含有該屬性的任何現有實體仍會保有其索引項目。為了避免不必要的結果,您必須清除按該屬性 (現在未編入索引的屬性) 篩選或排序的所有查詢程式碼。

索引上限

針對可以與單一實體相關聯的索引項目,Datastore 會限制數量和總體大小。這些限制的上限很高,大多數應用程式都不受影響。不過,有些情況下您可能會受到限制。

前文所述,Cloud Datastore 會在每一個實體的每個屬性的預先定義索引中建立一個項目,但不含長文字字串 (Text)、長位元組字串 (Blob)、嵌入實體 (EmbeddedEntity),以及您已明確宣告為未編入索引的屬性。該屬性在您的 datastore-indexes.xml 設定檔 中也可能列於外加自訂索引中。如果實體並未列出屬性,則實體在每個這類的自訂索引 (非祖系索引) 中最多有一個項目,或在每個祖系 (祖系索引) 中有一個項目。每次變更屬性值時,就必須更新每個索引項目。

如果某屬性在每個實體只有單一值,便必須在該屬性的預先定義索引中,為每個實體儲存僅一次每個可能的值。即便如此,仍可能有實體因為該單一值屬性過大,導致超過索引項目或大小限制。同樣地,相同屬性可以有多個值的實體必須為每個值儲存不同的索引項目;同理,如果可能值的數字太大,便可能超過項目限制。

如果實體含有多個屬性,而每個屬性都有多個值,則情況會變得更嚴重。為了適應這類實體,索引必須包含每個可能屬性值「組合」的項目。如果自訂索引參照多個屬性,且每個屬性有多個值,只要實體有大量項目及相對較少的可能屬性值,便可能在組合的情況下「爆炸」。由於有大量必須更新的索引項目,這類「爆炸式索引」可能會大幅提高在 Cloud Datastore 寫入實體的費用,也能夠輕易使實體超過索引項目或大小上限。

舉例來說,查詢:

Java 8

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

Java 7

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 建議下列索引:

Java 8

<?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>

Java 7

<?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 的實體相關聯的值數量。例如,下列程式碼

Java 8

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

Java 7

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 屬性有四個值,y 屬性有三個值,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>)

當相同的屬性重複數次後,Cloud Datastore 便能偵測到爆炸式索引,並建議替代的索引。然而,在所有其他情況下 (例如本範例定義的查詢),Cloud Datastore 仍會產生爆炸式索引。在此情況下,您可以手動在索引設定檔中設定索引以避免此情形:

Java 8

<?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>

Java 7

<?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>
這樣能將需要的項目減少為 7 個 ((|x| * |date| + |y| * |date|)),而不需要 12 個:

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

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

會造成索引超過索引項目或大小上限的 put 作業都會失敗,並顯示 IllegalArgumentException。例外情況的文字會說明超出了哪一種上限 ("Too many indexed properties""Index entries too large"),以及發生問題的自訂索引。如果您建立的新索引在建構時超出任何實體上限,則對該索引執行的查詢將會失敗,且該索引會在 GCP 主控台中顯示 Error 狀態。如要解決 Error 狀態的索引:

  1. datastore-indexes.xml 檔案移除 Error 狀態的索引。

  2. datastore-indexes.xml 所在的目錄執行下列指令,以便從 Cloud Datastore 中移除該索引:

    appcfg.sh vacuum_indexes [YOUR_APP_DIR]
    
  3. 解決錯誤原因。例如:

    • 重新制定索引定義和對應查詢。
    • 移除導致索引爆炸的實體。
  4. 將索引加回 datastore-indexes.xml 檔案。

  5. datastore-indexes.xml 所在的目錄執行下列指令,以便在 Cloud Datastore 中建立索引:

    appcfg.sh update_indexes datastore-indexes.xml
    

若要避免爆炸式索引,可以使用清單屬性避開需要自訂索引的查詢。如上所述,其中包括含多種排序順序的查詢,或是混用等式及不等式篩選條件的查詢。

本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Java 8 適用的 App Engine 標準環境