My favorites | 中文(简体) | Sign in

查询和索引

每个数据存储区查询都使用一个索引,即包含按指定顺序排列的查询结果的表格。App Engine 应用程序会在一个名为 datastore-indexes.xml 的配置文件中定义其索引。 开发网络服务器在遇到未配置索引的查询时会自动为该文件生成建议。

基于索引的查询机制支持大多数常见查询类型,但不支持您可能惯用的来自其他数据库技术的一些查询。以下描述了对查询的限制及其对此所做的说明。

引入查询

查询从数据存储区中检索满足一组条件的实体。查询会指定一种实体、基于属性值的零个或多个条件(有时称作“过滤器”)以及零个或多个排序顺序描述。执行查询时,它会抓取指定类型中满足所有指定条件并按照描述的顺序排序的全部实体。

JDO 可针对满足特定条件的实体执行查询。您还可使用 JDO Extent 来表示某种类型每个实体的集合(某个类的每个存储对象)。

使用 JDOQL 的查询

JDO 包含一种查询语言,用于检索满足一组条件的对象。该语言名为 JDOQL,可直接引用 JDO 数据类和字段,并包括对查询参数和结果的类型检查。JDOQL 与 SQL 类似,但是更适合面向对象的数据库(如 App Engine 数据存储区)。(App Engine 数据存储区不支持对 JDO 接口的 SQL 查询)

查询 API 支持多种调用样式。可使用 JDOQL 字符串语法在字符串中指定一个完整的查询。还可通过对查询对象调用方法来指定查询的一部分或全部。

以下是一个使用方法调用样式的简单查询示例,带有一个过滤条件和一种排序顺序,对过滤条件中使用的值使用参数替换。查询对象的 execute() 方法和要在查询中替换的值按照其声明的顺序一起调用。

import java.util.List;
import javax.jdo.Query;

// ...

    Query query = pm.newQuery(Employee.class);
    query.setFilter("lastName == lastNameParam");
    query.setOrdering("hireDate desc");
    query.declareParameters("String lastNameParam");

    try {
        List<Employee> results = (List<Employee>) query.execute("Smith");
        if (results.iterator().hasNext()) {
            for (Employee e : results) {
                // ...
            }
        } else {
            // ... no results ...
        }
    } finally {
        query.closeAll();
    }

下面是使用字符串语法的相同查询:

    Query query = pm.newQuery("select from Employee " +
                              "where lastName == lastNameParam " +
                              "order by hireDate desc " +
                              "parameters String lastNameParam")

    List<Employee> results = (List<Employee>) query.execute("Smith");

可混合这些定义查询的样式。例如:

    Query query = pm.newQuery(Employee.class,
                              "lastName == lastNameParam order by hireDate desc");
    query.declareParameters("String lastNameParam");

    List<Employee> results = (List<Employee>) query.execute("Smith");

可通过多次调用 execute() 方法,使用不同的值替换参数来重用单个 Query 实例。每次调用都将执行查询,并以集合的形式返回结果。

JDOQL 字符串语法对字符串值和数字值支持字符串中的值字面量。用单引号 (') 或双引号 (") 将字符串引起。所有其他值类型必须使用参数替换。以下是使用字符串字面值的示例:

    Query query = pm.newQuery(Employee.class,
                              "lastName == 'Smith' order by hireDate desc");

查询过滤条件

过滤条件指定字段名称、运算符和值。值必须由应用程序提供;它不能引用其他属性或按照其它属性计算。运算符可以是任意以下运算符:< <= == >= >

注意:Java 数据存储区接口不支持在 Python 数据存储区接口中实现的 !=IN 过滤条件运算符。(在 Python 接口中,这些运算符是在客户端的库中作为多数据存储区查询实现的,它们并非数据存储区本身的功能。)

过滤条件的主题可以是任意对象字段,包括主键和父实体组(请参阅事务)。

实体必须匹配所有的过滤条件才可成为结果。在 JDOQL 字符串语法中,用 &&(逻辑“与”)分隔指定多个过滤条件。不支持过滤条件的其他逻辑组合(逻辑“或”、“非”)。

由于 App Engine 数据存储区执行查询的方式,单个查询无法对多个属性使用不等过滤条件 (< <= >= >)。但允许对同一属性使用多个不等过滤条件(如查询值范围)。请参阅查询限制

    query.setFilter("lastName == 'Smith' && hireDate > hireDateMinimum");
    query.declareParameters("Date hireDateMinimum");

查询排序顺序

排序顺序可指定属性和方向(升序或降序)。结果返回时将以给定的顺序排序(按照它们指定的顺序)。如果没有为查询指定排序顺序,则结果将按照其实体键排序。

由于 App Engine 数据存储区执行查询的方式,如果查询对一个属性指定了不等过滤条件并对其他属性指定了排序顺序,则使用不等过滤条件的属性必须在其他属性之前排序。请参阅查询限制

    query.setOrdering("hireDate desc, firstName asc");

查询范围

查询可指定要返回到应用程序的结果的范围。范围可指定在完整结果集中哪个结果应最先返回,哪个应最后返回(使用数字索引,第一个结果从 0 开始)。例如,范围 5, 10 将返回第 6、7、8、9、10 个结果。

起始偏移具有性能含义:数据存储区必须检索然后丢弃起始偏移前的所有结果。例如,范围为 5, 10 的查询将先从数据存储区抓取 10 个结果,然后丢弃前 5 个并将剩余的 5 个返回到应用程序。

    query.setRange(5, 10);

扩展

JDO Extent 表示特定类在数据存储区中的每个对象。

可使用 PersistenceManager 的 getExtent() 方法(向其传递数据类)开始 Extent。Extent 类可实现用于访问结果的 Iterable 接口。在完成访问结果后,可调用 closeAll() 方法。

以下示例循环访问数据存储区中的每个 Employee 对象:

import java.util.Iterator;
import javax.jdo.Extent;

// ...

    Extent extent = pm.getExtent(Employee.class, false);
    for (Employee e : extent) {
        // ...
    }
    extent.closeAll();

扩展可批量检索结果,并可超出应用到查询的 1000 个结果的限制。

引入索引

App Engine 数据存储区会为应用程序要进行的每个查询都保留一个索引。当应用程序对数据存储区实体做出更改时,数据存储区会使用正确的结果更新索引。当应用程序执行查询时,数据存储区会直接从相应的索引中抓取结果。

应用程序对查询中使用的每个类型、过滤器属性和操作符以及排序顺序的组合都具有一个索引。请考虑 JDOQL 中所述的示例查询:

select from Person where lastName == "Smith"
                      && height < 72
                order by height desc

该查询的索引是 Person 类型实体的键表,其中包括 heightlastName 属性的值列。该索引按照 height 的降序排序。

形式相同但过滤器值不同的两个查询会使用相同的索引。例如,下面的查询与上面的查询使用相同的索引:

select from Person where lastName == "Jones"
                      && height < 64
                order by height desc

数据存储区按照以下步骤执行查询:

  1. 数据存储区会标识符合查询的种类、过滤器属性、过滤器操作符和排序顺序的索引。
  2. 数据存储区会使用该查询的过滤器值在满足全部过滤器条件的第一个实体处开始扫描该索引。
  3. 数据存储区继续扫描索引,同时返回每个实体,直到发现不满足过滤条件的下一个实体,直到达到索引末尾,或者直到已收集了查询所请求的最多结果。

索引表包含在过滤器或排序顺序中使用的每个属性列。行的排序会按照以下几个方面进行:

  • 祖先
  • 在等式过滤条件中使用的属性值
  • 在不等式过滤器中使用的属性值
  • 在排序顺序中使用的属性值

这可将使用该索引的每项可能查询的所有结果以连续行的形式排在表格中。

该机制可支持许多查询,且适用于大部分应用程序。然而,该机制不支持您可能惯用的其他数据库技术中的一些查询类型。

Query 永远不会返回没有已过滤属性的实体

索引仅包含其每个属性都由该索引引用的实体。如果实体没有由某索引引用的属性,那么该实体将不会显示在该索引中,且永远不会成为使用该索引的查询的结果。

请注意,App Engine 数据存储区区分不具有属性的实体和具有带空值 (null) 的属性的实体。如果您希望某个种类的每个实体都成为查询的潜在结果,则可以使用 JDO 或 JPA 数据类,这些类始终将值赋给与类中的字段相对应的每个属性 。

Text 和 Blob 值未编入索引

索引中不包含具有长文本值类型 (Text) 或长二进制值类型 (Blob) 值的属性,所以不能通过查询查找。

不将这些属性值编入索引的结果是,属性上带有过滤器或排序顺序的查询将永远不会匹配属性值为 Text 或 Blob 的实体。带有此类值的属性会表现得如同未对该属性的查询过滤器和排序顺序进行设置。

混合类型的属性值按类型排序

如果两个实体所具有的属性名称相同而值类型不同,那么属性的索引首先按值类型对实体排序,然后按适合该类型的顺序排序。例如,如果两个实体所具有的属性名称均为“年龄”,其中一个具有整数值,另一个具有字符串值,则按“年龄”属性排序时,不管值本身是多少,具有整数值的实体将始终出现在具有字符串值的实体之前。

对于整数和浮点数更值得注意这一点,因为数据存储区会将它们视为彼此独立的类型。具有整数值 38 的属性排在具有浮点值 37.5 的属性之前,因为所有的整数都排在浮点数之前。

(如果使用的是 JDO 或 JPA,则不会出现这种情况,除非您修改字段类型而不同时在数据存储区中更新现有的实体,或者使用低级别的数据存储区 API 或非 Java API。)

用配置定义索引

App Engine 在默认情况下为许多简单查询创建索引。对于其他查询,应用程序必须在一个名为 datastore-indexes.xml 的配置文件中指定所需的索引。如果在 App Engine 下运行的应用程序试图执行一个无相应索引(默认情况下提供或在 datastore-indexes.xml 中描述)的查询,该查询会失败。

App Engine 会为以下形式的查询提供自动索引:

  • 只使用等式和祖先过滤条件的查询
  • 只使用不等式过滤条件(只能具有单个属性)的查询
  • 只有一种排序顺序的属性查询(要么升序,要么降序)

其他形式的查询需要在 datastore-indexes.xml 中指定其索引,这类查询包括:

  • 有多种排序顺序的查询
  • 对键的降序查询
  • 在一个属性上具有一个或多个不等式过滤条件,以及在其他属性上具有一个或多个等式过滤条件的查询
  • 具有不等式过滤条件和祖先过滤条件的查询

开发网络服务器使得管理索引配置很容易:开发网络服务器可以生成索引的配置从而使查询成功,而不会无法执行没有索引但又需要索引的查询。如果对您的应用程序进行的本地测试调用该应用程序可能执行的每个查询(祖先、过滤条件和排序顺序的每种组合),则生成的条目便表示一组完整的索引。如果您的测试不会应用每种可能的查询形式,您可以在上传该应用程序之前审阅和调整索引配置。

可使用名为 datastore-indexes.xml 的配置文件(位于您的应用程序 WAR 的 WEB-INF/ 目录中)手动定义索引。如果您启用了自动索引配置(参见下文),则开发服务器将在 WEB-INF/appengine-generated/ 目录下名为 datastore-indexes-auto.xml 的文件中创建索引配置,并使用两个文件确定索引的完整集。

请再次考虑以下示例查询:

select from Person where lastName = 'Smith'
                      && height < 72
                   order by height desc

该查询所需的索引配置将显示在 datastore-indexes.xml 中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes
  xmlns="http://appengine.google.com/ns/datastore-indexes/1.0"
  autoGenerate="true">
    <datastore-index kind="Person" ancestor="false">
        <property name="lastName" direction="asc" />
        <property name="height" direction="desc" />
    </datastore-index>
</datastore-indexes>

如果 datastore-indexes.xml 中的 <datastore-indexes> 元素具有 autoGenerate="true" 属性(同上),或应用程序没有 datastore-indexes.xml 文件,则自动索引配置已启用。自动索引配置启用后,如果应用程序在开发服务器中执行该查询且不存在索引配置,则服务器会将该索引配置添加到 datastore-indexes-auto.xml 文件。

有关 datastore-indexes.xmldatastore-indexes-auto.xml 的详细信息,请参阅 Java 数据存储区索引配置

对键的查询

实体键可作为查询过滤条件或排序顺序的主题。在 JDO 中,您将使用对象的主键字段在查询中引用实体键。数据存储区将对这类查询考虑完整键值,包括实体的父实体路径、类型以及应用程序分配的键名称字符串或系统分配的数字 ID。

由于实体键在系统中的所有实体间唯一,键查询可使批量检索给定类型的实体(例如批量转储数据存储区内容)变得容易。与 JDOQL 范围不同,它对任意数量的实体都可高效地工作。

健首先按父路径排序,再按种类,然后按键名或 ID。种类和键名是字符串,按字节值排序。ID 是整数,按数字顺序排序。如果具有相同父实体和种类的实体同时使用键名字符串和数字 ID,则将带有数字 ID 的实体视为少于带有键名字符串的实体。对父路径的元素进行类似的比较:根据种类(字符串),然后根据键名(字符串)或 ID(数字)。

涉及键的查询与涉及属性的查询使用索引的情况相似,只有一个小小的差别:与使用属性的查询不同的是,使用等式过滤条件对键的查询还使用其他过滤条件,必须使用在应用程序的索引配置文件中定义的自定义索引。和所有查询一样,当对需要自定义索引的查询进行测试时,开发网络服务器在此文件中创建合适的配置条目。

对查询的限制

索引查询机制的本质是对查询功能强加一些限制。

对一个属性进行过滤或排序需要确认该属性确实存在

属性的查询过滤条件或排序顺序也暗含了一个条件,即实体必须具有该属性的值。

数据存储区实体不需具有其他同类实体所具有的属性值。属性上的过滤器只能与具有该属性的值的实体相匹配。过滤器或排序顺序中所使用的不具有属性值的实体会从为该查询创建的索引中删除。

没有可与不具有属性的实体相匹配的过滤器

无法为缺少指定属性的实体执行查询。一种解决方法是创建一个固定的(已建模的)属性,默认值为 null,然后为实体创建一个过滤条件,属性值为 null

不等式过滤条件只能用于一个属性

查询在其所有过滤器中只能在一个属性上使用不等式过滤条件(<<=>=>)。

例如,以下查询是允许使用的:

select from Person where birthYear >= minBirthYearParam
                      && birthYear <= maxBirthYearParam

而以下查询是不允许使用,因为此查询在同一个查询中的两个不同属性上使用了不等式过滤条件:

select from Person where birthYear >= minBirthYearParam
                      && height >= minHeightParam   // ERROR

过滤器可合并同一查询(包括属性上带有一个或多个不等式条件的查询)中不同属性的同级 (==) 比较。允许执行此操作:

select from Person where lastName == lastNameParam
                      && city == cityParam
                      && birthYear >= minBirthYearParam

查询机制基于将查询的所有结果彼此相邻地排列在索引表中,以避免为查找结果而不得不扫描整个整个表。在保持所有结果连续出现在表中的同时,单个索引表无法表示用于多个属性上的多个不等式过滤器。

必须在采用其他排序顺序之前对不等式过滤器中的属性进行排序

如果查询具有带不等式比较以及带一个或多个排序顺序的两种过滤器,那么,该查询必须包含在该不等式中使用的属性的排序顺序,且该排序顺序必须出现在其他属性上的排序顺序之前。

以下查询无效,因为此查询使用了不等式过滤条件,且未根据已过滤的属性进行排序:

select from Person where birthYear >= minBirthYearParam
                order by lastName                    // ERROR

同样,以下查询无效,因为它在根据其他属性排序之前未根据已过滤的属性进行排序:

select from Person where birthYear > minBirthYearParam
                order by lastName, birthYear         // ERROR

以下查询有效:

select from Person where birthYear >= minBirthYearParam
                order by birthYear, lastName

要获得与不等式过滤条件匹配的所有结果,查询会扫描索引表寻找第一个匹配的行,然后返回所有连续的匹配结果,直到找到不匹配的行为止。表示完整结果集的连续行必须先按照不等式过滤条件排序,然后再按其他顺序排序。

排序顺序和具有多个值的属性

鉴于将具有多个值的属性编入索引的方式,这些属性的排序顺序不平常:

  • 如果实体按照多值属性以升序排列,则用于排序的值会成为最小的值。
  • 如果实体按照多值属性以降序排列,则用于排序的值会成为最大的值。
  • 其他值不影响排序顺序,值的数目也不影响。
  • 就链而言,该实体的键被用作该链的断路器。

此排序顺序引起异常结果:以升序和降序排序时 [1, 9] 都显示在 [4, 5, 6, 7] 之前。

需要特别注意对于多值属性既具有等式过滤条件又具有排序顺序的查询。在这些查询中,排序顺序将被忽略。对于单值属性,这是一个简单的优化。对于属性来说,每个结果都具有相同的值,因此,不需要对结果进一步排序。

然而,多值属性可能具有其他的值。由于忽略了排序顺序,与应用了排序顺序时相比,可能会以一个不同于前者顺序的顺序返回查询结果。(恢复已放弃的排序顺序会花费巨大并需要额外的标记,又因为这种使用情况很少出现,因此查询计划员禁止此项操作。)

大的实体和分解索引

如上所述,会把每个实体的每个属性(不具有 Text 或 Blob 值)添加到至少一个索引表(包含默认情况下提供的简单索引和在引用该属性的应用程序的 文件中所描述的任何索引)datastore-indexes.xml 中。如果实体的每个属性都有一个值,App Engine 会将属性值在简单索引中存储一次,并且在每次引用属性时在自定义索引中存储一次。每次更改该属性的值时,这些索引条目中的每一个都必须进行更新,因此,引用该属性的索引越多,更新该属性所需要的时间就越长。

为防止更新实体花费过多时间,数据存储区会限制单个实体可具有的索引条目的数量。该限制涉及的范围广大,且使用大部分应用程序时将不会引起用户的注意。然而,有些情况下您可能会遇到该限制。例如,具有许多单一值属性的实体可能会超出索引条目的限制。

具有多个值的属性将每个值作为单独的条目存储在索引中。当单个属性具有许多值时,具有该单个属性的实体可以超出索引条目的限制。

当自定义索引引用多个多值属性时,即使仅有几个值,容量也会很大。要完整记录这类属性,索引表必须包含一行,该行内容为该索引每个属性的值的所有排列。

例如,以下索引(用 datastore-indexes.xml 语法描述)包括类型为 MyModel 的实体的 xy 属性:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes>
    <datastore-index kind="MyModel">
        <property name="x" direction="asc" />
        <property name="y" direction="asc" />
    </datastore-index>
</datastore-indexes>

以下代码会创建分别具有属性 x 和属性 y 的 2 个值的实体:

        MyModel m = new MyModel();

        m.setX(Arrays.asList("one", "two"));
        m.setY(Arrays.asList("three", "four"));

        pm.makePersistent(m);

为了正确表示这些值,该索引必须存储 12 个属性值:xy 上的每个内置索引各 2 个,自定义索引中 xy 的 4 项排列中每项各 1 个。如果存在许多多值属性的值,这可能意味着一个索引必须存储单个实体的许多索引条目。您可以将引用多个多值属性的索引称作“爆炸式索引”,因为该类索引即使仅有几个值,容量也会很大。

如果 put() 会导致产生大量超出限制的索引条目,那么调用会失败,并引发异常。如果您创建一个包含大量索引条目的新索引,且这些索引条目超出了对所创建的任何实体的限制,那么针对该索引的查询会失败,并且该索引会以“错误”的状态显示在管理控制台中。

您可以通过避免需要自定义索引的查询使用列表属性来避免分解索引。如上所述,这包括具有多种排序顺序的查询、使用等式和不等式过滤条件的查询以及使用祖先过滤条件的查询。