FastAPI बैकएंड डेवलपमेंट बूटकैंप · पाठ

DataLoaders से N+1 क्वेरी हल करना

रिज़ॉल्वर में N+1 क्वेरी विस्फोट समाप्त करने के लिए dataloaders से डेटाबेस लुकअप को बैच और कैश कीजिए।

पाठ 2, कुल 4 में से13 चरण

DataLoaders से N+1 क्वेरी हल करना, CoddyKit पर FastAPI बैकएंड डेवलपमेंट बूटकैंप का एक निःशुल्क पाठ है। यह 4 में से 2वाँ पाठ है। इस अध्ययन पथ के 3 तक कोई भी पाठ पूरा पढ़ना निःशुल्क है — इसके बाद CoddyKit PRO हर पाठ अनलॉक करता है, साथ ही अंतर्निर्मित कोड संपादक और चौबीसों घंटे एआई शिक्षक के साथ व्यावहारिक अभ्यास भी उपलब्ध कराता है। यह FastAPI बैकएंड डेवलपमेंट बूटकैंप सीखने के मार्ग का हिस्सा है और आपकी प्रगति वेब तथा CoddyKit ऐप पर सिंक होती रहती है। FastAPI बैकएंड डेवलपमेंट बूटकैंप पाठ्यक्रम में कुल 4 पाठ शामिल हैं।

GraphQL में N+1 समस्या

GraphQL clients को एक ही request में nested data माँगने देता है, जैसे posts की list और प्रत्येक post का author। खतरा resolvers में छिपा होता है।

मान लें कि आप 1 query से 100 posts fetch करते हैं, फिर प्रत्येक post के author को resolve करने के लिए हर post पर एक query चलाते हैं। कुल 1 + 100 = 101 queries होती हैं — यही प्रसिद्ध N+1 problem है।

  • List load करने के लिए 1 query (यह 1 है)
  • Related field load करने के लिए हर item पर एक, यानी N queries (यह N है)

बड़े स्तर पर यह latency को बहुत बढ़ा देता है और database पर अत्यधिक भार डालता है। DataLoaders इसका मानक समाधान हैं।

Strawberry Resolver में N+1 देखना

यह एक सरल Strawberry resolver है जो N+1 पैदा करता है। प्रत्येक author resolver अपना अलग database call करता है।

यदि कोई query 50 posts लौटाती है, तो यह author resolver 50 अलग-अलग SELECT statements चलाएगा। List query और वे 50 lookups मिलकर N+1 explosion बनाते हैं।

import strawberry

@strawberry.type
class Author:
    id: int
    name: str

@strawberry.type
class Post:
    id: int
    title: str
    author_id: int

    @strawberry.field
    async def author(self) -> Author:
        # BAD: one DB round-trip per post -> N+1
        row = await db.fetch_one(
            "SELECT id, name FROM authors WHERE id = :id",
            {"id": self.author_id},
        )
        return Author(id=row["id"], name=row["name"])

मुख्य विचार: Batch और Cache

एक DataLoader दो तकनीकों से N+1 समस्या हल करता है:

  • Batching: प्रत्येक author_id को तुरंत resolve करने के बजाय, loader event loop के एक tick के दौरान माँगी गई सभी keys इकट्ठी करता है और एक batched query में उन्हें साथ resolve करता है (जैसे WHERE id = ANY(...))।
  • Caching: एक ही request के भीतर वही key केवल एक बार fetch की जाती है। Author 7 के लिए दस बार पूछने पर एक ही lookup होता है।

परिणाम: posts के लिए 1 query + सभी authors के लिए 1 batched query = 101 के बजाय 2 queries।

Event Loop पर Batching कैसे काम करती है

Strawberry का DataLoader asyncio event loop पर निर्भर करता है। जब कई resolvers loader.load(key) call करते हैं, तो loader तुरंत नहीं चलता। यह प्रत्येक key दर्ज करता है और एक pending awaitable लौटाता है।

अगले tick पर loader queue की गई हर key लेता है, पूरी keys list के साथ आपके batch function को एक बार call करता है और फिर प्रत्येक individual awaitable को उसके matching result से resolve करता है।

इसीलिए DataLoaders केवल async code में काम करते हैं: defer करने की यह व्यवस्था इस बात पर निर्भर करती है कि loop वर्तमान synchronous work समाप्त होने के बाद batch dispatch schedule करे।

Batch Load Function लिखना

DataLoader का केंद्र batch function है। इसे keys की एक list मिलती है और इसे keys के बिल्कुल उसी क्रम में results की एक list लौटानी होती है।

दो अनिवार्य नियम:

  • लौटाई गई list की लंबाई keys की लंबाई के बराबर होनी चाहिए।
  • Index i पर result, keys[i] से संबंधित होना चाहिए। Missing rows को None (या Exception) में map करें, कभी हटाएँ नहीं।

नीचे हम rows को id के अनुसार map करते हैं और फिर उन्हें keys के क्रम में दोबारा निकालते हैं।

from typing import List, Optional

async def load_authors(keys: List[int]) -> List[Optional[Author]]:
    rows = await db.fetch_all(
        "SELECT id, name FROM authors WHERE id = ANY(:ids)",
        {"ids": keys},
    )
    by_id = {row["id"]: Author(id=row["id"], name=row["name"]) for row in rows}
    # Preserve order; None for missing keys
    return [by_id.get(key) for key in keys]

Order Alignment का प्रदर्शन

क्रम-संरक्षण अनुबंध DataLoader की त्रुटियों का सबसे आम स्रोत है। यहाँ एक स्वतंत्र अनुकरण दिया गया है: डेटाबेस से पंक्तियाँ किसी भी क्रम में आती हैं, लेकिन हमें उन्हें अनुरोधित कुंजियों के अनुरूप क्रम में लौटाना होता है।

इसे चलाकर देखें कि लुकअप डिक्शनरी और कुंजी-क्रम वाली कॉम्प्रिहेंशन किस तरह सही संरेखण सुनिश्चित करते हैं, भले ही DB पंक्तियाँ गलत क्रम में लौटाए या किसी अनुपलब्ध कुंजी को छोड़ दे।

def batch_load(keys, rows):
    by_id = {row["id"]: row["name"] for row in rows}
    return [by_id.get(k) for k in keys]

keys = [3, 1, 7, 4]
# DB returns rows shuffled and is missing id=7
rows = [
    {"id": 1, "name": "Ada"},
    {"id": 4, "name": "Linus"},
    {"id": 3, "name": "Grace"},
]

result = batch_load(keys, rows)
print(result)  # ['Grace', 'Ada', None, 'Linus']
assert len(result) == len(keys)
for key, name in zip(keys, result):
    print(f"key={key} -> {name}")

स्ट्रॉबेरी में DataLoader बनाना

स्ट्रॉबेरी के साथ DataLoader वर्ग मिलता है। आप इसे अपने बैच फ़ंक्शन के साथ बनाते हैं। .load(key) को कॉल करने पर ऐसा awaitable मिलता है जिसका परिणाम बैचिंग पूरी होने के बाद प्राप्त होता है।

महत्वपूर्ण बात यह है कि DataLoader इंस्टेंस अपने-अपने स्तर पर कैश रखता है। आपको हर अनुरोध के लिए नया लोडर बनाना होगा, ताकि पुराना डेटा और अलग-अलग उपयोगकर्ताओं के डेटा का आपस में रिसाव कभी न हो। अब हम इसे context के माध्यम से जोड़ेंगे।

from strawberry.dataloader import DataLoader

# batch function from the previous scene
author_loader = DataLoader(load_fn=load_authors)

# Inside a resolver you would now write:
#   author = await author_loader.load(self.author_id)
# Many concurrent .load() calls collapse into ONE call to load_authors.

GraphQL Context के ज़रिए हर अनुरोध के लोडर

अनुरोध-सीमा वाले लोडर रखने के लिए GraphQL का context सबसे उपयुक्त स्थान है। FastAPI + स्ट्रॉबेरी में आप get_context को ओवरराइड करके हर अनुरोध पर नए लोडर बना सकते हैं।

इससे बैच विंडो और कैश केवल एक ही अनुरोध तक सीमित रहते हैं — ठीक उतनी ही अवधि जितनी आपको चाहिए।

from strawberry.fastapi import GraphQLRouter
from strawberry.dataloader import DataLoader

async def get_context() -> dict:
    return {
        "author_loader": DataLoader(load_fn=load_authors),
        # one loader per relation, all rebuilt per request
    }

graphql_app = GraphQLRouter(schema, context_getter=get_context)
# app.include_router(graphql_app, prefix="/graphql")

रिज़ॉल्वर के अंदर लोडर का उपयोग

अब author रिज़ॉल्वर info.context से लोडर पढ़ता है और .load() को कॉल करता है। जब आप info को पैरामीटर के रूप में घोषित करते हैं, तो स्ट्रॉबेरी उसे स्वतः उपलब्ध कराता है।

हालाँकि यह रिज़ॉल्वर हर पोस्ट के लिए एक बार चलता है, सभी .load() कॉल एक ही SELECT ... WHERE id = ANY(...) में बैच हो जाती हैं — N+1 समस्या समाप्त हो जाती है।

import strawberry
from strawberry.types import Info

@strawberry.type
class Post:
    id: int
    title: str
    author_id: int

    @strawberry.field
    async def author(self, info: Info) -> Author:
        loader = info.context["author_loader"]
        return await loader.load(self.author_id)

कैशिंग के लाभ और उनकी सीमाएँ

एक ही अनुरोध के भीतर लोडर कुंजी के आधार पर कैश करता है, इसलिए बार-बार होने वाली load(7) कॉल डेटाबेस को केवल एक बार पढ़ती हैं। यह उन विस्तार वाली क्वेरी के लिए बहुत उपयोगी है जिनमें कई पोस्ट में वही लेखक दिखाई देता है।

इन बातों का ध्यान रखें:

  • डिज़ाइन के अनुसार कैश हर अनुरोध के लिए अलग होता है — लोडर को कभी भी अलग-अलग अनुरोधों के बीच साझा न करें, वरना पुराना डेटा परोसा जाएगा।
  • यदि अनुरोध के बीच कोई रिकॉर्ड बदल जाए और आप उसे फिर से पढ़ें, तो आपको कैश की गई प्रति मिलेगी। अमान्य करने के लिए परिवर्तन के बाद loader.clear(key) कॉल करें।
  • कैश कुंजी मूल कुंजी का मान होती है, इसलिए कुंजियों को हैश योग्य और एकसमान रखें (जैसे हमेशा int, कभी-कभी str नहीं)।

संग्रह और Tuple कुंजियाँ लोड करना

DataLoaders केवल एक-से-एक लुकअप के लिए नहीं होते। एक-से-अनेक संबंध (जैसे किसी पोस्ट की comments) के लिए बैच फ़ंक्शन हर कुंजी के लिए एक सूची लौटाता है। विदेशी कुंजी के आधार पर पंक्तियों को समूहित करें, फिर हर अनुरोधित कुंजी के लिए एक सूची दें (कुछ न मिलने पर खाली सूची)।

संयुक्त लुकअप के लिए हैश योग्य tuple को कुंजी के रूप में उपयोग करें, जैसे (post_id, locale)। कैशिंग सही रखने के लिए उसका प्रकार हमेशा एकसमान रखें।

from collections import defaultdict

async def load_comments(post_ids):
    rows = await db.fetch_all(
        "SELECT id, post_id, body FROM comments WHERE post_id = ANY(:ids)",
        {"ids": post_ids},
    )
    grouped = defaultdict(list)
    for row in rows:
        grouped[row["post_id"]].append(row)
    # one list per key, in key order
    return [grouped.get(pid, []) for pid in post_ids]

त्वरित जाँच: DataLoader की अवधि

एक सहकर्मी "मेमोरी बचाने" के लिए मॉड्यूल-स्तर पर एक ही DataLoader बनाकर पूरे ऐप में उसका पुनः उपयोग करता है। अनेक उपयोगकर्ताओं वाली FastAPI GraphQL सेवा के लिए यह गलत विकल्प क्यों है?

पुनरावलोकन: DataLoaders ने N+1 को समाप्त किया

आपने सीखा कि स्ट्रॉबेरी + FastAPI रिज़ॉल्वर में N+1 क्वेरी-विस्फोट को कैसे समाप्त किया जाता है:

  • N+1 तब होता है जब कोई नेस्टेड रिज़ॉल्वर हर मूल आइटम के लिए एक क्वेरी चलाता है।
  • DataLoader एक ही इवेंट-लूप टिक से मिली सभी कुंजियों को एक क्वेरी में बैच करके और अनुरोध के भीतर दोहराई गई कुंजियों को कैश करके इसे ठीक करता है।
  • बैच फ़ंक्शन को इनपुट कुंजियों के अनुरूप परिणाम लौटाने चाहिए: लंबाई समान हो, क्रम समान हो, और न मिलने पर None या खाली सूचियाँ हों।
  • get_context में हर अनुरोध के लिए लोडर बनाएँ और रिज़ॉल्वर के भीतर उन्हें info.context से पढ़ें।
  • एक-से-अनेक संबंधों के लिए हर कुंजी की अलग सूची और संयुक्त लुकअप के लिए हैश योग्य tuple कुंजियों का उपयोग करें; परिवर्तनों के बाद clear() कॉल करें।

इस पैटर्न से गहराई तक नेस्ट की गई GraphQL क्वेरी तेज़ रहती हैं और आपका डेटाबेस भी बिना अनावश्यक दबाव के काम करता है।

शुरुआत निःशुल्क

एआई शिक्षक के साथ FastAPI बैकएंड डेवलपमेंट बूटकैंप सीखें — निःशुल्क

अपने ब्राउज़र में वास्तविक कोड लिखें और चलाएँ, चौबीसों घंटे एआई शिक्षक से तुरंत सहायता पाएँ, और वेब या ऐप पर वहीं से शुरू करें जहाँ आपने छोड़ा था।

पाठ्यक्रम
21
पाठ
84

अक्सर पूछे जाने वाले प्रश्न

क्या “DataLoaders से N+1 क्वेरी हल करना” पाठ निःशुल्क है?

हाँ — FastAPI बैकएंड डेवलपमेंट बूटकैंप अध्ययन पथ के 3 तक कोई भी पाठ, जिसमें “DataLoaders से N+1 क्वेरी हल करना” भी शामिल है, यहाँ वेब पर पूरा पढ़ना निःशुल्क है। इसके बाद CoddyKit PRO हर पाठ अनलॉक करता है, साथ ही अंतर्निर्मित कोड संपादक और चौबीसों घंटे एआई शिक्षक के साथ इंटरैक्टिव अभ्यास भी उपलब्ध कराता है। FastAPI बैकएंड डेवलपमेंट बूटकैंप पाठ्यक्रम में कुल 4 पाठ शामिल हैं।

“DataLoaders से N+1 क्वेरी हल करना” में मैं क्या सीखूँगा?

रिज़ॉल्वर में N+1 क्वेरी विस्फोट समाप्त करने के लिए dataloaders से डेटाबेस लुकअप को बैच और कैश कीजिए। आप ब्राउज़र में सीधे चलाए जाने वाले व्यावहारिक कोड के साथ FastAPI बैकएंड डेवलपमेंट बूटकैंप का अभ्यास करते हैं, और पाठ पूरा करते समय 24/7 एआई ट्यूटर आपके प्रश्नों के उत्तर देता है।

क्या FastAPI बैकएंड डेवलपमेंट बूटकैंप शुरू करने के लिए मुझे किसी अनुभव की आवश्यकता है?

पहले के अनुभव की आवश्यकता नहीं है। CoddyKit पर FastAPI बैकएंड डेवलपमेंट बूटकैंप शुरुआती से लेकर उन्नत शिक्षार्थियों तक सभी के लिए व्यवस्थित किया गया है, इसलिए आप यहीं से या शुरुआत से सीखना शुरू कर सकते हैं और अपनी गति से आगे बढ़ सकते हैं। यह 4 में से 2वाँ पाठ है।

“DataLoaders से N+1 क्वेरी हल करना” पाठ पूरा करने में कितना समय लगता है?

CoddyKit का अधिकांश पाठ लगभग 5–10 मिनट में पूरा हो जाता है। हर पाठ छोटा और संवादात्मक है, इसलिए आप लगातार प्रगति करते हैं और वेब या ऐप पर वहीं से सीखना जारी रख सकते हैं जहाँ आपने छोड़ा था।

क्या मैं इस FastAPI बैकएंड डेवलपमेंट बूटकैंप पाठ में कोड लिख और चला सकता हूँ?

हाँ। हर FastAPI बैकएंड डेवलपमेंट बूटकैंप पाठ में एक अंतर्निर्मित कोड संपादक शामिल है, जिससे आप सीधे अपने ब्राउज़र में वास्तविक कोड लिख और चला सकते हैं और तुरंत एआई प्रतिक्रिया पा सकते हैं—स्थानीय सेटअप की आवश्यकता नहीं है।

इस पाठ्यक्रम के सभी पाठ

  1. प्रकार, क्वेरी और म्यूटेशन निर्धारित करना
  2. DataLoaders से N+1 क्वेरी हल करना
  3. रीयल-टाइम GraphQL सब्सक्रिप्शन
  4. क्वेरी लागत विश्लेषण और गहराई सीमा
← FastAPI बैकएंड डेवलपमेंट बूटकैंप पर वापस जाएँ