Firestore 复合索引缺失:3 个原因 + 修复路径

FAILED_PRECONDITION: The query requires an index——多字段 where + orderBy 必须建。

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

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 的硬性规则——任何涉及多字段 where、或 where + orderBy 不同字段的查询,都需要预先创建复合索引。Firestore 没有 PostgreSQL 那种”自动用索引”机制,索引是显式 schema 的一部分。

常见原因

按命中率从高到低:

1. where + orderBy 用了不同字段

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

任何 where 字段 + 非该字段的 orderBy = 需要复合索引。

如何判断:query 里 where 和 orderBy 的字段不一样?

2. 多个不等值 where(!=, <, <=, >, >=, NOT_IN)

// 需要索引
query(coll, where('status', '!=', 'archived'), orderBy('createdAt'))

不等值过滤本身就要索引;多个不等值更要。

如何判断:where 里有 !=, <, >, not-in, array-contains-any 这些?

3. 多个 where + 任意 orderBy

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

两个以上 where(即使都是等值)+ orderBy = 复合索引。

如何判断:where 子句 ≥ 2 个?

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

emulator 自动建虚拟索引(用得到就建),生产必须手动建。代码合并、deploy、用户第一次访问 → 报错。

如何判断:emulator 正常、生产报错。

5. 索引建好但还在 build 中

新建索引需要几分钟到几小时(看数据量),build 期间查询仍报 FAILED_PRECONDITION。

如何判断:Firebase Console → Firestore → Indexes → 看状态是 “Building” 还是 “Enabled”。

6. 索引建错方向(asc vs desc)

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

索引必须建成 (authorId asc, createdAt desc)。如果建成 desc + asc 也会 mismatch。

如何判断:报错链接点开看建议的 schema,对比已有索引。

最短修复路径

Step 1:点报错里的 URL 自动创建

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

URL 已经预填好需要的 schema,登录 Firebase Console 点 “Create Index” 就行。建好后等几分钟到几小时(看数据量)。

Step 2:把索引同步到 firestore.indexes.json

只在 Console 建会丢失(重新 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

Step 3:等 build 完成

firebase firestore:indexes --project my-prod
# 或在 Console 看 status: Building / Enabled

数百万 doc 的集合可能要几小时。期间错误不会消失。

Step 4:可以的话简化查询

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

// 简化版(单字段,不需要复合索引)
query(coll,
  where('authorId_status', '==', `${x}_published`), // 把两个字段合并成一个
  orderBy('createdAt', 'desc')
)

这种”复合 key”技巧能省一个索引,但要保证写入时同步生成 authorId_status

Step 5:emulator 提前发现

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

# 跑你的应用 + e2e 测试
# emulator 报 FAILED_PRECONDITION 时把建议的索引加进 firestore.indexes.json

# deploy 前 dry-run 看索引差异
firebase firestore:indexes

Step 6:方向对齐

query(..., orderBy('a', 'desc'), orderBy('b', 'asc'))

对应索引必须 (a DESCENDING, b ASCENDING)。Console 自动生成的链接已经对齐了,手写 firestore.indexes.json 要小心。

预防建议

  • 所有新查询先在 emulator 跑一次,把需要的索引提前加进 firestore.indexes.json
  • firestore.indexes.json 走 git,PR review 能看到索引变化
  • CI 加一步 firebase firestore:indexes --dry-run 对比 prod 和代码里的差异
  • 大数据集(百万级 doc)新加索引在低峰期,避免 build 影响读
  • 查询模式相对稳定的写一份”查询模式 → 索引”对照表,新人参考
  • 不要为每个 ad-hoc 查询建索引,定期 audit 删除没用的(每个索引都占存储)
  • 复杂查询考虑反范式(把数据冗余到一起),减少多 where 需要

相关阅读

标签: #Firebase #排查 #排查