Chapter 05 · Queries: Filters, Ordering, Limits, Cursors, Collection Groups, and Query Constraints

Equality, Inequality, in / not-in, array-contains / array-contains-any, and Compound Query Semantics

Predict Firestore Core filter semantics across equality, inequality, membership, arrays, compound queries, missing fields, Rules, and Standard-edition constraints.

Advanced beginner110–130 minutesOperator semantics + deterministic query matrixFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

AtlasMart's catalog screen has filters for category, price, publication state, and tags. A relational habit says “write whatever predicate the UI asks for.” Firestore Core queries are different: the operator combination, indexed fields, missing/null values, Rules, and edition all form one executable contract. The goal is to be able to predict the candidate result set before running the SDK call.

01

Explain equality, inequality, in/not-in, array-contains/array-contains-any, and compound filter semantics without treating them as interchangeable syntax.

02

Predict how missing fields, null, arrays, duplicate matches, and Standard-edition disjunction limits alter the result set.

03

Separate query validity from index availability and authorization; a syntactically valid query can still require an index or be denied by Rules.

04

Build a deterministic query matrix over AtlasMart fixtures and assert exact document IDs rather than eyeballing console output.

05

Recognize when an unsupported access pattern should be remodeled or precomputed instead of forced through client-side filtering.

Execution and safety note

Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.

Chapter 05 reproducibility baseline · reviewed 15 September 2026

AtlasMart keeps the same local course environment used in Chapters 01–04: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database for Core-operation labs, Firestore emulator 127.0.0.1:8080, Authentication emulator 127.0.0.1:9099, Emulator UI 127.0.0.1:4000, Firebase CLI 15.30.0, Firebase JavaScript SDK 12.19.0, Firebase Admin Node SDK 14.4.0 (which currently carries @google-cloud/firestore 9.1.0), @firebase/rules-unit-testing 5.0.2, and Node.js 22+. No moving latest tag is required by the lab.

What was and was not executed while authoring

The generation environment does not run the Firebase emulators, so code and commands are checked against current official Firebase/Google Cloud documentation but the output shown in this lesson is an expected invariant, not fabricated captured output. In particular, the Firestore emulator does not enforce production composite indexes in the same way as the managed service. Operator validation, deterministic result ordering, cursor contracts, and Security Rules tests belong in the mandatory local lab; a missing-composite-index error or Query Explain evidence must be treated as a separately labeled production verification.

1. A query is a result-set contract, not an arbitrary predicate

For AtlasMart, where("category", "==", "camera") asks Firestore for documents whose indexed category value equals the string camera. The operator is type-sensitive: an array containing camera is not equal to the scalar string camera. Conversely, array-contains only matches array fields. Query design therefore begins with the stored representation from Chapter 02 and the access-pattern decisions from Chapter 03.

Operator Mental model Important boundary
== Field equals one value Missing field cannot match; type/value must satisfy Firestore comparison semantics.
<, <=, >, >=, != Range or exclusion predicate Inequality affects ordering/index planning; != excludes missing fields.
in OR of equality comparisons on one field Standard: up to 30 comparison values/disjunction budget still applies.
not-in AND of not-equal comparisons on one field Standard: up to 10 values; missing and null behavior is intentionally restrictive.
array-contains Array contains one member At most one per disjunction in Standard.
array-contains-any OR across array membership values Standard: up to 30 values; one matching document appears once even if several values match.
Missing is not null.

A field set to null exists. A field that is absent does not. not-in excludes documents where the target field does not exist, and it does not behave like “everything except these values” over a schemaless universe. Treat field presence as part of the query schema.

2. Compound filters consume both semantic and structural budgets

Multiple where() clauses usually mean logical AND. Firestore also supports logical OR through explicit composite filters and shorthand operators such as in and array-contains-any. In Standard edition, Firestore converts OR-like expressions to disjunctive normal form (DNF) and caps the result at 30 disjunctions. The multiplication can be surprising: two independent in lists can create more disjunctions than either list alone suggests.

Standard also forbids several combinations, including mixing not-in with in, array-contains-any, or or, and allows only one not-in or != per query. Do not memorize these as timeless folklore: they are edition-specific product constraints and must be rechecked when the course is refreshed.

Web modular SDK · query matrix
import { collection, query, where, getDocs } from "firebase/firestore";const items = collection(db, "catalogItems");const contracts = {  cameras: query(items, where("category", "==", "camera")),  priceAtLeast99: query(items, where("price", ">=", 99)),  selectedCategories: query(items, where("category", "in", ["camera", "gateway"])),  notSellerB: query(items, where("sellerId", "not-in", ["seller-b"])),  outdoor: query(items, where("tags", "array-contains", "outdoor")),  cameraOrIot: query(items, where("tags", "array-contains-any", ["camera", "iot"]))};for (const [name, q] of Object.entries(contracts)) {  const snap = await getDocs(q);  console.log(name, snap.docs.map(d => d.id));}

With the deterministic fixture, the important evidence is the exact set of IDs. Ordering is a separate contract covered in Lesson 2; do not accidentally turn whatever iteration order you happened to observe into an application guarantee.

3. Arrays: membership is not array equality

array-contains-any asks whether an array field contains at least one candidate member. An in query can also use arrays as comparison values, but then it compares the whole array value—including length, order, and values. These are different access patterns. A document whose tags are ["outdoor","camera"] can match array-contains("outdoor"); it does not equal the scalar "outdoor".

De-duplication is part of the operator contract.

If one document matches more than one value in an array-contains-any query, that document appears once in the result set. Do not multiply-match it in an application-side billing or ranking estimate.

4. Deliberately wrong approach: overbroad read, then client-side filter

Suppose the UI wants “published camera products owned by seller-a.” A tempting implementation reads all catalogItems, filters in JavaScript, and assumes Security Rules will prevent unauthorized rows from leaking. Rules are not filters. A client query must be authorized for its potential result set; Firestore does not fetch an overbroad result then remove documents that fail Rules.

The repair is to encode the access pattern in the query and data model: query only fields the Rules and indexes can prove, or create a precomputed collection/materialized view if the desired predicate is unsupported or too expensive. If several bounded authorized queries are required, merge their already-authorized results deliberately and account for the extra reads.

5. Hands-on lab: assert the filter contract

shell · pinned local dependencies
mkdir atlasmart-firestore-query-labcd atlasmart-firestore-query-labnpm init -ynpm install firebase@12.19.0 firebase-admin@14.4.0npm install --save-dev firebase-tools@15.30.0 @firebase/rules-unit-testing@5.0.2node --versionnpx firebase --versionnpx firebase emulators:start --only firestore,auth --project demo-atlasmart-firestore
seed-query-fixtures.mjs · deterministic AtlasMart query data
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";import { initializeApp, applicationDefault } from "firebase-admin/app";import { getFirestore, Timestamp } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();const catalog = {  "p-1001": { category:"camera",  price:99,  rating:4.8, tags:["outdoor","camera"], published:true,  rank:10, sellerId:"seller-a" },  "p-1002": { category:"camera",  price:99,  rating:4.5, tags:["studio","camera"],  published:true,  rank:20, sellerId:"seller-a" },  "p-1003": { category:"sensor",  price:49,  rating:4.8, tags:["outdoor","iot"],     published:true,           sellerId:"seller-b" },  "p-1004": { category:"sensor",  price:149, rating:4.1, tags:["industrial","iot"], published:false, rank:null, sellerId:"seller-b" },  "p-1005": { category:"camera",  price:199, rating:4.9, tags:["outdoor","camera"], published:true,  rank:30, sellerId:"seller-c" },  "p-1006": { category:"gateway", price:99,  rating:4.2, tags:["iot"],              published:true,  rank:40, sellerId:"seller-c" }};for (const [id, data] of Object.entries(catalog)) {  await db.collection("catalogItems").doc(id).set({ ...data, updatedAt: Timestamp.fromMillis(1760000000000) });}const orders = [  ["u-alice","o-1001",{ ownerUid:"u-alice", tenantId:"seller-a", status:"paid",    total:198, createdAt:Timestamp.fromMillis(1760000100000) }],  ["u-alice","o-1002",{ ownerUid:"u-alice", tenantId:"seller-b", status:"shipped", total:49,  createdAt:Timestamp.fromMillis(1760000200000) }],  ["u-bob",  "o-1003",{ ownerUid:"u-bob",   tenantId:"seller-a", status:"paid",    total:99,  createdAt:Timestamp.fromMillis(1760000300000) }]];for (const [uid,id,data] of orders) await db.doc(`users/${uid}/orders/${id}`).set(data);console.log("seeded", Object.keys(catalog).length, "catalog items and", orders.length, "orders");
query-contracts.mjs · deterministic ID assertions
import assert from "node:assert/strict";import { initializeApp } from "firebase/app";import { connectFirestoreEmulator, collection, getDocs, getFirestore, query, where } from "firebase/firestore";const app = initializeApp({ projectId:"demo-atlasmart-firestore", apiKey:"demo", appId:"demo" });const db = getFirestore(app); connectFirestoreEmulator(db, "127.0.0.1", 8080);const items = collection(db, "catalogItems");async function ids(q){ return (await getDocs(q)).docs.map(d=>d.id).sort(); }assert.deepEqual(await ids(query(items, where("category","==","camera"))), ["p-1001","p-1002","p-1005"]);assert.deepEqual(await ids(query(items, where("tags","array-contains","outdoor"))), ["p-1001","p-1003","p-1005"]);assert.deepEqual(await ids(query(items, where("category","in",["gateway","sensor"]))), ["p-1003","p-1004","p-1006"]);console.log("filter contracts passed");

Extend the file with one intentionally invalid Standard-edition operator combination and assert that the SDK/backend rejects it. Record the error code and SDK version rather than depending on human-readable wording. Then add a document whose target field is missing and another whose field is null; prove exactly which operators include or exclude each.

Verification boundary.

The emulator can validate many query/operator contracts, but do not use it to prove production composite-index enforcement or billing. Save production index creation/error verification for the explicit optional check in Lesson 4.

Production judgment

A query shape is acceptable only if its semantics, index support, authorization proof, result ordering, expected read scope, and future schema behavior are all explicit. If AtlasMart needs arbitrary full-text, analytical predicates, joins, or unconstrained ad-hoc exploration, that is a signal to add a search/analytics system or evaluate Enterprise Pipeline operations—not to hide an overbroad Firestore read behind client code.

Knowledge check

  1. Why is not-in not equivalent to “all documents except these values”?
  2. How does array-contains-any treat a document that matches two requested array members?
  3. Why can two small in lists still exceed a Standard query limit?
  4. Why is client-side filtering not an authorization strategy?
  5. What evidence should a filter contract test assert?
Review the answers

1. Because missing fields do not match, null has special behavior, and Standard imposes operator-combination/value-count constraints. The stored field-presence contract matters.

2. It returns the document once; matches are de-duplicated at the document-result level.

3. OR-like expressions are converted to DNF; independent choices multiply the number of disjunctions.

4. Security Rules are not post-query row filters. The query itself must be provably authorized for its possible result set.

5. Exact document IDs plus expected rejection cases and missing/null boundary behavior, with SDK/edition/emulator state recorded.

Summary and next step

Filters define the candidate set, but production screens also need deterministic order and pagination. Lesson 2 turns the same fixtures into a stable ordering/cursor contract, including tie values and missing fields.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.