自作アプリのランキング画面が「読み込み中」のまま止まったことがあります。そのときは、Firebase Realtime Databaseのルールに.indexOnが無いのが原因だと結論づけ、ルールに足してデプロイして直しました。
この記事を書くためにエミュレータで確かめ直したところ、結論から書くと、.indexOnが無いときに失敗したのはget()とREST APIで、onValueは普通にデータを受け取りました。失敗したときのエラーはIndex not definedで、Permission deniedになったのは読み取りのルールが無いときだけでした。
この記事では、条件を変えて確かめた結果と、当時の結論のどこが確かめきれていなかったか、2つのエラーを取り違えないための切り分け方をまとめます。
スポンサーリンク
.indexOnが無いとorderByChildのクエリはどうなるか
結論:get()とREST APIはIndex not definedで失敗し、onValueはデータを受け取れました。
Firebase Local Emulator Suite(firebase-tools 15.16.0)のRealtime Databaseに、ランキングと同じ形のデータを入れました。Firebase JS SDK 12.19.0から、orderByChild('balance')とlimitToLast(2)のクエリを投げています。
| ルールの条件 | get() | onValue | REST API |
|---|---|---|---|
| .indexOn なし・読み取り許可あり | 失敗(Index not defined) | 取得できた | 400(Index not defined) |
| .indexOn あり・読み取り許可あり | 取得できた | (試していない) | 200 |
| .indexOn あり・読み取り許可なし | 失敗(Permission denied) | 失敗(PERMISSION_DENIED) | 401(Permission denied) |
| .indexOn なし・読み取り許可なし | 失敗(Permission denied) | (試していない) | 401(Permission denied) |
.indexOnが無いときのget()のエラーは、次のとおりでした。直すべきルールの書き方まで、メッセージに書かれています。
Error: Index not defined, add ".indexOn": "balance", for path "/scores", to the rules
読み取りのルールが無いときは、onValueのエラーが次のようになりました。
permission_denied at /scores: Client doesn't have permission to access the desired data.
読み取りのルールが無い場合は、.indexOnの有無に関係なくPermission deniedが先に返りました。2つのエラーは、原因もメッセージもまったく別物です。
確かめたコード
結論:get()とonValueに、同じクエリを渡して比べました。
import { initializeApp } from 'firebase/app';
import { getDatabase, connectDatabaseEmulator, ref, get, onValue, query, orderByChild, limitToLast } from 'firebase/database';
const app = initializeApp({ projectId: 'demo-rinblog', databaseURL: 'https://demo-rinblog-default-rtdb.firebaseio.com' });
const db = getDatabase(app);
connectDatabaseEmulator(db, '127.0.0.1', 9000);
const q = query(ref(db, 'scores'), orderByChild('balance'), limitToLast(2));
try {
console.log('get()', (await get(q)).val());
} catch (e) {
console.log('get() error:', e.message);
}
onValue(q,
(snapshot) => console.log('onValue', snapshot.val()),
(e) => console.log('onValue error:', e.message));
.indexOnが無い状態では、get()だけがIndex not definedで失敗し、onValueには上位2件(balanceが1200と800)が届きました。
公式ドキュメントの説明との関係
結論:公式ドキュメントは「リアルタイムのクライアントライブラリはインデックスなしでもクエリを実行できる」と説明しています。今回の結果では、それが当てはまったのはonValueで、get()はREST APIと同じく失敗しました。
Firebaseのデータのインデックス登録のドキュメントは、インデックスはREST APIを使う場合を除いて開発中は必須ではなく、データが増えると性能が落ちるので公開前に追加すべき、と書いています。
get()がREST APIと同じ振る舞いになる理由までは、今回は確かめていません。「SDKからのクエリだからインデックスが無くても動く」とは限らない、というのが確かめた範囲で言えることです。
当時の「permission-deniedに見えた」はどう考えるべきか
結論:今回の検証では、.indexOnの不足でPermission deniedは出ませんでした。当時の症状を.indexOnの不足だけで説明したのは、確かめきれていない結論でした。
当時のアプリは、ランキングをorderByChild('balance').limitToLast(30).get()で取得していました。get()を使っていたので、.indexOnが無ければ取得に失敗するのは今回の結果と合います。
一方で、当時の記録には「クライアント側にはpermission-deniedとして現れた」と書いていました。今回の検証では、.indexOnが無いときのエラーはIndex not definedで、Permission deniedとは別物です。
▼当時の結論で確かめきれていなかった点
▶ 直した操作は「.indexOnを足したルールファイルのデプロイ」で、ルール全体が反映されたこととインデックスが足されたことを切り分けていない
▶ permission-deniedが、Flutterのアプリでどういう形で表示されたのかを、今回の検証では再現できていない
▶ REST APIで確かめたIndex not definedは、インデックスが無ければ必ず出るので、原因を1つに決める根拠としては弱い
当時のデプロイ前に、本番に反映されていたルールがどういう状態だったかは、記録に残っておらず確かめられません。.indexOnが必要だったこと自体は、get()を使っていた以上は正しかったと考えています。ただ、それだけが原因だったとは言い切れません。
見えているエラーの表示を、そのまま原因として扱ってしまう失敗は、AIがビルド失敗を「成功」と報告した件とも根が同じだと感じています。
スポンサーリンク
permission deniedとIndex not definedを切り分ける手順
結論:REST APIで同じクエリを投げてエラーの文言を確かめ、そのうえでエミュレータでルールを1つずつ変えて再現します。
▼切り分けの手順
① REST APIで同じクエリを投げる。400 Index not definedならインデックス、401 Permission deniedならルール
② アプリ側でget()とonValueの両方を試す。onValueだけ通るならインデックスを疑う
③ エミュレータでルールを1つずつ変えて、同じエラーが再現するかを見る
④ 直したら、何を変えたから直ったのかを、1回に1つずつ変えて確かめる
エミュレータで試す方法
結論:プロジェクトIDをdemo-で始めると、Firebaseにログインしなくてもエミュレータを起動できます。
エミュレータの起動には、Java(今回はJDK 21)が要ります。
firebase emulators:start --only database --project demo-rinblog
起動中は、ルールをREST APIで差し替えられます。データを入れ直さずに、条件だけを次々に変えて試せます。
# ルールを差し替える(.indexOn なし・読み取り許可あり)
curl -X PUT "http://127.0.0.1:9000/.settings/rules.json?ns=demo-rinblog-default-rtdb" \
-H "Authorization: Bearer owner" \
-d '{"rules":{".read":true,".write":true}}'
# REST API で orderByChild 相当のクエリを投げる
curl "http://127.0.0.1:9000/scores.json?ns=demo-rinblog-default-rtdb&orderBy=%22balance%22&limitToLast=2"
.indexOnが無いこの状態で、REST APIは400と次の本文を返しました。
{
"error" : "Index not defined, add \".indexOn\": \"balance\", for path \"/scores\", to the rules"
}
.indexOnはどこに書けばいいのか
結論:クエリを投げるノード(orderByChildを呼ぶrefの場所)に書きます。週ごとに分けたノードなら、ワイルドカードの下に書きます。
{
"rules": {
"scores": {
".read": "auth != null",
".indexOn": ["balance"]
},
"weekly": {
"$weekId": {
".read": "auth != null",
".indexOn": ["balance"]
}
}
}
}
weekly/2026-W37のように週ごとのノードでクエリを投げる場合を、エミュレータで確かめました。
| .indexOnの位置 | weekly/2026-W37 への get() |
|---|---|
| 書かない | 失敗(Index not defined) |
weekly/$weekIdの下 |
取得できた |
weeklyの直下(1階層ずれ) |
失敗(Index not defined) |
1階層ずれて書くと効きません。エラーメッセージのfor pathに書かれたパスと、ルールを書いた位置が合っているかを確かめてください。
スポンサーリンク
関連する記事
▶ Firebase RTDBで連番キーが配列(List)で返る条件と対処…同じエミュレータで確かめた、Realtime Databaseのデータの返り方
▶ AIがビルド失敗を「成功」と報告した原因|パイプの終了コードとnoclobber…見えている結果を根拠にして原因を取り違えた別の例
まとめ
▼要点
① .indexOnが無いと、get()とREST APIはIndex not definedで失敗し、onValueは取得できた
② Permission deniedは読み取りのルールが無いときのエラーで、インデックスの不足では出なかった
③ 切り分けは、REST APIのエラー文言と、get()・onValueの両方で確かめる
④ .indexOnはクエリを投げるノードの位置に書く。1階層ずれると効かない
当時は「直ったのだから原因は.indexOn」と結論づけました。直った操作に複数の変更が含まれていたことに、書き直すまで気づいていませんでした。確かめ直した結果、.indexOnが要ることは正しく、permission-deniedの説明は確かめきれていなかった、というのが今の結論です。
※Firebase Local Emulator Suite(firebase-tools 15.16.0・JDK 21)とFirebase JS SDK 12.19.0で、2026年9月12日に確認した内容です。本番のRealtime Databaseでの挙動と、Flutter(firebase_database)での表示は、この検証では確かめていません。