🙋

Firestore で in を使う時の注意点:「クエリの分離句が最大 30 に制限される」ことを理解しよう

2024/11/29に公開

Firestore を使ってデータを扱うとき、inarray-contains-any を使うと、複数の条件に合致するデータを簡単に取得できます。

例えば、以下のようなクエリ:

const query = collection.where('status', 'in', ['active', 'pending']);

これは「status が active または pending のデータを取得する」というものです。

この in は内部的に「OR 条件」として処理されており、Firestore には「クエリ内の OR 条件(分離句)が最大 30 まで」という制限があります。条件が30を超えるとエラーが発生します。

Firestore ではこのクエリを 選言標準形(Disjunctive Normal Form, DNF) に変換して処理します。この変換により、条件の組み合わせが増えて制限に達しやすくなることがあります。

今回は、「分離句とは何か?」や「in を使う時の工夫」について、具体例を使ってわかりやすく解説します!

1. Firestore のクエリ処理: 選言標準形(Disjunctive Normal Form, DNF)とは?

Firestore では、クエリを処理する際に 選言標準形(Disjunctive Normal Form, DNF) に変換します。

To prevent a query from becoming too computationally expensive, Cloud Firestore limits how many AND and OR clauses you can combine. To apply this limit, Cloud Firestore converts queries that perform logical OR operations (orin, and array-contains-any) to disjunctive normal form (also known as an OR of ANDs). Cloud Firestore limits a query to a maximum of 30 disjunctions in disjunctive normal form.
(クエリの計算コストが高くなりすぎるのを防ぐため、Cloud FirestoreではAND句とOR句を組み合わせられる数を制限しています。 この制限を適用するために、Cloud Firestoreは論理OR演算(or、in、array-contains-any)を実行するクエリを選言標準形(ANDのORとも呼ばれる)に変換します。 Cloud Firestoreは、選言標準形に基づいて、クエリを最大30個の分離に制限します。)
https://firebase.google.com/docs/firestore/query-data/queries#limits_on_or_queries

選言標準形とはあまり聞き慣れない単語ですが、Wikipediaによると以下のように説明されています。

選言標準形(せんげんひょうじゅんけい、: Disjunctive normal form, DNF)は、数理論理学においてブール論理での論理式の標準化(正規化)の一種であり、連言節(AND)の選言(OR)の形式で論理式を表す。加法標準形主加法標準形積和標準形とも呼ぶ。正規形としては、自動定理証明で利用されている。
https://ja.wikipedia.org/wiki/選言標準形

うーん、意味がよくわかりませんね🤔 少しまとめて 分解して見てみましょう。

選言標準形は、連言節(ANDのグループ)の選言(OR)の形式で論理式を表す

連言(AND)とは

  • 意味:「かつ(AND)」を表す言葉。AかBか(…Nか)の全てが成り立つという状態を示す
  • 例:「焼肉を食べて、かつ 寿司も食べた」

数学で懐かしい「∧」の記号を使って表現されるものです: A ∧ B

このA ∧ BC ∧ D ∧ Eをそれぞれ連言節(ANDのグループ)と言います。

選言(OR)とは

  • 意味:「または(OR)」を表す言葉。AかBか(…Nか)のどれか少なくとも一つが成り立つという状態を示す
  • 例:「焼肉または寿司を食べたい」

こちらも懐かしい「∨」の記号を使って表現されるものです: A ∨ B

連言節(ANDのグループ)の選言(OR)の形式

これは、連言節(ANDのグループ)選言(OR) で繋げる形式という意味で、以下のようなイメージです。

(A ∧ B) ∨ (C ∧ D) ∨ (E ∧ F)

  • 連言節: A ∧ B, C ∧ D, E ∧ F → それぞれのグループ内で「かつ」が成立する。
  • 選言: それらのグループを「または」で繋げる。

つまり選言標準形とは

ここで選言標準形についての引用に戻りましょう。

選言標準形(せんげんひょうじゅんけい、: Disjunctive normal form, DNF)は、数理論理学においてブール論理での論理式の標準化(正規化)の一種であり、連言節(AND)の選言(OR)の形式で論理式を表す。加法標準形主加法標準形積和標準形とも呼ぶ。正規形としては、自動定理証明で利用されている。

つまりざっくり言うと、

選言標準形とは、数理論理学という分野において綺麗な式に整理する方法の一つであり、それは(A ∧ B) ∨ (C ∧ D) のように、それぞれのグループ内でANDが成立する組み合わせをORで繋げることで True, Falseを表現した式(論理式)である」ということですね!

選言標準形に基づいて、クエリを最大30個の分離に制限する とは

Firestore ドキュメントの引用を改めて確認しましょう。

To prevent a query from becoming too computationally expensive, Cloud Firestore limits how many AND and OR clauses you can combine. To apply this limit, Cloud Firestore converts queries that perform logical OR operations (orin, and array-contains-any) to disjunctive normal form (also known as an OR of ANDs). Cloud Firestore limits a query to a maximum of 30 disjunctions in disjunctive normal form.
(クエリの計算コストが高くなりすぎるのを防ぐため、Cloud FirestoreではAND句とOR句を組み合わせられる数を制限しています。 この制限を適用するために、Cloud Firestoreは論理OR演算(or、in、array-contains-any)を実行するクエリを選言標準形(ANDのORとも呼ばれる)に変換します。 Cloud Firestoreは、選言標準形に基づいて、クエリを最大30個の分離に制限します。)
https://firebase.google.com/docs/firestore/query-data/queries#limits_on_or_queries

さて「クエリを最大30個の分離に制限(limits a query to a maximum of 30 disjunctions)する」とはどういうことでしょうか。

ここで分離と呼んでいるの「disjunctions(論理和)」です。

数理論理学において論理和(ろんりわ、: logical disjunction)とは、与えられた複数の命題のいずれか少なくとも一つが真であることを示す命題を作る論理演算である。離接(りせつ)、選言(せんげん)とも呼ぶ。
https://ja.wikipedia.org/wiki/論理和

「30個の分離」とは「30個の選言(OR)」ということですね。

つまり、Firestoreでは、論理OR演算(or、in、array-contains-any)を実行するクエリを選言標準形(A ∧ B) ∨ (C ∧ D)のような形式)に変換し、最大30個の分離(=選言=OR=「」←これ!)に制限するというルールがあります。

2. Firestore での選言標準形を使った変換

ここまで見てきた通り、Firestoreでは実行するクエリを選言標準形(A ∧ B) ∨ (C ∧ D)のような形式)に変換します。

そしてその結果生まれる分離の数を最大30個までと制限しています。

では実際にどのように選言標準形に変換しているのでしょうか。

これには以下のルールが適用されます:

(1) 平坦化

ネストされた AND 条件を単純化します。

  • 例:

    A AND (B AND C) → A AND B AND C
    

(2) 分配法

数学で学ぶ分配法則と同じ考え方です。ANDOR を展開してすべての条件の組み合わせを列挙するために使われます。

  • 数学の分配法則の例:

    a × (b + c) = (a × b) + (a × c)

Firestore のクエリでも同じように展開します:

  • 例 1:

    A AND (B OR C) → (A AND B) OR (A AND C)
    

    元々の形式を、連言節(ANDでまとめたグループ)に直して、それを選言(OR)で繋げた形式(選言標準形)にしていますね!

  • 例 2:

    (A OR B) AND (C OR D) → (A AND C) OR (A AND D) OR (B AND C) OR (B AND D)
    

3. in の内部処理: 分離句が増える仕組み

さて、ここからが本記事の主題です。

Firestore の公式ドキュメントでは、inarray-contains-anyOR 条件の短縮形 であると説明されており、以下のような注意書きがあります。

警告: 選言標準形への変換は乗算的であるため、複数の OR グループの AND を複数回実行した場合、上限に達する可能性が高くなります。

inarray-contains-any の裏で動いているのは単なる OR 条件です。

各 OR 条件が AND を通じて他の OR 条件と「全ての組み合わせ」を作るため、各 OR 条件に含まれる選択肢の数を掛け算していくことで、全体の分離句が決まります。
掛け算で増えていくので、30という上限にすぐ達するから気をつけてねということです。

例えば:

const query = collection.where('status', 'in', ['active', 'pending', 'archived']);

このクエリは次のように変換されます:

status = 'active' OR status = 'pending' OR status = 'archived'

この場合、分離句の数は 3 つです。

もし複数の in を組み合わせるとどうなるでしょう?例えば:

const query = collection
  .where('status', 'in', ['active', 'pending'])
  .where('role', 'in', ['admin', 'editor']);

Firestore の内部では以下のように変換されます:

(status = 'active' AND role = 'admin') OR
(status = 'active' AND role = 'editor') OR
(status = 'pending' AND role = 'admin') OR
(status = 'pending' AND role = 'editor')

分離句の数は 2 × 2 = 4 つに増加します。


条件がさらに増えた場合

もし次のように条件が増えた場合:

const query = collection
  .where('status', 'in', ['active', 'pending', 'archived'])
  .where('role', 'in', ['admin', 'editor', 'viewer']);

Firestore 内部では次のように展開されます:

(status = 'active' AND role = 'admin') OR
(status = 'active' AND role = 'editor') OR
(status = 'active' AND role = 'viewer') OR
(status = 'pending' AND role = 'admin') OR
(status = 'pending' AND role = 'editor') OR
(status = 'pending' AND role = 'viewer') OR
(status = 'archived' AND role = 'admin') OR
(status = 'archived' AND role = 'editor') OR
(status = 'archived' AND role = 'viewer')

この場合、分離句の数は 3 × 3 = 9 つです。

もし条件がさらに増え、例えば status が 5 個、role が 5 個、location が 5 個の場合、分離句の数は 5 × 5 × 5 = 125 となり、Firestore の制限(30 分離句)を大幅に超えてしまいます。

4. 分離句制限を回避するための工夫

ここまで見てきたように、裏でorが走るinarray-contains-anyを使う際は、分離句の数(=orの数)が簡単に増えすぎるので気を付ける必要があります。

a. クエリを分割する

分離句の数が30を超える可能性がある場合は、複数回に分けてクエリを実行し、結果を統合します。

以下の例は、引数として受け取った任意の都道府県に合致するドキュメントをDBから取得するコード例です。
都道府県は全部で47あるため、受け取る都道府県の数によっては1回のinクエリで分離句制限に引っかかる可能性があります。
このような場合は一度のinクエリで使用する条件を30個以内に分け、分割してクエリを実行し、最後にその結果を統合することで実現できます。

  • 分割例:

    let allResults = [];
    
    // 都道府県を30個ずつに分割する
    const prefectureChunks = [];
    while (prefectures.length > 0) {
        prefectureChunks.push(prefectures.splice(0, 30));
    }
    
    // 分割したそれぞれでクエリを実行する
    for (const chunk of prefectureChunks) {
        const snapshot =
            await db.collection("list").where("prefecture", "in", chunk).get();
        // クエリ結果を統合
        allResults.push(...snapshot.docs);
    }
                        
    

b. クライアントサイドでフィルタリングする

クエリ内で全ての条件を指定せず、まず基本的な条件でデータを取得し、その後クライアントサイドで追加のフィルタリングを行う方法です。

  • Firestore クエリ:

    const query = collection.where('status', 'in', ['active', 'pending']);
    const docs = await query.get();
    
    const filteredResults = docs.docs.filter((doc) => doc.data().department === 'sales');
    

これにより、Firestore 側のクエリを簡潔に保ちながら、柔軟な条件を適用できます。

5. まとめ

Firestore では、inarray-contains-any などのクエリを使用する際、選言標準形(Disjunctive Normal Form, DNF) に変換して処理します。この過程で分離句(論理和)の数が増加し、最大30個という制限に引っかかることがあります。

クエリが複雑になるほど、この制限に達しやすくなるため、次のような回避策を検討しましょう:

  1. クエリを分割する
    • 条件ごとに複数回クエリを実行し、クライアントサイドで結果を統合します。
  2. クライアントサイドでフィルタリング
    • 一旦基本的な条件でデータを取得し、追加の絞り込みをクライアント側で行います。
株式会社melty

Discussion