一句话结论: 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,都需要复合索引。如果 where 和 orderBy 是同一个字段,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-in、array-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 个范围/不等值字段。你仍然需要一个覆盖这些字段的复合索引,并且应该把选择性最高的字段放在最前面。