优化索引

本页面介绍了在为应用选择 Datastore 模式 Firestore 索引时要考虑的概念。

Datastore 模式 Firestore 为所有查询使用索引,以提供较高的查询性能。大多数查询的性能取决于结果集的大小,而不取决于数据库的总大小。

Datastore 模式 Firestore 为实体中的每个属性定义内置索引。 这些单属性索引支持许多简单查询。Datastore 模式 Firestore 支持索引合并功能,该功能允许您的数据库合并内置索引以支持其他查询。对于更复杂的查询,您必须提前定义复合索引。

本页主要介绍索引合并功能,因为它会影响两个重要的索引优化机会:

  • 加速查询
  • 减少复合索引数量

以下示例演示了索引合并功能。

过滤 Photo 实体

假设 Datastore 模式数据库包含种类为 Photo 的实体:

照片
属性 值类型 说明
owner_id 字符串 用户 ID
tag 字符串数组 标记化关键字
size 整数 枚举:
  • 1 icon
  • 2 medium
  • 3 large
coloration 整数 枚举:
  • 1 black & white
  • 2 color

假设您需要一项应用功能,该功能允许用户根据以下项的逻辑 AND 来查询 Photo 实体:

  • 基于以下属性的最多三个过滤条件:

    • owner_id
    • size
    • coloration
  • 一个 tag 搜索字符串。应用会将搜索字符串标记化为标记,并为每个标记添加一个过滤条件。

    例如,应用会将搜索字符串 outside, family 转换为查询过滤条件 tag=outsidetag=family

如果使用内置索引和 Datastore 模式 Firestore 的索引合并功能,您可以满足此 Photo 过滤功能的索引需求,而无需添加其他复合索引。

Photo 实体的内置索引支持单过滤条件查询,例如:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_id = client.query(kind="Photo", filters=[("owner_id", "=", "user1234")])

query_size = client.query(kind="Photo", filters=[("size", "=", 2)])

query_coloration = client.query(kind="Photo", filters=[("coloration", "=", 2)])

Photo 过滤功能还需要将多个等式过滤条件与一个逻辑 AND 组合在一起的查询:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Datastore 模式 Firestore 可通过合并内置索引来支持这些查询。

索引合并

如果您的查询和索引满足以下所有限制条件,则 Datastore 模式 Firestore 可以使用索引合并功能:

  • 查询仅使用等式 (=) 过滤条件
  • 不存在与查询的过滤条件和排序完全匹配的复合索引
  • 每个等式过滤条件至少匹配一个其顺序与查询相同的现有索引

在这种情况下,Datastore 模式 Firestore 可以使用现有索引来支持查询,而无需您配置其他复合索引。

如果两个或多个索引按相同条件排序,则 Datastore 模式 Firestore 可以合并多个索引扫描的结果,以查找所有此类索引的共同结果。Datastore 模式 Firestore 可以合并内置索引,因为它们都是按实体键对值进行排序。

通过合并内置索引,Datastore 模式 Firestore 支持针对多个属性执行使用等式过滤条件的查询:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Datastore 模式 Firestore 还可以合并来自同一索引的多个部分的索引结果。通过合并 tag 属性的内置索引的不同部分,Datastore 模式 Firestore 支持执行将多个 tag 过滤条件与一个逻辑 AND 组合在一起的查询:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_tag = client.query(
    kind="Photo",
    filters=[
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

query_owner_size_color_tags = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

合并的内置索引所支持的查询会完成 Photo 过滤功能所要求的一组查询。请注意,支持 Photo 过滤功能并不需要其他复合索引。

在为应用选择最佳索引时,了解索引合并功能非常重要。索引合并功能赋予 Datastore 模式 Firestore 更大的查询灵活性,但可能牺牲部分性能。下一部分将介绍索引合并功能的性能,以及如何通过添加复合索引来提高性能。

查找完美的索引

索引按照索引定义中指定的顺序,先按祖先实体再按属性值进行排序。查询的完美复合索引可让系统以最高效的方式执行查询,它按如下顺序基于以下属性定义:

  1. 相等性过滤条件中使用的属性
  2. 排序顺序中使用的属性
  3. distinctOn”过滤条件中使用的属性
  4. 范围和不等式过滤条件中使用的属性(尚未包含在排序顺序中)
  5. 聚合和投影中使用的属性(尚未包含在排序顺序及范围和不等式过滤条件中)

这样可确保计算每次可能执行查询的所有结果。Datastore 模式 Firestore 数据库使用完美索引执行查询,具体步骤如下:

  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 采用 Z 型合并连接算法高效合并索引。使用此算法,Datastore 模式会连接多个索引扫描的可能匹配项,以生成与查询匹配的结果集。索引合并功能会在读取时(而非写入时)组合过滤条件组成成分。在大多数 Datastore 模式 Firestore 查询中,性能仅取决于结果集的大小;与这些查询不同,索引合并查询的性能取决于查询中的过滤条件以及数据库考虑的可能匹配项数。

如果索引中的每个可能匹配项都满足查询过滤条件,则索引合并就会出现最佳情况的性能。在这种情况下,性能为 O(R * I),其中 R 是结果集的大小,而 I 是扫描的索引数量。

如果数据库必须考虑许多可能的匹配项,但这些匹配项基本都不满足查询过滤条件,则会出现最糟糕情况的性能。在这种情况下,性能为 O(S),其中 S 为单个索引扫描中的最小可能实体集的大小。

实际性能取决于数据的形状。每个返回结果所考虑的平均实体数为 O(S/(R * I))。如果许多实体与每个索引扫描匹配,但整个查询的匹配实体很少,则查询性能较差,这意味着 R 较小,而 S 较大。

可通过以下四个因素降低此风险:

  • 查询规划器不查找某一实体,除非知道该实体与整个查询匹配。

  • Z 型算法不需要找到所有结果即可返回下一个结果。如果您请求前 10 个结果,则只需为查找这 10 个结果的延迟时间付费。

  • Z 型算法会跳过大部分误报结果。 只有两次扫描之间误报结果在排序顺序上完全交织在一起时,才会出现最坏情况的性能。

  • 延迟时间取决于每个索引扫描中找到的实体数量,而不是与每个过滤条件匹配的实体数量。如下一部分所示,您可以添加复合索引来提高索引合并性能。

加快索引合并查询的速度

如果 Datastore 模式 Firestore 合并索引,则每个索引扫描通常会映射到查询中的单个过滤条件。您可以通过添加与查询中的多个过滤条件匹配的复合索引来提高查询性能。

设想以下查询:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_size_tag = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "username"),
        ("size", "=", 2),
        ("tag", "=", "family"),
    ],
)

每个过滤器映射到以下内置索引中的一个索引扫描:

Index(Photo, owner_id)
Index(Photo, size)
Index(Photo, tag)

如果您添加复合索引 Index(Photo, owner_id, size),则查询会映射到两个索引扫描,而不是三个:

#  Satisfies both 'owner_id=username' and 'size=2'
Index(Photo, owner_id, size)
Index(Photo, tag)

设想某一场景具有许多大型图片、许多黑白图片,但只有少量的大型全景图片。如果查询要过滤全景图片和黑白图片,且合并内置索引,则查询速度会很慢:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_size_coloration = client.query(
    kind="Photo", filters=[("size", "=", 2), ("coloration", "=", 1)]
)

如需提高查询性能,您可以通过添加以下复合索引来调小 O(S/(R * I))S(单个索引扫描中的最小实体集)的值:

Index(Photo, size, coloration)

与使用两个内置索引相比,此复合索引为两个相同的查询过滤条件产生的可能结果较少。此方法以另一个索引的费用大幅提升了性能。

通过索引合并功能减少复合索引数量

尽管与查询中的过滤条件完全匹配的复合索引具有最佳性能,但为每个过滤条件组合添加复合索引并不总是最好或可行的。您必须根据以下项平衡您的复合索引:

  • 复合索引限制:

    限制 金额
    一个数据库的复合索引数量上限
    一个实体的复合索引条目的大小总和上限 2 MiB
    一个实体的下述各项的数量总和上限:
    • 已编制索引的属性值数量
    • 复合索引条目的数量
    20000
  • 每个其他索引的存储费用。
  • 对写入延迟时间的影响。

多值字段(例如 Photo 实体的 tag 属性)通常会出现索引编制问题。

例如,假设 Photo 过滤功能现在需要根据 4 个其他属性来支持降序排序子句:

照片
属性 值类型 说明
date_added 整数 日期/时间
rating 浮点数 汇总用户评分
comment_count 整数 注释数量
download_count 整数 下载次数

如果您忽略 tag 字段,则可以选择与 Photo 过滤条件的每个组合相匹配的复合索引:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -comments)
Index(Photo, size, -date_added)
Index(Photo, size, -comments)
...
Index(Photo, owner_id, size, -date_added)
Index(Photo, owner_id, size, -comments)
...
Index(Photo, owner_id, size, coloration, -date_added)
Index(Photo, owner_id, size, coloration, -comments)

复合索引的总数为:

2^(number of filters) * (number of different orders) = 2 ^ 3 * 4 = 32 composite indexes

如果您尝试支持最多 3 个 tag 过滤条件,则复合索引的总数为:

2 ^ (3 + 3 tag filters) * 4 = 256 indexes.

包含 tag 等多值属性的索引也会导致爆炸式索引问题,造成存储费用增加和写入延迟时间延长。

您可以依靠合并的索引来减少索引总数,以支持针对此功能按 tag 字段进行过滤。下面的复合索引集是支持进行排序的 Photo 过滤功能所需的最低要求:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -rating)
Index(Photo, owner_id, -comments)
Index(Photo, owner_id, -downloads)
Index(Photo, size, -date_added)
Index(Photo, size, -rating)
Index(Photo, size, -comments)
Index(Photo, size, -downloads)
...
Index(Photo, tag, -date_added)
Index(Photo, tag, -rating)
Index(Photo, tag, -comments)
Index(Photo, tag, -downloads)

定义的复合索引数量为:

(number of filters + 1) * (number of orders) = 7 * 4 = 28

索引合并功能还具有以下优势:

  • 允许一个 Photo 实体支持多达 1000 个标记,但不限制每个查询的 tag 过滤条件数量。
  • 减少索引总数,从而降低存储费用和缩短写入延迟时间。

为您的应用选择索引

您可以使用以下两种方法为 Datastore 模式数据库选择最佳索引:

  • 使用索引合并功能来支持其他查询

    • 需要较少的复合索引
    • 降低每个实体的存储费用
    • 缩短写入延迟时间
    • 避免爆炸式索引
    • 性能取决于数据的形状
  • 定义与查询中的多个过滤条件相匹配的复合索引

    • 提高了查询性能
    • 不依赖于数据形状的一致查询性能
    • 必须小于复合索引的限制
    • 增加了每个实体的存储费用
    • 延长写入延迟时间

在确定应用的最佳索引时,答案可能会因数据形状的变化而不同。抽样查询性能让您能够很好地了解应用的常见查询及应用的慢速查询。利用这些信息,您可以添加索引以提高慢速常见查询的性能。