← Vissza a cikkekhez
technologytutorial

Firestore — gondolkodj lekérdezésekben

Zsaga
#featured
Firestore data modeling — kollekciók, dokumentumok, subcollection-ök és a lekérdezés, ami a formát meghatározza
A lekérdezés formája meghatározza az adat formáját — nem fordítva
Mit tanulsz
  • firestoreBevezető
  • data-modelingKözéphaladó

01 — Az alapelv

Először a lekérdezés, aztán az adat

SQL-ben az adatból indulunk: tervezünk egy szép, normalizált modellt, és majd később írunk hozzá lekérdezéseket. Firestore-ban ez fordítva működik — előbb összegyűjtjük a lekérdezéseket, és ezekből vezetjük le, hogyan kell az adatnak kinéznie.
A különbség oka egy fontos technikai sajátosság: Firestore-ban minden lekérdezésnek azonos idő alatt kell lefutnia, függetlenül attól, mekkora az adatbázis. Ezt csak úgy tudja teljesíteni, hogy nem támogatja a join-okat, nem támogatja a tetszőleges aggregációt, és minden olvasott dokumentumért fizetsz. Emiatt egy normalizált modellnél, ahol egyetlen képernyő megrajzolásához 4-5 különböző kollekcióból kell adatot összerakni, a számla szépen kúszik felfelé — minden felhasználói kattintás 4-5 olvasás.
A query-first gondolkodás tehát így néz ki: leírod, milyen képernyők vannak az appodban, mindegyikre milyen adatot kell megjeleníteni, és milyen feltételek mellett. Ezt a listát úgy fogalmazod meg, mintha mindegyiket egyetlen Firestore lekérdezéssel akarnád kiszolgálni — még akkor is, ha még nem tudod, hogyan. Aztán kitalálod, milyennek kell lennie az adatszerkezetnek, hogy ez tényleg igaz legyen.
Példa
SQL-ben elsőre azt mondanád: users tábla, orders tábla, products tábla, és join-olom őket. Firestore-ban először tedd fel a kérdést: „a felhasználó saját rendeléseinek listájához hány olvasásra van szükségem?" Ha a válasz „1", akkor jó modelled van. Ha „N + 1", akkor még gondolkodj.

A négy gyakori lekérdezés-típus

A Firestore lekérdezések szinte mindig az alábbi négy kategória valamelyikébe esnek. Érdemes minden képernyőről eldönteni, melyikbe tartozik, mielőtt az adatszerkezetet véglegesíted:
  • Egyetlen dokumentum lookup — ID alapján egyetlen rekord (1 olvasás).
  • Kollekció szűrés — egy mező értéke alapján (N olvasás, ahol N az eredmény mérete).
  • Kollekció rendezés és szűrés — pl. „aktív rendelések, létrehozási idő szerint csökkenően, top 20" (20 olvasás).
  • Collection group query — minden azonos nevű subcollection (pl. „minden tenant összes rendelése"), egyetlen lekérdezésben.
Ami nem kategória: bármi, ami több mezőre szűr OR-ral, ami join-t igényel, vagy ami egy nagy halmazon aggregál. Ezek nem lehetetlenek — de minden ilyen kompromisszumot tudatosan kell vállalnod.
Mit tanulsz
  • firestoreKözéphaladó

02 — A dokumentum

A dokumentum: egy egység, amit egyben olvasol

A dokumentum a Firestore alapegysége. Egyetlen olvasással egyetlen dokumentumot kapsz — minden mezőjével együtt. Ez egyszerre adottság és tervezési kérdés: mit tegyél bele, és mit ne.

Korlátok, amiket érdemes észben tartani

KorlátÉrték
Maximum dokumentum-méret1 MB
Maximum mezők egy dokumentumban20 000
Maximum mélységi szint (beágyazott objektum)100
Maximum egy tömbbennincs konkrét határ, de a teljes dokumentum max. 1 MB
Maximum dokumentum-írás / másodperc (egy dokumentumra)~1 fenntartható
Maximum tranzakció-méret500 dokumentum

Mit ágyazz be, mit ne

A legfontosabb tervezési döntés: ha egy adat együtt jelenik meg, akkor érdemes együtt tárolni; ha külön, akkor külön. A „rendelés tételei" jó példa: ha a rendelés képernyőn mindig megjeleníted az összes tételt (általában 1-10 tétel egy rendelésen), akkor érdemes a rendelés dokumentumába egy items tömbben tárolni. Egyetlen olvasás, és minden ott van.
Ha viszont egy chat üzeneteit „beágyazod" a beszélgetés dokumentumába, akkor egy aktív beszélgetés egy hét alatt megette az 1 MB-ot — és minden új üzenet teljes dokumentum-újraírást jelent, ami az összes meglévő üzenetet is újraírja. Ide subcollection való.
Döntés

Beágyazás vagy subcollection?

Beágyazás (tömb vagy map)

Ha az elem mindig a parent kontextusában látható, az elemszám kicsi (max ~50), és a parent dokumentum összesen elfér 1 MB-ban. Pl. címek, telefonszámok, rendelés-tételek.

Subcollection

Ha az elemek száma korlátlan, vagy önállóan is le kell kérdezni őket. Pl. üzenetek, log bejegyzések, kommentek, történeti rekordok.

Figyelem
A beágyazott tömböket nehéz részlegesen frissíteni. Új elemet csak arrayUnion()-nal lehet hozzáadni, de egy konkrét elem mező-szintű módosításához az egész tömböt ki kell cserélni. Ha ez gyakori művelet, inkább subcollection legyen — még ha az elemszám kicsi is.
Mit tanulsz
  • subcollectionKözéphaladó

03 — Hierarchia

Subcollection vagy root collection?

Firestore-ban kétféle helyen lehet kollekció: a gyökérben, vagy egy dokumentum alatt (subcollection). Mindkettő működik, és a választás nem mindig egyértelmű — de van néhány konkrét szempont, ami eldönti.
Tegyük fel, hogy felhasználói rendeléseket akarunk tárolni. Két alap-megoldás:
structure-Aplaintext
users / usr_abc                    # root collection
  └── orders / ord_001            # subcollection
  └── orders / ord_002
structure-Bplaintext
users / usr_abc                    # root collection
orders / ord_001                   # separate root collection
  └── userId: "usr_abc"          # field for the relation

Mikor melyik?

  • Subcollection akkor jó, ha az adat csak a parent kontextusában értelmes (pl. egy felhasználó saját címei, egy beszélgetés üzenetei). A struktúra magától beszédes lesz: a users/usr_abc/addresses azonnal mond valamit.
  • Root collection akkor jó, ha az adatot gyakran a parent nélkül kérdezed le. Pl. „minden ma leadott rendelés" — ezt subcollection-ből csak collection group query-vel lehet, ami külön index, és egyébként is ugyanaz, mintha root lenne.
Tévhit
Sokan azt hiszik, hogy ha egy felhasználó dokumentumát törlöd, akkor a subcollection-jei is törlődnek. Nem törlődnek. A Firestore a dokumentumot és a subcollection-jeit külön kezeli — egy subcollection életben marad akkor is, ha a parent doc nem létezik. Ha a teljes ágat törölni akarod, az a te dolgod (Cloud Function-nel batch-ekben, vagy a Firebase CLI firestore:delete rekurzív kapcsolójával).

Collection group query

Ha subcollection-be tetted az adatot, de időnként mégis át kell látnod minden parent-en, a collection group query erre való. Egy lekérdezés, ami minden olyan subcollection-be belenéz, aminek a neve egyezik:
collection-group.tstypescript
import { collectionGroup, query, where, orderBy, limit } from 'firebase/firestore';

// Every order across every user — regardless of hierarchy
const q = query(
  collectionGroup(db, 'orders'),
  where('status', '==', 'active'),
  orderBy('createdAt', 'desc'),
  limit(20)
);
Ehhez egy collection group indexet kell deklarálni az orders névre. Ha sok lekérdezésed parent-független, akkor lehet, hogy egyszerűbb lett volna eleve root collection-ben tartani az adatot, egy userId mezővel.
Mit tanulsz
  • denormalizationKözéphaladó

04 — Denormalizáció

Denormalizáció — amit írsz kétszer, gyorsabban olvasod

SQL-ben a denormalizáció a profi optimalizáció eszköze, valami amit csak végszükségben szabad. Firestore-ban viszont ez az alap-eljárás. A NoSQL alapfilozófia: olcsón olvass, drágán írj — és inkább írj kétszer, mint hogy kétszer olvass.
Klasszikus eset: egy rendelés-listán meg akarod jeleníteni a felhasználó nevét. SQL-ben ez egy JOIN users ON users.id = orders.user_id — egyetlen lekérdezés. Firestore-ban viszont a rendelés dokumentumon csak egy userId mező van. Hogy a nevet is megkapd, minden rendeléshez egy plusz olvasás kell — 20 rendelés listázásához tehát 20 + 20 = 40 olvasás.
order-denormalizedjson
{
  "id": "ord_001",
  "userId": "usr_abc",
  "userName": "Anna Kovács",           // denormalized
  "userEmail": "anna@example.com",      // denormalized
  "total": 12990,
  "createdAt": "2026-04-21T09:14:00Z"
}
Most a 20 rendelés listázása 20 olvasás. A névhez nincs külön lekérdezés. Ára: ha a felhasználó megváltoztatja a nevét, a régi rendeléseken ez nem frissül magától.

Mit kezdj a régi adattal?

Két lehetőség van — és mindkettő helyes lehet, attól függően, mit akarsz:
Stratégia

Frissítjük vagy fagyasztjuk?

Frissítés Cloud Function-nel

Egy onUpdate trigger a /users/{uid}-on, ami minden érintett rendelést újraír. Egyszerű, de drága lehet (sok rendelésnél sok írás). Akkor jó, ha az adat „mindig friss" elvárás (pl. profil oldalon).

Fagyasztott pillanatkép

A régi rendeléseken marad a régi név. Ez sokszor kifejezetten helyes! Egy 2 évvel ezelőtti számlán a vevő akkori neve a jó adat, nem a mostani. Pénzügyi és jogi rekordoknál mindig ezt válaszd.

Részleges denormalizáció

Nem kell az egész user objektumot bemásolni a rendelésbe — csak azokat a mezőket, amiket a rendelés képernyőn megjelenítesz. Ez jellemzően a név, az email, és néha az avatar URL. A többit — felhasználói preferenciákat, beállításokat — hagyd a /users alatt.
Konvenció
Egy denormalizált mezőt érdemes prefixszel jelölni, hogy egyértelmű legyen: ez egy másolat, nem az igazság forrása. Pl. cachedUserName vagy denormUserName. Hat hónap múlva, amikor egy másik fejlesztő nézi, ez segít megérteni, miért van ott.
Mit tanulsz
  • firestoreKözéphaladó

05 — Referenciák

Referenciák — foreign key Firestore-ban

Ha mégis muszáj egy másik dokumentumra hivatkozni, két lehetőség van: egy egyszerű string ID, vagy a Firestore beépített DocumentReference típusa. Mindkettő működik, de általában az egyszerűbb a jobb.
SzempontString IDDocumentReference
TárolásSima stringReference objektum (path)
Méretkicsinagyobb (teljes path)
MigrálhatóságRugalmas (path-független)Path-függő — kollekció átnevezésnél fáj
TípusbiztonságManuálisBeépített, de a path-ot SDK-szinten ismerni kell
Olvashatóság a Console-banCsak az IDKattintható link
A gyakorlatban a string ID szinte mindig nyer. Egyszerűbb, könnyebb migrálni, és ha valaha SQL-be vagy másik adatbázisba portolnád az adatot, akkor a DocumentReference-t úgyis át kell konvertálni. Az egyetlen hely, ahol a DocumentReference határozottan jobb, a Firebase Console-ban való navigálás — egy kattintással ugorhatsz a hivatkozott dokumentumra.

A referencia feloldása: 1 olvasás

Akárhogy is tárolod a hivatkozást, a feloldása mindig egy külön olvasás. Ha egy listán N dokumentumot mutatsz, és minden elemhez kell egy referenciát is feloldani, akkor N + 1 probléma keletkezik (vagy N + N, ha minden elemnek külön referenciája van). Ez ugyanaz a probléma, amit ORM-ekben látunk — a megoldás Firestore-ban: denormalizálj.
Mit tanulsz
  • many-to-manyKözéphaladó

06 — Many-to-many

Many-to-many kapcsolatok — három mintázat

A klasszikus „felhasználó több csoporthoz tartozik, csoport több felhasználót tartalmaz" mintára SQL-ben egy kapcsolótábla a válasz. Firestore-ban három különböző megoldás lehetséges, és egyik sem nyer mindig — méret, lekérdezés-irány és metaadat-igény dönt.

1) Tömb a parent dokumentumban

user-with-arrayjson
{
  "id": "usr_abc",
  "name": "Anna Kovács",
  "groupIds": ["grp_eng", "grp_design", "grp_admin"]
}
A where('groupIds', 'array-contains', 'grp_eng') mindkét irányból működik: meg tudod kérdezni, hogy „melyik csoportokban van a user", és „kik vannak grp_eng-ben". Egyszerű, de van két korlátja: max 256 elem ajánlott egy tömbben (ennél több is mehet, de az index költsége nő), és nem tudsz extra metaadatot tárolni a kapcsolaton (pl. mikor csatlakozott, milyen szerepkörrel).

2) Map a parent dokumentumban

user-with-mapjson
{
  "id": "usr_abc",
  "groups": {
    "grp_eng":    { "role": "admin",  "joinedAt": "2024-08-12" },
    "grp_design": { "role": "member", "joinedAt": "2025-03-04" }
  }
}
Itt extra metaadat is fér, és a lookup gyors (user.groups['grp_eng'].role). Hátránya: nem tudod lekérdezni, hogy „kik az adminok grp_eng-ben" — mert a map kulcsok nem indexelhetők egységesen. Akkor jó, ha mindig a user kontextusából indulsz.

3) Kapcsolótábla (junction collection)

memberships-collectionplaintext
memberships
├── usr_abc__grp_eng                # compound ID
│   ├── userId: "usr_abc"
│   ├── groupId: "grp_eng"
│   ├── role: "admin"
│   └── joinedAt: Timestamp
└── usr_xyz__grp_eng
Ez a klasszikus, SQL-szerű megoldás. Mindkét irányból lekérdezhető (where('userId', '==', ...) vagy where('groupId', '==', ...)), korlátlanul skálázódik, és tárolhat extra mezőket. Ára: minden lekérdezésnél két lépés (előbb a kapcsolótábla, aztán a céldokumentumok), amit denormalizációval lehet csökkenteni.
Mikor melyik?

Many-to-many döntés

Tömb — kis lépték (max ~50-100 elem), mindkét irányból kell lekérdezni, nincs extra metaadat. A legolcsóbb, legegyszerűbb.
Map — kis lépték, kell extra metaadat, de mindig csak a parent kontextusából kérdezed le.
Kapcsolótábla — nagy lépték (több ezer kapcsolat), kell extra metaadat, mindkét irányból le kell kérdezni. Drágább, de skálázódik.
Hibrid — gyakori esetben tömb a parenten + kapcsolótábla a teljes történethez. A tömb adja a gyors lookup-ot, a kapcsolótábla a naplót.
Mit tanulsz
  • aggregationKözéphaladó

07 — Aggregátok

Aggregátok és számlálók

A „hány rendelés van", „mennyi az összes árbevétel", „hányan lájkolták ezt a posztot" — egyszerű kérdések, amik Firestore-ban meglepően sokféleképpen oldhatók meg, és a választás a hívások gyakoriságától függ.

Aggregation queries

A Firestore tudja a count(), sum() és average() műveleteket egy lekérdezésre — ez minden megkezdett 1000 dokumentum után 1 olvasásnak számít:
aggregation.tstypescript
import { collection, query, where, getCountFromServer, getAggregateFromServer, sum } from 'firebase/firestore';

// How many active orders are there?
const q = query(collection(db, 'orders'), where('status', '==', 'active'));
const snap = await getCountFromServer(q);
console.log(snap.data().count);

// What's total revenue?
const agg = await getAggregateFromServer(q, { revenue: sum('total') });
console.log(agg.data().revenue);
Ez nagyon hasznos riportokhoz, dashboardokhoz, ahol a számot ritkán kérdezik le, de pontos érték kell. Élő számlálóhoz (pl. „lájkok száma a poszton, valós időben") nem ez a megoldás.

Mezőszámláló a parent dokumentumon

Ha a számláló sokszor változik, és minden olvasónak látnia kell, akkor az érték a parent dokumentumon él. A poszton egy likeCount mező, amit minden lájk növel:
like-counter.tstypescript
import { doc, updateDoc, increment } from 'firebase/firestore';

await updateDoc(doc(db, 'posts', postId), {
  likeCount: increment(1)
});
Az increment() egy szerver-oldali atomi művelet — a kliensnek nem kell beolvasnia és visszaírnia, így nincs race condition. De: ha a poszt nagyon népszerű és sokan lájkolják ugyanabban a pillanatban, beleütközünk a Firestore „1 write/sec per dokumentum" határába. Ekkor jön a sharding (lásd 08. szekció).
Mit tanulsz
  • shardingHaladó

08 — Hot document

Hot document és sharding

A Firestore minden dokumentumra ~1 write / másodperc fenntartható írást garantál. Ha ennél nagyobb a forgalom egy konkrét dokumentumon, akkor lassul, dobálja az írásokat, és a felhasználói élmény szétesik. A megoldás: szétdaraboljuk az adatot.

Elosztott számláló

A klasszikus eset egy népszerű poszt lájkjai. Egyetlen likeCount mező a posztra, és 100 user párhuzamosan lájkol. A Firestore sorbaállítja a kéréseket, és némelyik write timeout-ot kap. A megoldás: 10 shard-ra szétosztjuk a számlálót, és minden user egy véletlen shardba ír. Az összérték a shardok összege:
distributed-counter.tstypescript
// Structure: posts/{postId}/likeShards/{shardId}
// Each shard: { count: number }

const NUM_SHARDS = 10;

// Like — write to a random shard
async function incrementLike(postId: string) {
  const shardId = Math.floor(Math.random() * NUM_SHARDS).toString();
  const ref = doc(db, `posts/${postId}/likeShards/${shardId}`);
  await updateDoc(ref, { count: increment(1) });
}

// Read — sum every shard
async function getLikeCount(postId: string): Promise<number> {
  const snap = await getDocs(collection(db, `posts/${postId}/likeShards`));
  return snap.docs.reduce((sum, d) => sum + d.data().count, 0);
}
Az eredmény: 10x annyi írási kapacitás (mert már nem 1 dokumentumra megy minden), olvasáskor viszont 10 dokumentumot kell elolvasni 1 helyett. Ha gyakori a lájk és ritka a megjelenítés, ez jó. Ha fordítva (sokszor megjelenítjük, ritkán lájkolják), akkor felesleges — egyetlen mező is elég.
Mikor kell sharding?
Csak akkor, ha tényleg eléred az 1 write/sec határt egy dokumentumon. Egy közepesen népszerű blogposztnál nem kell. Egy live sport-eseményhez kapcsolódó kommentszámlálóhoz, ami másodpercenként 50 új írást kap, mindenképp.

Idő alapú particionálás

Egy másik klasszikus eset: napi statisztika. Egyetlen /stats/2026-04-28 dokumentumra írnánk minden hibalogot. De ha napi 100 ezer hiba van, az 1 dokumentumra ennyi write nem fér bele. Megoldás: óránkénti vagy 5-perces particionálás:
stats-partitionedplaintext
stats
├── 2026-04-28-h00          # 0:00 - 0:59
├── 2026-04-28-h01
├── 2026-04-28-h02
└── ...
Egy óra alatt szétoszlik az írási terhelés, és a napi összeg lekérdezése egy where szűrés a 24 dokumentumra.
Mit tanulsz
  • paginationKözéphaladó

09 — Pagináció

Pagináció és időalapú lekérdezések

Firestore-ban nincs offset alapú pagináció — és ez nem hiányosság, hanem szándékos. A cursor-alapú megközelítés valós időben is működik, és nagy adathalmazon sokkal hatékonyabb.

Cursor alapú pagináció

paging.tstypescript
import { collection, query, orderBy, limit, startAfter, getDocs } from 'firebase/firestore';

// First page
const first = query(
  collection(db, 'orders'),
  orderBy('createdAt', 'desc'),
  limit(20)
);
const snap = await getDocs(first);
const lastDoc = snap.docs[snap.docs.length - 1];

// Next page — continue from here
const next = query(
  collection(db, 'orders'),
  orderBy('createdAt', 'desc'),
  startAfter(lastDoc),
  limit(20)
);
A startAfter() egy konkrét dokumentumtól folytatja a lekérdezést. Ez azért hatékony, mert a Firestore az indexen lépked, és nem kell a már átugrott rekordokat „beolvasnia". Egy SQL OFFSET 10000 minden átugrott sort érint — Firestore-ban a cursor csak megmondja, hol folytassa.

Idő alapú lekérdezések — szervesen passzol

Mivel Firestore-ban a cursor egy konkrét érték (egy dokumentum, vagy egy szám/timestamp), könnyű olyan paginációt csinálni, ahol nem oldal-számokkal, hanem időszelettel dolgozol: „mutasd a múlt heti rendeléseket, 50-esével". A timestamp az index része lesz, és az új írások valós időben becsorognak a már nyitott listenerbe is.
Tipp
Ha a lista UI-ja realtime listenerre épül (onSnapshot), és lapozható, akkor minden „új oldal" egy újabb listener — vagy elengeded a régieket, vagy a memóriahasználat növekszik. A Firestore SDK ezt nem oldja meg helyetted.
Mit tanulsz
  • multi-tenancyHaladó

10 — Multi-tenancy

Multi-tenancy — négy stratégia

Ha SaaS terméket építesz, ahol több cég (tenant) használja ugyanazt az alkalmazást, az adat szétválasztásának módja befolyásolja a biztonságot, a költséget, a teljesítményt és a GDPR-megfelelést is. Négy alapminta van.

1) Egy projekt egy tenantnak — teljes szigetelés

Minden tenant kap egy saját Firebase projektet. Teljes adatszigetelés, külön számlázás, külön Auth pool. Hátránya: drága, ha sok kis tenant van — minden projekt önálló GCP entitás, külön karbantartás. Akkor érdemes, ha a tenant-jaid nagy ügyfelek (enterprise SaaS), vagy ha jogi okból kell a teljes adatszigetelés (egészségügy, pénzügy).

2) Közös kollekció + tenantId mező

shared-tenantIdjson
// One /orders root collection, every doc has a tenantId field
{
  "id": "ord_001",
  "tenantId": "acme-corp",
  "userId": "usr_abc",
  "total": 12990
}
Olcsó, egyszerű implementáció, de minden lekérdezésen where('tenantId', '==', currentTenant) kell, és a Security Rules-ban is meg kell védeni. Ha valaki elfelejti a szűrést, az adatok keveredhetnek — kockázatos.

3) Tenantonként egy subcollection

tenant-subcollectionplaintext
tenants
├── acme-corp
│   ├── orders / ord_001
│   ├── products / prod_001
│   └── members / usr_abc
└── beta-inc
    └── ...
A path-ban benne van a tenant ID, így a lekérdezés szintaxisa magától garantálja a szigetelést. A Security Rules is egyszerűbb: match /tenants/{tid}/orders/{oid}, és csak az adott tenant tagjai férnek hozzá. Ez a leggyakoribb választás közepes SaaS-ekhez, mert egyszerre olcsó, biztonságos és átlátható.

4) Identity Platform tenants

A Firebase Auth Identity Platform Blaze planon támogatja a több bérlős hitelesítést — minden tenant saját user-poolt kap, külön bejelentkezési providerekkel. Az adat-szétválasztást viszont neked kell megoldani (általában a 3. stratégiával kombinálva). Akkor jó, ha a tenant-eknek külön SSO providere kell, vagy ha jogi szempontból külön kell tartani a felhasználói adatokat is.
StratégiaSzigetelésKöltségSkálázódásGDPR egyszerűség
Egy projekt egy tenantnakTeljesMagasKorlátos (sok tenant esetén)Egyszerű (törléssel)
Közös + tenantIdLogikaiAlacsonyKiválóBonyolult
Tenant subcollectionPath-szintűAlacsonyKiválóKözepes
Identity PlatformAuth-szintűKözepesKiválóEgyszerű
Mit tanulsz
  • data-modelingHaladó

11 — Példa

Egy SaaS rendelési rendszer — végigfutó példa

Most egy konkrét példában rakjuk össze az egészet. Egy multi-tenant SaaS, ahol cégek tartanak rendeléseket — felhasználókkal, termékekkel, számlákkal. A célunk, hogy minden képernyőt 1-2 olvasással ki tudjunk szolgálni.

A lekérdezések, amiket ki kell szolgálni

  1. Egy tenant utolsó 30 napjának rendeléslistája, dátum szerint csökkenően.
  2. Egy konkrét felhasználó saját rendelései.
  3. Egy termék készletének ellenőrzése a rendeléskor.
  4. Havi árbevétel egy tenantra (riport).
  5. Top 10 legnépszerűbb termék egy tenantnál.

A struktúra

A 10. szekció alapján tenantonkénti subcollection-t választunk. A modell:
data-modelplaintext
tenants / {tid}
├── name, plan, createdAt
├── members / {uid}                # tenant-bound user
│   ├── role: 'admin' | 'user'
│   ├── displayName, email          # denormalized from /users
│   └── joinedAt
├── products / {pid}
│   ├── name, price, stock
│   ├── purchaseCount               # denormalized, for top lists
│   └── updatedAt
├── orders / {oid}
│   ├── userId, userDisplayName     # denormalized
│   ├── items: [                    # embedded array, max ~50 items
│   │     { productId, name, price, qty }
│   │   ]
│   ├── total, status, createdAt
│   └── month: '2026-04'             # precomputed for reports
└── monthlyStats / {YYYY-MM}        # aggregated monthly data
    └── revenue, orderCount

A lekérdezések leképezése

QueryImplementációOlvasás
1. Tenant rendelései, 30 naptenants/acme/orders
+ where('createdAt', '>', d30)
+ orderBy('createdAt', 'desc')
+ limit(50)
50
2. Egy user rendeléseitenants/acme/orders
+ where('userId', '==', uid)
+ orderBy('createdAt', 'desc')
N (eredmény)
3. Termék készletEgyszerű getDoc(tenants/acme/products/p123)1
4. Havi árbevételtenants/acme/monthlyStats/2026-04 egyetlen doc1
5. Top 10 terméktenants/acme/products
+ orderBy('purchaseCount', 'desc')
+ limit(10)
10
Mind az öt lekérdezés vagy egyetlen olvasás, vagy a megjelenített elemek számával egyenlő olvasásszám. Ez a query-first tervezés célja — nincs „N + 1", nincsenek rejtett aggregációk, ami a skálázódást elrontaná.

Háttér-szinkronizáció Cloud Function-nel

A denormalizációkat és aggregátokat egy onWrite(/tenants/{tid}/orders/{oid}) trigger tartja karban:
order-trigger.ts (Cloud Function)typescript
import { onDocumentWritten } from 'firebase-functions/v2/firestore';
import { getFirestore, FieldValue } from 'firebase-admin/firestore';

export const onOrderWritten = onDocumentWritten(
  'tenants/{tid}/orders/{oid}',
  async (event) => {
    const db = getFirestore();
    const tid = event.params.tid;
    const before = event.data?.before.data();
    const after  = event.data?.after.data();

    // 1) Update monthly stats
    if (after) {
      const month = after.month;
      const diff = (after.total ?? 0) - (before?.total ?? 0);
      await db.doc(`tenants/${tid}/monthlyStats/${month}`).set({
        revenue: FieldValue.increment(diff),
        orderCount: FieldValue.increment(before ? 0 : 1),
      }, { merge: true });
    }

    // 2) Product purchase counters
    if (!before && after) {
      for (const item of after.items) {
        await db.doc(`tenants/${tid}/products/${item.productId}`).update({
          purchaseCount: FieldValue.increment(item.qty),
          stock: FieldValue.increment(-item.qty),
        });
      }
    }
  }
);
Az írási költséget elfogadjuk, mert a riport-lekérdezés ára ez. Ha nem így csinálnánk, akkor a havi árbevétel-lekérdezés egy sum() aggregation lenne minden olvasáskor — sokszorosa a tárolási és olvasási költségnek hosszú távon.

A jó adatmodell minden lekérdezést két számjegynyi olvasással kiszolgál. Ha ezt nem tudja, akkor még nem ér véget a tervezés.

Mit tanulsz
  • data-modelingHaladó

12 — Konklúzió

Tervezési checklist

Mielőtt egy új kollekciót létrehozol Firestore-ban, érdemes ezeken a pontokon végigmenni. Ha valamelyik piros, az nem feltétlenül baj — de tudatos döntés legyen, ne véletlen.
  • Felírtam a top 5-10 lekérdezést, amit ennek a kollekciónak ki kell szolgálnia. Mindegyikhez tudom, hány olvasást igényel.
  • Van olyan lekérdezés, ami N + 1 problémát okoz? Ha igen, denormalizációval megoldható-e?
  • A dokumentum mérete a tervezett tartalommal nem fogja közelíteni az 1 MB-os határt egy év múlva sem?
  • Ha beágyazott tömböt használok, az elemszáma korlátos (max ~50-100), és az elemek ritkán módosulnak részlegesen?
  • Subcollection esetén tudom, hogy a parent dokumentum törlése nem érinti a subcollection-t — és van-e tervem a takarításra?
  • Van-e olyan dokumentum, ami másodpercenként többször írásra kerülne? Ha igen, sharding kell.
  • Multi-tenant rendszer esetén az adatszigetelés a path-ban vagy a tenantId mezőben van? Mindkét esetben fedi-e a Security Rules?
  • Ha denormalizált adatokat használok, dokumentáltam, hogy melyik mező a forrás, és melyik a másolat?
  • Van-e olyan riport-lekérdezés, ami sok dokumentumot olvas? Ha igen, érdemesebb-e előre aggregálni Cloud Function-nel?
  • Az indexek deklarálva vannak a firestore.indexes.json-ban, és nem hagyatkozom a Console „1-click" javaslataira élesben?
A Firestore data modeling nem nehéz — csak más, mint amit SQL-ből megszoktunk. Ha az alapelvet (előbb a lekérdezés, aztán az adat) mindig betartod, és nem félsz denormalizálni, a legtöbb esetben olcsó és gyors rendszert kapsz. A következő cikkben a Security Rules-okat vesszük mélyebbre — mert akármilyen jó az adatmodell, ha a hozzáférés-szabályozás rossz, az adat nyitva van.
Vissza az elejére