索引

每个 Datastore 模式 Firestore 的查询都会使用一个或多个索引来计算结果,索引按索引属性指定的序列包含实体键和实体的祖先实体(可选)。如果应用对其实体进行了任何更改,索引会相应地更新,因此无需进一步计算即可提供所有查询的正确结果。

索引有两种类型:

内置索引
默认情况下,Datastore 模式数据库会自动为每个实体种类的每个属性预定义一个索引。这些单属性索引适用于简单类型的查询。
复合索引
复合索引根据每个已编入索引的实体的多个属性值编制索引。复合索引支持复杂查询,可在索引配置文件 (index.yaml) 中进行定义。

索引类型将在本主题的后面部分详细说明。

索引定义和结构

索引基于给定实体种类的属性列表定义,并按每个属性的相应顺序(升序或降序)排序。为了与祖先查询一起使用,索引还可包含实体的祖先实体(可选)。

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

索引表中的行按先祖先实体再属性值的次序以索引定义中指定的顺序进行排序。查询的完美索引(可让系统以最有效率的方式执行查询)按如下顺序根据以下属性定义

  1. 等式过滤条件中使用的属性
  2. 不等式过滤条件中使用的属性(不超过一个属性
  3. 排序顺序中使用的属性
  4. 投影中使用的属性(尚未包含在排序顺序中)

这可确保可能执行的每次查询的所有结果都显示在表内连续的行中。Datastore 模式数据库使用完美索引执行查询,具体步骤如下所示:

  1. 确定与查询种类、过滤条件属性、过滤条件运算符和排序顺序相对应的索引
  2. 从索引的开头开始扫描,直至找到符合所有查询过滤条件的第一个实体
  3. 继续扫描该索引,依次返回每个实体,直到
    • 遇到不符合过滤条件的实体,或者
    • 到达索引的末尾,或者
    • 已收集到查询所请求的结果数上限

例如,假设执行下面的查询:

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority < 3
ORDER BY priority DESC

此查询的完美索引是 Task 种类实体的键表,其列为 categorypriority 属性的值。索引先按 category 升序排序,再按 priority 降序排序:

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: desc

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

SELECT * FROM Task
WHERE category = 'Work'
  AND priority < 5
ORDER BY priority DESC

对于此索引

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: asc
  - name: created
    direction: asc

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

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority = 5
ORDER BY created ASC

SELECT * FROM Task
WHERE category = 'Work'
ORDER BY priority ASC, created ASC

上面创建的索引可同时满足这两个查询。

索引配置

Datastore 模式 Firestore 会为以下形式的查询提供内置(自动)索引

  • 仅使用祖先实体和键过滤条件的不分类查询
  • 仅使用祖先实体和等式过滤条件的查询
  • 仅使用不等式过滤条件的查询(限于单个属性
  • 仅使用祖先实体过滤条件、属性等式过滤条件和键不等式过滤条件的查询
  • 对属性不使用任何过滤条件且仅按照某个属性的一种排序顺序(升序或降序)进行排序的查询

例如,默认情况下,Datastore 模式数据库会自动为每个实体种类的各属性预定义两个单属性索引:一个按升序排列,另一个按降序排列。如果您不希望数据库保留某个属性的索引,请从索引中排除该属性。请注意,排除某个属性会将其从所有复合索引中移除。

内置索引足以执行许多简单的查询,例如纯等式查询和简单的不等式查询。

内置索引不在 Google Cloud Console 的“索引”页面中显示。

对于更复杂的查询,应用必须定义复合或手动索引。以下形式的查询需使用复合索引:

  • 具有祖先实体和不等式过滤条件的查询
  • 对某属性设定了一个或多个不等式过滤条件,且对其他属性设定了一个或多个等式过滤条件的查询
  • 具有基于键的降序排序顺序的查询
  • 具有多个排序顺序的查询
  • 具有一个或多个过滤条件和一个或多个排序顺序的查询

复合索引在应用的索引配置文件 (index.yaml) 中定义。(索引配置文件中没有内置索引。)

复合索引由多个属性构成,并要求每个单独的属性都不得从索引中排除

复合索引可在 Cloud Console 的“索引”页面中查看。您不能使用 Cloud Console 创建或更新复合索引。

如果应用尝试执行无法使用可用索引(无论是内置索引,还是索引配置文件中指定的索引)执行的查询,则该查询将失败。

Datastore 模式 API 会自动提供适合大多数应用的索引建议。您可能需要手动调整您的索引,具体取决于您的应用对 Datastore 模式数据库的使用情况以及数据的大小和形状。例如,编写具有多个属性值的实体可能会导致产生存储费用极高的爆炸式索引

Datastore 模拟器可帮助您更轻松地管理索引配置文件。在执行需要索引但没有索引的查询时,Datastore 模拟器不会使查询失败,而是会生成一个索引配置,使查询成功。如果应用的本地测试作业执行应用将会发出的每个可能查询(使用过滤条件和排序顺序的每种组合),则生成的条目将构成一个完整的索引集。如果您的测试不执行每种可能的查询形式,则可在更新索引之前查看并调整索引配置文件。

如需详细了解 index.yaml,请参阅索引配置

部署或删除索引

索引配置文件修改完毕后,请运行 gcloud datastore indexes create 命令以将索引部署到服务中。如需了解详情,请参阅更新索引

如果无需再使用之前部署的索引,可删除未使用的索引

索引和属性

下面是一些需牢记的特殊注意事项,它们针对的是索引及其与实体属性进行关联的方式:

具有混合值类型的属性

当两个实体具有名称相同但值类型不同的属性时,属性的索引首先按值类型排列实体,然后按照适合每种类型的次级排序排列实体。例如,如果两个实体都具有一个名为 age 的属性,一个属性的值为整数,另一个属性的值为字符串,则在按 age 属性排序时,无论属性值本身是何值,属性值为整数的实体始终排在属性值为字符串的实体前面。

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

排除的属性

如果您知道永远不需要对特定属性进行过滤或排序,则可以通过将该属性从索引中排除,指示 Datastore 模式数据库不保留其索引条目。这可减少索引条目所需的存储空间大小,进而降低应用的运行费用。具有已排除属性的实体表现如同未设置该属性一样:对已排除的属性应用过滤条件或排序顺序的查询永远不与该实体匹配。

在以下示例中,description 属性被排除在索引之外:

C#

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore C# API 参考文档

Entity task = new Entity()
{
    Key = _db.CreateKeyFactory("Task").CreateKey("sampleTask"),
    ["category"] = "Personal",
    ["created"] = new DateTime(1999, 01, 01, 0, 0, 0, DateTimeKind.Utc),
    ["done"] = false,
    ["priority"] = 4,
    ["percent_complete"] = 10.0,
    ["description"] = new Value()
    {
        StringValue = "Learn Cloud Datastore",
        ExcludeFromIndexes = true
    },
};

Go

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Go API 参考文档

type Task struct {
	Category        string
	Done            bool
	Priority        int
	Description     string `datastore:",noindex"`
	PercentComplete float64
	Created         time.Time
}
task := &Task{
	Category:        "Personal",
	Done:            false,
	Priority:        4,
	Description:     "Learn Cloud Datastore",
	PercentComplete: 10.0,
	Created:         time.Now(),
}

Java

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Java API 参考文档

Entity task = Entity.newBuilder(taskKey)
    .set("category", "Personal")
    .set("created", Timestamp.now())
    .set("done", false)
    .set("priority", 4)
    .set("percent_complete", 10.0)
    .set("description",
      StringValue.newBuilder("Learn Cloud Datastore").setExcludeFromIndexes(true).build())
    .build();

Node.js

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Node.js API 参考文档

const task = [
  {
    name: 'category',
    value: 'Personal',
  },
  {
    name: 'created',
    value: new Date(),
  },
  {
    name: 'done',
    value: false,
  },
  {
    name: 'priority',
    value: 4,
  },
  {
    name: 'percent_complete',
    value: 10.0,
  },
  {
    name: 'description',
    value: 'Learn Cloud Datastore',
    excludeFromIndexes: true,
  },
];

PHP

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore PHP API 参考文档

$task = $datastore->entity(
    $key,
    [
        'category' => 'Personal',
        'created' => new DateTime(),
        'done' => false,
        'priority' => 4,
        'percent_complete' => 10.0,
        'description' => 'Learn Cloud Datastore'
    ],
    ['excludeFromIndexes' => ['description']]
);

Python

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Python API 参考文档

task = datastore.Entity(
    key,
    exclude_from_indexes=['description'])
task.update({
    'category': 'Personal',
    'description': 'Learn Cloud Datastore',
    'created': datetime.datetime.utcnow(),
    'done': False,
    'priority': 4,
    'percent_complete': 10.5,
})

Ruby

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Ruby API 参考文档

task = datastore.entity "Task" do |t|
  t["category"] = "Personal"
  t["created"] = Time.now
  t["done"] = false
  t["priority"] = 4
  t["percent_complete"] = 10.0
  t["description"] = "Learn Cloud Datastore"
  t.exclude_from_indexes! "description", true
end

GQL

不适用

如果排除 description 属性,则以下示例中的查询不返回任何结果:

C#

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore C# API 参考文档

Query query = new Query("Task")
{
    Filter = Filter.Equal("description", "Learn Cloud Datastore")
};

Go

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Go API 参考文档

query := datastore.NewQuery("Tasks").Filter("Description =", "A task description")

Java

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Java API 参考文档

Query<Entity> query = Query.newEntityQueryBuilder()
    .setKind("Task")
    .setFilter(PropertyFilter.eq("description", "A task description"))
    .build();

Node.js

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Node.js API 参考文档

const query = datastore
  .createQuery('Task')
  .filter('description', '=', 'A task description.');

PHP

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore PHP API 参考文档

$query = $datastore->query()
    ->kind('Task')
    ->filter('description', '=', 'A task description.');

Python

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Python API 参考文档

query = client.query(kind='Task')
query.add_filter('description', '=', 'Learn Cloud Datastore')

Ruby

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Ruby API 参考文档

query = datastore.query("Task")
                 .where("description", "=", "A task description.")

GQL


# Will not return any results!
SELECT * FROM Task WHERE description = 'A task description.'

您稍后可将该属性重新编入索引。

但请注意,将属性从“已排除”状态更改为“编入索引”不会影响此项更改前可能已创建的任何现有实体。根据该属性过滤的查询不返回此类现有实体,因为这些实体在创建时未写入查询的索引中。要让实体可供将来的查询使用,您必须将其重新写入数据库,进而加入适当的索引中。也就是说,您必须对现有的每个此类实体执行以下操作:

  1. 查找(获取)实体。
  2. 将实体写回(放入)您的数据库中。

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

索引限制

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

上文所述,Datastore 模式数据库会在预定义索引中为每个实体的每个属性创建一个条目(已明确声明从索引中排除的属性除外)。属性还可能包含在索引配置文件 (index.yaml) 中声明的其他自定义索引中。如果某实体没有列表属性,则它最多能在每个此类自定义索引(针对非祖先索引)或实体的每个祖先实体(针对祖先索引)中拥有一个条目。每当属性值发生变化时,都必须更新上述每个索引条目。

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

如果实体具有多个属性,每个属性具有多个值,则情况更加糟糕。为了容纳这种实体,对每个可能的属性值组合,索引都必须包含一个对应的条目。如果自定义索引引用了具有多个值的多个属性,则自定义索引可能会通过组合爆炸式增长,这需要将大量条目对应于仅具有相对较少数量的可能属性值的实体。由于必须存储大量索引条目,因此这种爆炸式索引会大幅增加实体所占用的存储空间大小。爆炸式索引还容易导致实体超出索引条目数或大小的限制。

请参考以下代码:

C#

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore C# API 参考文档

Entity task = new Entity()
{
    Key = _db.CreateKeyFactory("Task").CreateKey("sampleTask"),
    ["tags"] = new ArrayValue() { Values = { "fun", "programming", "learn" } },
    ["collaborators"] = new ArrayValue() { Values = { "alice", "bob", "charlie" } },
    ["created"] = DateTime.UtcNow
};

Go

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Go API 参考文档

task := &Task{
	Tags:          []string{"fun", "programming", "learn"},
	Collaborators: []string{"alice", "bob", "charlie"},
	Created:       time.Now(),
}

Java

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Java API 参考文档

Entity task = Entity.newBuilder(taskKey)
    .set("tags", "fun", "programming", "learn")
    .set("collaborators", "alice", "bob", "charlie")
    .set("created", Timestamp.now())
    .build();

Node.js

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Node.js API 参考文档

const task = {
  method: 'insert',
  key: datastore.key('Task'),
  data: {
    tags: ['fun', 'programming', 'learn'],
    collaborators: ['alice', 'bob', 'charlie'],
    created: new Date(),
  },
};

PHP

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore PHP API 参考文档

$task = $datastore->entity(
    $datastore->key('Task'),
    [
        'tags' => ['fun', 'programming', 'learn'],
        'collaborators' => ['alice', 'bob', 'charlie'],
        'created' => new DateTime(),
    ]
);

Python

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Python API 参考文档

task = datastore.Entity(client.key('Task'))
task.update({
    'tags': [
        'fun',
        'programming',
        'learn'
    ],
    'collaborators': [
        'alice',
        'bob',
        'charlie'
    ],
    'created': datetime.datetime.utcnow()
})

Ruby

如需了解如何安装和使用 Cloud Datastore 客户端库,请参阅 Cloud Datastore 客户端库。 如需了解详情,请参阅 Cloud Datastore Ruby API 参考文档

task = datastore.entity "Task" do |t|
  t["tags"] = ["fun", "programming", "learn"]
  t["collaborators"] = ["alice", "bob", "charlie"]
  t["created"] = Time.now
end

GQL

不适用

以上代码会创建一个 Task 实体,其中 tags 属性具有三个值,collaborators 属性也具有三个值,并且 created 被设置为当前日期。这将需要 9 个索引条目,每个条目对应每个可能的属性值组合:

('fun', 'alice', NOW())
('fun', 'bob', NOW())
('fun', 'charlie', NOW())

('programming', 'alice', NOW())
('programming', 'bob', NOW())
('programming', 'charlie', NOW())

('learn', 'alice', NOW())
('learn', 'bob', NOW())
('learn', 'charlie', NOW())

如果同一属性多次重复,则 Datastore 模式 Firestore 可检测到爆炸式索引并推荐一个替代索引。但是,在其他所有情况下(例如本示例中定义的查询),Datastore 模式数据库将生成一个爆炸式索引。在这种情况下,您可以通过在索引配置文件中手动配置索引来避免产生爆炸式索引:

indexes:
- kind: Task
  properties:
  - name: tags
  - name: created
- kind: Task
  properties:
  - name: collaborators
  - name: created

此配置将所需条目数更改为仅有 (|tags| * |created| + |collaborators| * |created|) 个,即将 9 个条目变成了 6 个:

('fun', NOW())
('programming', NOW())
('learn', NOW())

('alice', NOW())
('bob', NOW())
('charlie', NOW())

任何会导致索引超过索引条目数或大小限制的 commit 操作都将失败。可在错误文本中查看超出了哪个限制("Too many indexed properties" 还是 "Index entries too large")以及哪个自定义索引导致错误。如果您创建了新索引,而此索引会在构建时超出任何实体的限制,则针对该索引的查询将失败,且该索引将在 Cloud Console 中显示为 Error 状态。要处理这种 Error 索引,请执行以下操作:

  1. 从索引配置文件 (index.yaml) 中移除该索引。
  2. gcloud 命令行工具中使用 vacuum_indexes 选项清空该索引(请参阅删除未使用的索引)。
  3. 执行以下任一操作
    • 更改索引定义及相应的查询,或
    • 移除导致索引爆炸式增长的实体。
  4. 重新将索引添加到 index.yaml
  5. gcloud 命令行工具中使用 update_indexes 选项更新该索引(请参阅更新索引)。

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

投影索引

投影查询要求投影中指定的所有属性都包含在索引中。Datastore 模拟器会在索引配置文件 index.yaml 中为您自动生成所需的索引,该文件会随应用一起上传。

要最大程度减少所需的索引数量,一种方式是始终投影相同的属性,即便并非总是需要所有这些属性。例如,以下查询需要两个单独的索引:

SELECT priority, percent_complete FROM Task

SELECT priority, percent_complete, created FROM Task

不过,尽管您始终投影属性 prioritypercent_completecreated(即使不需要 created),所需要的也只有一个索引。

如果投影中的属性尚未包含在查询的另一部分中,则将现有查询转换为投影查询可能需要构建新的索引。例如,假设您有如下现有查询

SELECT * FROM Task
WHERE priority > 1
ORDER BY priority, percent_complete

该查询需要以下索引:

indexes:
- kind: Task
  properties:
  - name: priority
  - name: percent_complete

将其转换为以下任一投影查询

SELECT created FROM Task
WHERE priority > 1
ORDER BY priority, percent_complete

SELECT priority, percent_complete, created FROM Task
WHERE priority > 1
ORDER BY priority, percent_complete

会引入一个新属性 (created),因此需要构建一个新索引:

indexes:
- kind: Task
  properties:
  - name: priority
  - name: percent_complete
  - name: created

但是,

SELECT priority, percent_complete FROM Task
WHERE priority > 1
ORDER BY priority, percent_complete

不会更改所需索引,因为投影属性 prioritypercent_complete 已包含在现有查询中。