编制索引概览
索引是决定数据库性能的一个重要因素。就像书的索引可以将书中的主题映射到页码一样,数据库中的索引可以将数据库中的数据项映射到数据库中的位置。当您查询数据库时,数据库可以使用索引来快速确定您请求的项目的位置。
本页面介绍了 Firestore 使用的两种索引类型:单字段索引和复合索引。
索引定义和结构
索引是在给定文档的字段列表上定义的,每个字段都有相应的索引模式。
对于索引定义中指定的每个字段,索引都会包含一个相应的条目。索引包含根据索引进行查询时,可能会出现在结果中的所有文档。只有当针对索引中使用的每个字段,文档都设置有索引值时,该文档才会包含在索引中。如果文档没有为索引定义所引用的字段设置值,该文档便不会出现在索引中。在这种情况下,基于索引的所有查询都不会将该文档作为结果返回。
复合索引按字段值以索引定义中指定的顺序进行排序。
每个查询的背后都有索引在发挥作用
如果查询没有索引,大部分数据库将需要一项一项地抓取其中的内容,这个过程非常缓慢,且随着数据库的不断增大,查询速度会越来越慢。 Firestore 为所有查询使用索引,以确保较高的查询性能。正因于此,查询的性能取决于结果集的大小,而不是数据库中的条目数量。
减少索引管理,专注应用开发
Firestore 的功能可减少管理索引所需的时间,它会自动为您创建最基本的查询所需的索引。在您使用并测试应用的过程中,Firestore 会帮助您识别并创建您的应用所需的其他索引。
索引类型
Firestore 使用了两种索引类型:单字段索引和复合索引。除了编入索引的字段数量不同之外,单字段索引和复合索引的管理方式也不同。
单字段索引
单字段索引会为一个集合中包含某个特定字段的所有文档存储一个映射,并按顺序对其排序。单字段索引中的每个条目会记录文档中一个特定字段的值,以及该文档在数据库中的位置。Firestore 使用这些索引执行多项基本查询。您可以通过配置数据库的自动索引设置和索引例外项来管理单字段索引。
自动编制索引
默认情况下,Firestore 会自动为文档中的每个字段和映射中的每个子字段维护单字段索引。Firestore 对单字段索引使用以下默认设置:
对于既非数组也非映射的每个字段,Firestore 会定义两个集合范围的单字段索引,一个采用升序模式,另一个采用降序模式。
Firestore 会为每个映射字段创建以下索引:
- 每个非数组、非映射子字段都有一个集合范围的升序索引。
- 每个非数组、非映射子字段都有一个集合范围的降序索引。
- 每个数组子字段的一个集合范围的 array-contains 索引。
- Firestore 会以递归方式为每个映射子字段编制索引。
对于文档中的每个数组字段,Firestore 会创建并维护一个集合范围内的 array-contains 索引。
默认情况下系统不维护采用集合组范围的单字段索引。
单字段索引例外项
您可以通过创建单字段索引例外项来从自动索引设置中排除字段。 索引例外项会覆盖适用于整个数据库的自动索引设置。例外项可让您启用自动索引设置停用的单字段索引,或停用自动索引启用的单字段索引。如需了解例外项的适用场景,请参阅索引最佳实践。
使用 *
字段路径值,为集合组中的所有字段添加集合级索引例外项。例如,对于集合组 comments
,将字段路径设置为 *
,使之匹配 comments
集合组中的所有字段,并禁止将该集合组下的所有字段编入索引。然后,可以添加例外项,以便仅将查询所需的字段编入索引。如果减少编入索引的字段的数量,则可以降低存储费用并提高写入性能。
如果为映射字段创建单字段索引例外项,映射的子字段会继承这些设置。不过,您也可以为特定子字段定义单字段索引例外项。如果删除子字段的例外项,子字段将继承其父级的例外项设置(如果有),如果没有父级例外项,则继承数据库范围的设置。
如需创建和管理单字段索引例外项,请参阅管理索引。
复合索引
复合索引根据要建立索引的字段的有序列表为一个集合中的所有文档存储一个映射,并按顺序对其排序。
Firestore 使用复合索引执行单字段索引不支持的查询。
与处理单字段索引不同,Firestore 不会自动创建复合索引,因为可能的字段组合数太多。但是,Firestore 可以帮您在构建应用时识别并创建必需的复合索引。
每当您尝试运行没有索引支持的查询时,Firestore 都会返回一条错误消息,并提供一个链接,您可以点击该链接来创建缺少的索引。
您还可以使用控制台或 Firebase CLI 手动定义和管理复合索引。如需详细了解如何创建和管理复合索引,请参阅管理索引。
索引模式和查询范围
虽然单字段索引和复合索引的配置方式不同,但都需要为其配置索引模式和查询范围。
索引模式
定义索引时,需要为每个编入索引的字段选择索引模式。每个字段的索引模式都支持针对该字段的特定查询子句。您可以选择以下某种索引模式:
索引模式 | 说明 |
---|---|
升序 | 支持针对该字段的 < 、<= 、== 、>= 、> 、!= 、in 和 not-in 查询子句,并且支持根据此字段值按升序对结果进行排序。 |
降序 | 支持针对该字段的 < 、<= 、== 、>= 、> 、!= 、in 和 not-in 查询子句,并且支持根据此字段值按降序对结果进行排序。 |
Array‑contains | 支持针对该字段的 array-contains 和 array-contains-any 查询子句。 |
矢量 | 支持在该字段上使用 FindNearest 查询子句。 |
查询范围
每个索引的范围都限定为一个集合或集合组。这称为索引的查询范围:
- 集合范围
- 默认情况下,Firestore 使用集合范围创建索引。 这些索引支持从单个集合返回结果的查询。
- 集合组范围
- 集合组包括具有相同集合 ID 的所有集合。如需运行从集合组返回已过滤或已排序结果的集合组查询,必须创建具有集合组范围的相应索引。
默认排序方式和 __name__
字段
除了按每个字段的指定索引模式(升序或降序)对文档进行排序之外,索引还会按每个文档的 __name__
字段进行最终排序。__name__
字段的值设置为完整的文档路径。这意味着结果集中具有相同字段值的文档将按文档路径排序。
默认情况下,__name__
字段的排序方向与索引定义中最后排序的字段的方向相同。例如:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
cities | __name__ |
name, 集合 |
cities | __name__ |
state, 集合 |
cities | __name__ |
country, population, 集合 |
如需按非默认 __name__
方向对结果排序,您需要创建该索引。
索引属性
可让查询以最高效方式执行的索引由以下属性定义:
- 等式过滤条件中使用的字段
- 排序顺序中使用的字段
- 范围和不等式过滤条件中使用的字段(尚未包含在排序顺序中)
- 聚合中使用的字段(尚未包含在排序顺序及范围和不等式过滤条件中)
Firestore 按如下方式计算查询结果:
- 找出与查询的集合、过滤条件属性、过滤条件运算符和排序顺序相对应的索引。
- 确定扫描开始的索引位置。起始位置以查询的等式过滤条件作为前缀,以第一个
orderBy
字段上的范围和不等式过滤条件结尾。 - 开始扫描索引,返回满足所有过滤条件的每个文档,直到扫描过程满足以下条件之一:
- 发现不满足过滤条件的文档,并确认后续的所有文档都不能完全满足过滤条件。
- 到达索引的末尾。
- 收集查询所请求的最大结果数量。
索引示例
由于 Firestore 会自动为您创建单字段索引,因此您的应用可以快速执行最基本的数据库查询。您可以使用单字段索引执行基于字段值和比较运算符(<
、<=
、==
、>=
、>
和 in
)的简单查询。对于数组字段,您可以使用单字段索引执行 array-contains
和 array-contains-any
查询。
为便于说明,请从索引创建的角度查看以下示例。以下代码段在 cities
集合中创建了一些 city
文档,并为每个文档设置了 name
、state
、country
、capital
、population
和 tags
字段:
Web
var citiesRef = db.collection("cities"); citiesRef.doc("SF").set({ name: "San Francisco", state: "CA", country: "USA", capital: false, population: 860000, regions: ["west_coast", "norcal"] }); citiesRef.doc("LA").set({ name: "Los Angeles", state: "CA", country: "USA", capital: false, population: 3900000, regions: ["west_coast", "socal"] }); citiesRef.doc("DC").set({ name: "Washington, D.C.", state: null, country: "USA", capital: true, population: 680000, regions: ["east_coast"] }); citiesRef.doc("TOK").set({ name: "Tokyo", state: null, country: "Japan", capital: true, population: 9000000, regions: ["kanto", "honshu"] }); citiesRef.doc("BJ").set({ name: "Beijing", state: null, country: "China", capital: true, population: 21500000, regions: ["jingjinji", "hebei"] });
假设使用默认的自动索引设置,Firestore 会为每个非数组字段更新一个升序单字段索引和一个降序单字段索引,并为数组字段更新一个 array-contains 单字段索引。下表中的每一行代表单字段索引中的一个条目:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
cities | name | 集合 |
cities | state | 集合 |
cities | country | 集合 |
cities | capital | 集合 |
cities | population | 集合 |
cities | name | 集合 |
cities | state | 集合 |
cities | country | 集合 |
cities | capital | 集合 |
cities | population | 集合 |
cities | array-contains regions |
集合 |
单字段索引支持的查询
使用这些自动创建的单字段索引,您可以运行如下所示的简单查询:
Web
const stateQuery = citiesRef.where("state", "==", "CA"); const populationQuery = citiesRef.where("population", "<", 100000); const nameQuery = citiesRef.where("name", ">=", "San Francisco");
您还可以创建 in
和复合等式 (==
) 查询:
Web
citiesRef.where('country', 'in', ["USA", "Japan", "China"]) // Compound equality queries citiesRef.where("state", "==", "CO").where("name", "==", "Denver") citiesRef.where("country", "==", "USA") .where("capital", "==", false) .where("state", "==", "CA") .where("population", "==", 860000)
如果您需要运行使用了范围比较运算符(<
、<=
、>
或 >=
)的复合查询,或者您需要按照另一个字段进行排序,则必须为该查询创建复合索引。
您可以使用 array-contains
索引查询 regions
数组字段:
Web
citiesRef.where("regions", "array-contains", "west_coast") // array-contains-any and array-contains use the same indexes citiesRef.where("regions", "array-contains-any", ["west_coast", "east_coast"])
复合索引支持的查询
Firestore 使用复合索引执行单字段索引不支持的复合查询。例如,您需要使用复合索引执行下列查询:
Web
citiesRef.where("country", "==", "USA").orderBy("population", "asc") citiesRef.where("country", "==", "USA").where("population", "<", 3800000) citiesRef.where("country", "==", "USA").where("population", ">", 690000) // in and == clauses use the same index citiesRef.where("country", "in", ["USA", "Japan", "China"]) .where("population", ">", 690000)
这些查询需要使用下列复合索引。由于查询针对 country
字段使用等式(==
或 in
),因此您可以对此字段使用升序或降序索引模式。默认情况下,不等式子句会基于其中的字段使用升序排序。
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
cities | (或 )country、 population | 集合 |
如需以降序运行相同的查询,您需要针对 population
字段额外创建一个降序的复合索引:
Web
citiesRef.where("country", "==", "USA").orderBy("population", "desc") citiesRef.where("country", "==", "USA") .where("population", "<", 3800000) .orderBy("population", "desc") citiesRef.where("country", "==", "USA") .where("population", ">", 690000) .orderBy("population", "desc") citiesRef.where("country", "in", ["USA", "Japan", "China"]) .where("population", ">", 690000) .orderBy("population", "desc")
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
cities | country、 population | 集合 |
cities | country、 population | 集合 |
为避免因索引合并而导致性能下降,我们建议您创建复合索引,以将 array-contains
或 array-contains-any
查询与其他子句组合使用:
Web
citiesRef.where("regions", "array-contains", "east_coast") .where("capital", "==", true) // array-contains-any and array-contains use the same index citiesRef.where("regions", "array-contains-any", ["west_coast", "east_coast"]) .where("capital", "==", true)
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
cities | array-contains tags、 | (或 )capital集合 |
集合组索引支持的查询
为了演示查询范围为集合组的索引,请将 landmarks
子集合添加到部分 city
文档:
Web
var citiesRef = db.collection("cities"); citiesRef.doc("SF").collection("landmarks").doc().set({ name: "Golden Gate Bridge", category : "bridge" }); citiesRef.doc("SF").collection("landmarks").doc().set({ name: "Golden Gate Park", category : "park" }); citiesRef.doc("DC").collection("landmarks").doc().set({ name: "National Gallery of Art", category : "museum" }); citiesRef.doc("DC").collection("landmarks").doc().set({ name: "National Mall", category : "park" });
使用查询范围为集合的如下单字段索引,您可以根据 category
字段查询单个城市的 landmarks
集合:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
landmarks | (或 )category | 集合 |
Web
citiesRef.doc("SF").collection("landmarks").where("category", "==", "park") citiesRef.doc("SF").collection("landmarks").where("category", "in", ["park", "museum"])
例如,如果您想要查询所有城市的地标,可以对包含所有 landmarks
集合的集合组运行此查询。您还必须启用具有集合组范围的 landmarks
单字段索引:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
landmarks | (或 )category | 集合组 |
启用此索引后,您可以查询 landmarks
集合组:
Web
var landmarksGroupRef = db.collectionGroup("landmarks"); landmarksGroupRef.where("category", "==", "park") landmarksGroupRef.where("category", "in", ["park", "museum"])
若要运行返回已过滤或已排序结果的集合组查询,必须启用范围为集合组的相应单字段索引或复合索引。但是,不过滤或不排序结果的集合组查询不需要任何其他索引定义。
例如,您可以在不启用其他索引的情况下运行以下集合组查询:
Web
db.collectionGroup("landmarks").get()
索引条目
项目的已配置索引和文档的结构决定了文档索引条目的数量。这些索引条目均会计入索引条目数限制。
以下示例展示了文档的索引条目。
文档
/cities/SF
city_name : "San Francisco"
temperatures : {summer: 67, winter: 55}
neighborhoods : ["Mission", "Downtown", "Marina"]
单字段索引
- city_name ASC
- city_name DESC
- temperatures.summer ASC
- temperatures.summer DESC
- temperatures.winter ASC
- temperatures.winter DESC
- neighborhoods Array Contains(ASC 及 DESC)
复合索引
- city_name ASC, neighborhoods ARRAY
- city_name DESC, neighborhoods ARRAY
索引条目
此索引配置会生成文档的以下索引条目:
索引 | 编入索引的数据 |
---|---|
单字段索引条目 | |
city_name ASC | city_name: "San Francisco" |
city_name DESC | city_name: "San Francisco" |
temperatures.summer ASC | temperatures.summer: 67 |
temperatures.summer DESC | temperatures.summer: 67 |
temperatures.winter ASC | temperatures.winter: 55 |
temperatures.winter DESC | temperatures.winter: 55 |
neighborhoods Array Contains ASC | neighborhoods: "Mission" |
neighborhoods Array Contains DESC | neighborhoods: "Mission" |
neighborhoods Array Contains ASC | neighborhoods: "Downtown" |
neighborhoods Array Contains DESC | neighborhoods: "Downtown" |
neighborhoods Array Contains ASC | neighborhoods: "Marina" |
neighborhoods Array Contains DESC | neighborhoods: "Marina" |
复合索引条目 | |
city_name ASC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Mission" |
city_name ASC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Downtown" |
city_name ASC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Marina" |
city_name DESC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Mission" |
city_name DESC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Downtown" |
city_name DESC, neighborhoods ARRAY | city_name: "San Francisco", neighborhoods: "Marina" |
索引和价格
使用索引时,您的应用会产生存储费用。 如需详细了解如何计算索引的存储空间大小,请参阅索引条目大小。
使用索引合并
虽然 Firestore 会为每个查询都使用索引,但实际上没有必要为每个查询都创建一个索引。如果查询使用多个等式 (==
) 子句和非必需的 orderBy
子句,Firestore 可以重复利用现有的索引。Firestore 可以将针对简单等式过滤条件创建的索引合并起来,构建运行更大的等式查询所需的复合索引。
通过找出可使用索引合并的情况,您可以降低索引产生的费用。例如,一个餐厅评分应用有一个 restaurants
集合:
restaurants
burgerthyme
name : "Burger Thyme"
category : "burgers"
city : "San Francisco"
editors_pick : true
star_rating : 4
此应用使用如下所示的查询。此应用使用的是 category
、city
和 editors_pick
等式子句的组合,并始终按 star_rating
进行升序排序:
Web
db.collection("restaurants").where("category", "==", "burgers") .orderBy("star_rating") db.collection("restaurants").where("city", "==", "San Francisco") .orderBy("star_rating") db.collection("restaurants").where("category", "==", "burgers") .where("city", "==", "San Francisco") .orderBy("star_rating") db.collection("restaurants").where("category", "==", "burgers") .where("city", "==" "San Francisco") .where("editors_pick", "==", true ) .orderBy("star_rating")
您可以为每个查询创建一个索引:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
restaurants | category、 star_rating | 集合 |
restaurants | city、 star_rating | 集合 |
restaurants | category、 city、 star_rating | 集合 |
restaurants | category、 city、 editors_pick、 star_rating | 集合 |
但更好的方式是,充分利用 Firestore 合并等式子句索引的功能,来减少索引的数量:
集合 | 编入索引的字段 | 查询范围 |
---|---|---|
restaurants | category、 star_rating | 集合 |
restaurants | city、 star_rating | 集合 |
restaurants | editors_pick、 star_rating | 集合 |
此索引集不仅更小,还可支持其他查询:
Web
db.collection("restaurants").where("editors_pick", "==", true) .orderBy("star_rating")
索引限制
索引存在以下限制。如需详细了解配额和限制,请参阅配额和限制。
本页介绍 Firestore 的请求配额和限制。
限制 | 详细信息 |
---|---|
一个数据库的复合索引数量上限 |
|
数据库单字段配置的数量上限 |
一个字段级配置可以包含同一字段的多个配置。例如,单字段索引例外项和针对同一字段的 TTL 政策将被视为一个字段配置计入限额。 |
每个文档的索引条目数量上限 |
40000 索引条目的数量是文档的以下各项数量的总和:
如需了解 Firestore 如何将一个文档和一组索引转变为索引条目,请参阅此索引条目计数示例。 |
复合索引中的字段数上限 | 100 |
索引条目的大小上限 |
7.5 KiB 如需了解 Firestore 如何计算索引条目大小,请参阅索引条目大小。 |
一个文档的索引条目的大小总和上限 |
8 MiB 总大小是文档的以下各项的大小总和: |
编入索引的字段值的大小上限 |
1500 字节 超出 1500 字节的字段值会被截断。包含被截断的字段值的查询可能会返回不一致的结果。 |
索引最佳实践
对于大多数应用,您可以依靠自动索引和错误消息链接来管理索引。但是,在以下情况下,您可能需要添加单字段例外项:
场景 | 说明 |
---|---|
大型字符串字段 | 如果您的字符串字段通常包含不用于查询的长字符串值,您可以选择不将该字段编入索引来降低存储费用。 |
向所含文档具有依序值的集合进行高速率写入 | 如果您将在某个集合中的各文档之间依序递增或递减的字段(如时间戳)编入索引,则向该集合写入数据的最大速率为每秒 500 次写入。如果您不根据具有序列值的字段进行查询,可以选择不将该字段编入索引来绕过此限制。 例如,在具有高写入速率的 IoT 使用场景中,一个所含文档具有时间戳字段的集合可能会达到每秒 500 次的写入限制。 |
TTL 字段 |
请注意,如果您使用 TTL(存留时间)政策,则 TTL 字段必须是一个时间戳。默认情况下,系统会将 TTL 字段编入索引,这可能会在流量传输速率较高时影响性能。最佳做法是为 TTL 字段添加单字段例外项。 |
大型数组或映射字段 | 大型数组或映射字段可能会达到每个文档 40,000 个索引条目的限制。如果您的查询不是基于大型数组或映射字段,建议您不要将该数组或字段编入索引。 |
如果您使用对多个字段使用范围和不等式运算符的查询,请参阅索引编制注意事项,以便优化 Firestore 查询的性能和费用
如需详细了解如何解决索引编制问题(索引扇出、INVALID_ARGUMENT
错误),请参阅问题排查页面。