数据存储区索引

App Engine 会为实体的每个属性预定义简单索引。App Engine 应用可以在名为 datastore-indexes.xml索引配置文件中定义更多的自定义索引,该文件在应用的 /war/WEB-INF/appengine-generated 目录中生成。开发服务器遇到无法使用现有索引执行的查询时,会自动向此文件添加建议。 在上传应用之前,可以通过编辑该文件来手动优化索引。

注意:基于索引的查询机制支持多种查询,适用于大多数应用,但它不支持其他数据库技术中常见的某些种类的查询:特别是 Datastore 查询引擎不支持联接和聚合查询。如需了解 Datastore 查询的相关限制,请参阅 Datastore 查询页面。

索引定义和结构

索引在给定实体种类的属性列表上定义,每个属性具有相应的顺序(升序或降序)。为了与祖先查询一起使用,索引还可以选择包括实体的祖先实体。

索引定义中所指定的每个属性在索引表中都有一个对应的列。表中的每一行都表示 Datastore 中的一个实体,即根据索引进行查询时,可能会出现在结果中的一个实体。仅在实体对索引中使用的每个属性均设置了已编制索引的值时,该实体才可包含在索引中;如果索引定义所指的属性在实体中没有值,则该实体不出现在索引中,因此基于索引的任何查询均不返回该实体。

注意:Datastore 会区分不具有某个属性的实体与该属性为 Null 值 (null) 的实体。如果您明确向某实体的属性分配了 null 值,则引用该属性的查询结果中也可能会包含该实体。

注意:包括多个属性的索引要求不得将各个单独的属性设置成未编入索引

索引表的行按索引定义中指定的顺序,先按祖先实体再按属性值进行排序。查询的完美索引可让查询最有效地执行,完美索引在以下属性上依下列顺序定义:

  1. 相等性过滤条件中使用的属性
  2. 不等性过滤条件中使用的属性(不超过一个属性
  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>

形式相同但过滤条件值不同的两个查询使用同一个索引。例如,以下查询使用与上述查询相同的索引:

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

虽然下面两个查询的形式不同,但两者也使用同一索引:

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 中的实体属性进行关联的方式:

具有混合值类型的属性

如果两个实体具有名称相同但值类型不同的属性,则这些属性的索引将依次按照值类型和适合每种类型的二次排序方式对实体进行排序。例如,如果两个实体都具有一个名为 age 的属性,一个属性的值为整数,另一个属性的值为字符串,则在按 age 属性排序时,无论属性值本身是何值,属性值为整数的实体始终排在属性值为字符串的实体前面。

在两个属性值分别为整数和浮点数的情况下,这一点尤其值得注意,因为 Datastore 会将它们视为不同的类型。由于所有整数都排在所有浮点数的前面,因此具有整数值 38 的属性排在具有浮点值 37.5 的属性的前面。

不编入索引的属性

如果您知道永远不需要对特定属性进行过滤或排序,则可以通过将该属性声明为“unindexed”,指示 Datastore 不为该属性保留索引条目。这可以减少必须执行的 Datastore 写入次数,从而降低应用的运行费用。具有“不编入索引”属性的实体表现如同未设置该属性一样:对不编入索引的属性使用过滤条件或排列顺序的查询永远不与该实体匹配。

注意:如果某属性出现在由多个属性构成的索引中,则将其设置为不编入索引,会导致无法将它编入复合索引。

例如,假设某实体具有 aa 属性,且您希望创建能够满足 WHERE a ="bike" and b="red" 这样的查询的索引。另外,假设您并不需要 WHERE a="bike"WHERE b="red" 查询的结果。如果您将 a 设置为不编入索引,并创建 aa 的索引,则 Datastore 不会为 aa 的索引创建索引条目,因此 WHERE a="bike" and b="red" 查询也就无法运行。如需让 Datastore 为 aa 的索引创建条目,必须将 aa 编入索引。

在低层级 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. 在 Datastore 中检索(获取)实体。
  2. 将实体写(放)回 Datastore。

同样,将属性从“编入索引”状态更改为“不编入索引”仅影响之后写入 Datastore 的实体。任何具有该属性的现有实体的索引条目将继续存在,直到这些实体更新或删除。为避免不需要的结果,您必须完全清除按照(现不编入索引)属性进行过滤或排序的所有查询的代码。

索引限制

Datastore 会限制可与单个实体关联的索引条目的数量和总体大小。这些限制很松,不影响大部分应用。但在某些情况下,您可能会受到限制。

上文所述,Datastore 在预定义索引中为每个实体的每个属性(长文本字符串 (Text)、长字节字符串 (Blob)、嵌入式实体 (EmbeddedEntity) 以及已明确声明为不编入索引的属性除外)创建了一个条目。属性还可能包含在 datastore-indexes.xml 配置文件中声明的其他自定义索引中。如果实体没有列表属性,则它最多能在每个此类自定义索引(针对非祖先索引)或实体的每个祖先实体(针对祖先索引)中具有一个条目。每当属性值发生更改时,则必须更新上述每个索引条目。

对于每个实体都有一个值的属性,每个可能存在的值只需针对每个实体,在属性的预定义索引中存储一次。即使如此,具有大量这种单值属性的实体也有可能超过索引条目限制或大小限制。同样,实体的同一属性可能具有多个值,这要求每个值都有一个单独的索引条目;如果可能存在大量的值,则这种实体可能超过条目限制。

如果实体具有多个属性,每个属性具有多个值,则情况更加糟糕。为了容纳这种实体,对每个可能的属性值组合,索引都必须包含一个对应的条目。如果自定义索引引用了具有多个值的多个属性,则自定义索引可能会通过组合爆炸式增长,这需要将大量条目对应于仅具有相对较少数量的可能属性值的实体。由于必须更新大量索引条目,因此这种爆炸式索引会大幅增加将实体写入 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 属性具有四个值,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>)

如果同一属性多次重复,则 Datastore 可检测到爆炸索引并建议一个替代索引。但是,在其他所有情况下(例如本示例中定义的查询),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>
此配置将所需条目数更改为仅有 (|x| * |date| + |y| * |date|) 个,即将 12 个条目变成了 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
    

通过使用列表属性避免需要自定义索引的查询,您可以避免分解索引。如上所述,具有多个排序顺序的查询或同时具有等式过滤条件和不等式过滤条件的查询都属于此类查询。