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

查询和索引

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

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

引入查询

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

数据存储区 Python 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)

过滤条件包括属性名称、比较运算符和值。如果实体具有给定名称的属性且其值与给定的值的比较满足运算符,则将通过过滤条件。如果实体通过了查询的所有过滤条件,它将成为该查询的结果。

过滤条件运算符可以是任意以下运算符:

  • < 小于
  • <= 小于或等于
  • = 等于
  • > 大于
  • >= 大于或等于
  • != 不等于
  • IN 等于给定列表中的任意值

!= 运算符实际上将执行 2 个查询:其中一个所有其他过滤条件都相同并将不等过滤条件替换为小于过滤条件,另一个将不等过滤条件替换为大于过滤条件。按顺序合并结果。如下文中关于不等过滤条件的讨论中所述,一个查询只能有一个不等过滤条件,这种查询不能有其他不等过滤条件。

IN 运算符也将执行多个查询,对给定列表中的每个项的值都执行一个查询,其中所有其他过滤条件相同而将 IN 过滤条件替换为等于过滤条件。按照项在列表中的顺序合并结果。如果一个查询不只有 IN 过滤条件,则该查询将作为多个查询执行,对 IN 过滤条件中的每个组合都执行一个查询。

包含 !=IN 运算符的单个查询限于 30 个子查询。

引入索引

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

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

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 过滤器,像处理其他不等式过滤器一样处理 != 过滤器。

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

提示:查询过滤条件没有明确的方法可以仅与一部分字符串值进行匹配,然而您可以使用以下不等式过滤条件仿造一个前缀匹配:

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

这样便可以将每个 MyModel 实体与以 abc 字符开头的字符串属性 prop 进行匹配。unicode 字符串 u"\ufffd" 表示可能存在的最大 Unicode 字符。当属性值在索引中进行排序时,属于此范围的值便是所有以指定的前缀开头的值。

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

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

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

请注意,App Engine 数据存储区区分不具有属性的实体和具有带空值 (None) 的属性的实体。如果您希望某个种类的每个实体都成为查询的潜在结果,则可以使用 将默认值(如 None)赋给查询过滤条件所使用的属性的数据模型 。

Text 和 Blob 值未编入索引

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

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

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

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

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

用配置定义索引

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

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

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

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

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

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

App Engine 在默认情况下为许多简单查询创建索引。对于其他查询,应用程序必须在一个名为 index.yaml 的配置文件中指定所需的索引。如果在 App Engine 下运行的应用程序试图执行一个无相应索引(默认情况下提供或在 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 语法的详细信息,请参阅配置索引

对键的查询

实体键可以是查询过滤条件或排序顺序的主题(使用特殊名称 __key__ 代替属性名称)。数据存储区将对这类查询考虑完整键值,包括实体的父实体路径、类型以及应用程序分配的键名称字符串或系统分配的数字 ID。

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

class MainHandler(webapp.RequestHandler):
  def get(self):
    query = Entity.gql('ORDER BY __key__')

    # Use a query parameter to keep track of the last key of the last
    # batch, to know where to start the next batch.
    last_key_str = self.request.get('last')
    if last_key_str:
      last_key = db.Key(last_key_str)
      query = Entity.gql('WHERE __key__ > :1 ORDER BY __key__', last_key)

    # For batches of 20, fetch 21, then use result #20 as the "last"
    # if there is a 21st.
    entities = query.fetch(21)
    new_last_key_str = None
    if len(entities) == 21:
      new_last_key_str = str(entities[19].key())

    # Return the data and new_last_key_str.  Client would use
    # http://...?last=new_last_key_str to fetch the next batch.
    # ...

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

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

对查询的限制

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

以下查询有效:

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

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

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

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

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

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

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

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

大的实体和分解索引

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

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

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

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

例如,以下索引(用 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()

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

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

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

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