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

查询和索引

每个数据库查询使用一个索引,即一个包含按照所需顺序排列的查询结果的表格。App Engine 应用程序会在一个名为 index.yaml 的配置文件中定义其索引。开发网络服务器在遇到未配置索引的查询时会自动为该文件添加建议。您可以通过在上传该应用程序之前编辑该文件来手动调整索引。

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

引入查询

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

数据库 API 提供两种接口来准备和执行查询:Query 接口(使用不同方法准备查询)和 GqlQuery 接口(使用一种称为 GQL 的类似 SQL 的查询语言从查询字符串准备查询)。在创建、获取和删除数据:使用 Query 获取实体以及相应的参考页面中更详细地介绍了这些接口。

class Person(db.Model):
  first_name = db.StringProperty()
  last_name = db.StringProperty()
  city = db.StringProperty()
  birth_year = db.IntegerProperty()
  height = db.IntegerProperty()

# The Query interface prepares a query using instance methods.
q = Person.all()
q.filter("last_name =", "Smith")
q.filter("height <", 72)
q.order("-height")

# The GqlQuery interface prepares a query using a GQL query string.
q = db.GqlQuery("SELECT * FROM Person " + 
                "WHERE last_name = :1 AND height < :2 " +
                "ORDER BY height DESC",
                "Smith", 72)

# The query is not executed until results are accessed.
results = q.fetch(5)
for p in results:
  print "%s %s, %d inches tall" % (p.first_name, p.last_name, p.height)

引入索引

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

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

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                     ORDER BY height DESC

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

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

SELECT * FROM Person WHERE last_name = "Jones"
                       AND height < 63
                     ORDER BY height DESC

数据库按照以下步骤执行查询:

  1. 数据库会标识符合查询的种类、过滤器属性、过滤器操作符和排序顺序的索引。
  2. 数据库会使用该查询的过滤器值在满足全部过滤器条件的第一个实体处开始扫描该索引。
  3. 数据库会继续扫描该索引并返回每个实体,直到发现下一个不满足过滤器条件的实体或到达到该索引末尾。

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

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

注意:要发挥索引的作用,可像处理 = 过滤器一样处理 IN 过滤器,像处理其他不等式过滤器一样处理 != 过滤器。

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

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

提示:查询过滤器没有一种明确的方法来与部分字符串值相匹配,然而您可以使用不等式过滤器

db.GqlQuery("SELECT * FROM MyModel WHERE prop >= :1 AND prop < :2", "abc", u"abc" + u"\xEF\xBF\xBD")

仿造一个前缀匹配。这样可以匹配字符串属性 propabc 字符开头的每个 MyModel 实体。字节字符串 "\xEF\xBF\xBD" 表示可能存在的最大 Unicode 字符。当属性值在索引中进行排序时,属于此范围的值是以指定的前缀开头的所有值。

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

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

请注意,App Engine 数据库区分不具有属性的实体和具有带空值(在 Python 中为 None)的属性的实体。如果您希望一类实体中的每个实体都成为可通过查询可搜索到的结果,则可以使用数据模型将一个默认值(例如 None)分配到过滤器在查询中所使用的属性。

Text 和 Blob 值未编入索引

具有 TextBlob(例如具有 TextPropertyBlobProperty 模型)类型的值的属性不包含在索引中,因此,无法通过查询找到这些属性。要对短字符串值进行过滤,请使用字符串或 Unicode 值(StringProperty 模型)。

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

用 index.yaml 定义索引

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

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

  • 只使用等式、IN 和祖先过滤器的查询
  • 只使用不等式过滤器(只能属于单个属性)的查询
  • 只有一种排序顺序(升序)的查询

其他形式的查询需要在 index.yaml 中指定其索引,这类查询包括:

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

开发网络服务器 (dev_appserver.py) 可以让您易于管理 index.yaml:以前无法执行没有索引配置且需要进行索引配置的查询,而开发网络服务器可向文件添加一个索引定义,这样就可执行这种查询了。

如果对您的应用程序进行的本地测试调用该应用程序可能执行的每个查询(祖先、过滤器和排序顺序的每种组合),则生成的条目会表示一组完整的索引。如果您的测试不会应用每种可能的查询形式,您可以在上传该应用程序之前审阅和调整文件中的索引定义。

提示:如果要以 --require_indexes 选项启动 dev_appserver.py,则会禁止生成 index.yaml,并且需要不存在的索引配置的查询会抛出错误。请使用此选项测试您的应用程序以验证是否所有必需的索引配置都存在。

index.yaml 描述了每个索引表,包括类型、查询过滤器和排序顺序所需的属性以及该查询是否使用了祖先子句(Query.ancestor() 子句或 GQL ANCESTOR IS 子句)。属性会按照将被排序的顺序列出:首先是在等式或 IN 过滤器中使用的属性,接着是在不等式过滤器中使用的属性,最后是查询结果排序顺序及其方向。

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

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                     ORDER BY height DESC

如果应用程序仅执行此查询(也可能是类似此查询但具有 "Smith"72 不同的值的其他查询),那么,index.yaml 文件会以以下形式显示:

indexes:
- kind: Person
  properties:
  - name: last_name
  - name: height
    direction: desc

当创建或更新了一个实体时,每个相应的索引也进行更新。应用到实体的索引数量会影响创建或更新该实体所需要的时间。

有关 index.yaml 语法的详细信息,请参阅配置索引

对查询的限制

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

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

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

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

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

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

只允许在一个属性上使用不等式过滤器

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

例如,允许使用此 GQL 查询:

SELECT * FROM Person WHERE birth_year >= :min
                       AND birth_year <= :max

然而,不允许使用此 GQL 查询,因为此 GQL 查询在同一个查询中的两个不同的属性上使用了不等式过滤器:

SELECT * FROM Person WHERE birth_year >= :min_year
                       AND height >= :min_height     # ERROR

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

SELECT * FROM Person WHERE last_name = :last_name
                       AND city = :city
                       AND birth_year >= :min_year

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

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

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

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

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name              # ERROR

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

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name, birth_year  # ERROR

此 GQL 查询有效:

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY birth_year, last_name

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

排序顺序和列表属性

由于为列表属性建立索引的方法的缘故,列表值的排序顺序异常:

  • 如果实体按照列表属性以升序排列,则进行排序所使用的值会成为该列表中最小的元素。
  • 如果实体按照列表属性以降序排列,则进行排序所使用的值会成为该列表中最大的元素。
  • 该列表中的其他元素不影响排序顺序,该列表的长度也不会影响排序顺序。
  • 就链而言,该实体的键被用作该链的断路器。

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

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

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

大的实体和分解索引

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

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

对于具有多个值的属性,例如使用列表值或 ListProperty 模型,则将每个值作为索引中单独的条目存储。当单个属性具有许多值(很长的一个列表)时,具有该单个属性的实体可以超出索引条目的限制。

引用带有多个值的多个属性的自定义索引可以仅带有几个值,但容量却很大。要完整记录这类属性,索引表必须包含该索引每个属性的值的每一项排列行。例如,以下索引(用 index.yaml 语法描述)包括类型为 MyModel 的实体的 xy 属性:

indexes:
- kind: MyModel
  properties:
  - name: x
  - name: y

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

class MyModel(db.Expando):
  pass

e2 = MyModel()
e2.x = ['red', 'blue']
e2.y = [1, 2]
e2.put()

为了正确表示这些值,该索引必须存储 8 个属性值:xy 上的每个内置索引各 2 个,自定义索引中 xy 的每项排列各 1 个。如果存在许多列表值,这可能意味着一个索引必须存储单个实体的许多索引条目。您可以将引用带有多个值的多个属性的索引称作 [分解索引],因为该类索引可以仅带有几个值,但容量却很大。

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

要处理 [错误] 索引,首先将这些索引从您的 index.yaml 文件中删除,并运行 appcfg.py vacuum_indexes。然后,重新制定索引定义以及相应的查询,或者删除导致该索引 [分解] 的实体。最后,将该索引添加回至 index.yaml 并运行 appcfg.py update_indexes

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