修复 Firestore "The query requires an index"(FAILED_PRECONDITION)

Firestore 报 FAILED_PRECONDITION,是因为多字段 where + orderBy 需要复合索引。最快修复、CLI 工作流、以及如何确认索引已建好。

一句话结论: Firestore 在告诉你这条查询需要一个还不存在的复合索引。报错信息里带了一个预填好的 console 链接,点开、登录、点 Create index,等状态从 Building 变成 Enabled(小集合几分钟,大集合可能要几小时),查询就能跑了。之后把同一个索引拷进 firestore.indexes.json 并提交,下次 deploy 才不会把它弄丢。

你写了一段看起来很普通的查询:

const q = query(
  collection(db, 'posts'),
  where('authorId', '==', uid),
  where('status', '==', 'published'),
  orderBy('createdAt', 'desc')
);

本地 emulator 跑得好好的,部署到生产就爆:

FAILED_PRECONDITION: The query requires an index. You can create
it here: https://console.firebase.google.com/...

Firestore 的规则(截至 2026 年 6 月没变):每条查询都由一个索引来服务。 单字段查询有自动创建的索引;但只要你开始组合字段——两个或以上 where,或者 where 加一个在不同字段上的 orderBy——就需要一个复合索引,而 Firestore 不会在查询时临时帮你造一个。它不像 PostgreSQL 或 MySQL 有查询规划器会挑一个”差不多能用”的索引。索引是你显式声明的 schema,缺了那个精确匹配的,查询就直接失败。

先判断你属于哪一类

现象最可能的原因跳到
where 字段和 orderBy 字段不一样需要复合索引Step 1
两个或以上 where + 一个 orderBy需要复合索引Step 1
emulator 正常,只有生产报错生产从没建过这个索引Step 1
索引已经建了还是报错还在 build,或方向建错了Step 3
昨天还好,deploy 之后坏了deploy 覆盖掉了只在 console 建的索引Step 2

常见原因

按命中率从高到低排。

1. where + orderBy 用了不同字段

// 需要复合索引:(authorId ASC, createdAt DESC)
query(coll, where('authorId', '==', x), orderBy('createdAt', 'desc'))

任何 where 字段 + 在另一个字段上的 orderBy,都需要复合索引。如果 whereorderBy 是同一个字段,Firestore 会用自动创建的单字段索引,不会报错。

2. 两个或以上等值 where + 任意 orderBy

// 需要复合索引:(status ASC, authorId ASC, createdAt DESC)
query(coll,
  where('status', '==', 'published'),
  where('authorId', '==', x),
  orderBy('createdAt', 'desc')
)

两个或以上 where(哪怕全是 ==)再加一个 orderBy,就需要复合索引。每个字段各自有索引是不够的,索引必须覆盖这个确切的组合

3. 范围 / 不等值过滤

// 需要索引
query(coll, where('price', '>=', 10), orderBy('createdAt'))

任何范围或不等值操作符(<<=>>=!=not-inarray-contains-any)配上 orderBy 或另一个过滤条件,都需要复合索引。

如果你还记得以前的限制,这里有个变化值得注意:自 2024 年多字段范围与不等值过滤正式 GA 后,Firestore 允许你在一条查询里对不同字段都用不等值了(比如 where('price', '>', 10)where('rating', '>', 4) 同时用)。上限是每条查询最多 10 个范围/不等值字段,而且这类查询仍然需要一个把这些字段都列上的复合索引。为了性能,把选择性最高的字段放在最前面。详见官方文档

4. 生产第一次跑这条查询

emulator 会按需建虚拟索引,所以一条从没在生产跑过的查询能通过所有本地测试,却在真实用户第一次触发时立刻失败。代码合并 -> deploy -> 第一个请求 -> FAILED_PRECONDITION

如何判断: emulator 正常、生产报错,而且 console 里没有匹配的索引。

5. 索引存在但还在 build

新建的索引在 build 完成前会一直报 FAILED_PRECONDITION。数百万 doc 的集合大约要 10-30 分钟;数亿可能要几小时。build 期间集合的读写都正常——只有需要这个特定索引的那条查询会失败。

如何判断: Firebase Console -> Firestore Database -> Indexes -> Composite 标签页里,这个索引显示为 Building 而不是 Enabled

6. 索引建错了排序方向

query(coll, where('authorId', '==', x), orderBy('createdAt', 'desc'))

这条需要 (authorId ASCENDING, createdAt DESCENDING)。如果你手写时把 createdAt 写成了 ASCENDING,就匹配不上,查询照样失败。console 链接生成的方向总是对的;手动改过的 firestore.indexes.json 才是这类问题溜进来的地方。

如何判断: 重新点开报错链接,把它预填的 schema 跟你已有的索引逐个字段对比。

最短修复路径

Step 1:点报错里的链接

完整报错长这样:

FAILED_PRECONDITION: The query requires an index.
You can create it here:
https://console.firebase.google.com/project/your-app/firestore/indexes?create_composite=...

这个 create_composite 链接打开时,每个字段和方向都已经预填好了。登录、确认是对的 project,点 Create index。这是最快的修法,永远应该先做这一步,因为生成的 schema 一定和查询匹配。

如果你的框架把报错吞掉了(有些框架只打印 message),去 Cloud Logging 或服务端日志里找完整字符串——链接就在里面。

Step 2:把索引存进 firestore.indexes.json

只在 console 建的索引不在你的代码库里,所以之后一次 firebase deploy --only firestore:indexes 可能把它删掉。把线上的索引导出到 Firebase 用来 deploy 的那个文件:

firebase firestore:indexes > firestore.indexes.json

或者手动维护:

{
  "indexes": [
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "authorId", "order": "ASCENDING" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ]
}

然后部署:

firebase deploy --only firestore:indexes

firestore.indexes.json 提交到 git。从此你的索引就是代码,能扛过 deploy,也会出现在 PR review 里。

Step 3:等 build 完成并验证

build 是异步的,没完成之前查询会一直失败。查状态:

firebase firestore:indexes

或者在 console 里看:Firestore Database -> Indexes -> Composite 标签页。当那一行显示 Enabled,索引就生效了。把刚才失败的那条查询原样再跑一遍——它应该返回数据而不是 FAILED_PRECONDITION。这个回路才是真正确认修好了的标志;光看 Enabled 状态偶尔会比实际滞后几秒。

百万级 doc 的集合预计 10-30 分钟;数亿可能要几小时。build 期间读写不受影响。

Step 4:等不及就简化查询

如果你不想再加一个索引,可以把两个等值过滤合并成一个在写入时维护的字段:

// 需要复合索引
query(coll,
  where('status', '==', 'published'),
  where('authorId', '==', x),
  orderBy('createdAt', 'desc')
)

// 写入时拼好的单一复合 key -> 只需要单字段索引
query(coll,
  where('authorId_status', '==', `${x}_published`),
  orderBy('createdAt', 'desc')
)

你仍然需要一个 (authorId_status ASC, createdAt DESC) 的索引,但这样把”多个等值”的组合压成了一个。当你有很多这种组合、想控制索引数量时,这是标准的取舍。代价是每次写入都要保证 authorId_status 同步生成、保持一致。

Step 5:deploy 之前在 emulator 里就抓到

# 本地启 emulator
firebase emulators:start --only firestore

用 emulator 跑你的应用和 e2e 测试。每当 emulator 打出 FAILED_PRECONDITION,当场就把它建议的索引拷进 firestore.indexes.json,别等代码到了生产。emulator 给的索引提示和生产要求的一致。

预防建议

  • 所有新查询先在 emulator 跑一次,合并前就把需要的索引加进 firestore.indexes.json
  • firestore.indexes.json 走 git,让索引变化出现在 PR review 里。
  • CI 加一步 firebase deploy --only firestore:indexes --dry-run(Firebase CLI 13.18.0+),不碰生产就能校验索引变更。
  • 给超大集合加索引时选低峰期,build 会抢资源。
  • 复杂查询考虑反范式(写入时拼一个复合 key 字段),而不是一直叠索引。
  • 定期 audit、删掉没用的索引——每个索引都占存储,还会略微拖慢写入。

常见问题

复合索引要建多久? 小集合几秒到一两分钟。数百万 doc 大约 10-30 分钟。数亿要几小时。整个过程集合都能读写,只有需要这个索引的那条查询会失败,直到状态变成 Enabled

索引显示 Enabled 了,查询还是失败,为什么? 通常两个原因:方向不匹配(需要 authorId ASC, createdAt DESC,你建成了 createdAt ASC),或者这根本是个别的索引、不是查询要的那个。重新点开报错链接,逐字段对比它预填的 schema。状态偶尔也会比实际 build 慢几秒——重试一次看看。

昨天还好,deploy 之后坏了,发生了什么? 基本可以肯定你只在 console 建了索引,然后一次来自不含该索引的代码库的 firebase deploy --only firestore:indexes 把它抹掉了。跑 firebase firestore:indexes > firestore.indexes.json,提交文件,重新部署。

为什么 emulator 通过、生产却失败? emulator 按需建虚拟索引;生产要求索引提前就存在。一条从没在生产跑过的查询能通过所有本地测试,然后在第一个真实请求上失败。

现在能在两个不同字段上都用不等值过滤吗? 能。自 2024 年多字段范围与不等值过滤 GA 后可以了,每条查询最多 10 个范围/不等值字段。你仍然需要一个覆盖这些字段的复合索引,并且应该把选择性最高的字段放在最前面。

相关阅读

标签: #Firebase #排查 #排查