2025, Oct 23 09:30
Django ManyToMany में AND फ़िल्टर के लिए annotate Count(distinct) वाला सेट-आधारित तरीका
जानें कि Django में ManyToMany पर AND फ़िल्टर कैसे बनाएं: annotate और Count(distinct) से एक क्वेरी में सेट-आधारित समाधान, लूप/उप-क्वेरी से बचें और बड़े इनपुट पर तेज़.
जब ManyToMany संबंध शामिल होता है, तो AND-शैली का फ़िल्टर बनाना अक्सर सहज नहीं होता। संबंधित रिकॉर्ड्स में से किसी एक से मेल खाने वाली ऑब्जेक्ट्स चुनना आसान है, लेकिन सभी दिए गए संबंधित मानों से मेल की शर्त डेवलपर्स को अक्सर प्रति-आइटम उप-क्वेरी जैसे अल्प-प्रभावी तरीकों की ओर धकेल देती है। यहाँ एक साफ-सुथरा, सेट-आधारित तरीका है जो एक ही क्वेरी में रहता है और बड़े इनपुट पर लूपिंग से बचाता है।
समस्या की रूपरेखा
दो मॉडलों की कल्पना करें जो ManyToMany के जरिए जुड़े हैं: एक ऐसा जो नामों वाले आइटम दर्शाता है, और दूसरा ऐसे लोकेशन (स्थानों) का जो उन आइटम्स को रखते हैं। काम यह है कि दी गई नामों की सूची के अनुसार वे सभी लोकेशन निकाले जाएँ जिनमें वे आइटम मौजूद हों—OR अर्थ (कोई भी एक मेल) और AND अर्थ (सभी से मेल अनिवार्य) दोनों का समर्थन करते हुए।
class Volume(Model):
    name = CharField(...)
    ...
class Outlet(Model):
    volumes = ManyToManyField('Volume', blank=True, related_name='outlets')
    ...
OR क्वेरी सीधी है: यदि कोई लोकेशन दिए गए नामों में से कम से कम एक से जुड़ा है, तो वह परिणाम में आ जाएगा।
Outlet.objects.filter(volumes__name__in=item_names)
AND क्वेरी पर आते ही चीज़ें पेचीदा होती हैं। एक भोला तरीका यह है कि नामों पर लूप चलाकर शर्तों को चेन किया जाए और परिणामों का प्रतिच्छेद लिया जाए—यह हर नाम पर कई JOIN या उप-क्वेरी बनाता है और स्केल पर अच्छा प्रदर्शन नहीं करता।
from django.db.models import Q
clauses = Q()
for nm in name_list:
    clauses &= Q(id__in=Volume.objects.get(name=nm).outlets)
Outlet.objects.filter(clauses)
सरल AND कहाँ टूटता है
हर मान के लिए अलग शर्त जोड़ते जाना बढ़ते हुए JOINs और nested lookups की कड़ी में बदल जाता है। कुछ ही मानों के साथ भी यह उपयुक्त नहीं; और जब उपयोगकर्ता इनपुट बड़ा हो सकता है, तो यह तरीका जल्द ही प्रदर्शन समस्या बन जाता है। साथ ही, एक ही रिलेशन पर कई filters से AND व्यक्त करने की सरल कोशिशें अक्सर OR जैसी प्रवृत्ति पर लौट आती हैं या केवल एक संबंधित पंक्ति तक ही सीमित रह जाती हैं।
एग्रीगेशन के साथ सेट-आधारित समाधान
मुख्य विचार है कि समस्या को “कितने मेल हुए” तक घटा दिया जाए। पहले, डुप्लिकेट को संभालने के लिए आने वाले नामों को सेट में बदलें। फिर, उन्हीं नामों से आउटलेट्स को फ़िल्टर करें, मेल खाने वाले संबंधित volumes की संख्या annotate करें, और गिनती को इनपुट सेट के आकार के बराबर होने की शर्त लगाएँ। यही समानता बिना प्रति-आइटम लूप के AND शर्त को लागू कर देती है।
from django.db.models import Count
name_pool = set(item_names)
Outlet.objects.filter(
    volumes__name__in=name_pool
).annotate(
    match_count=Count('volumes')
).filter(
    match_count=len(name_pool)
)
यदि क्वेरी में अतिरिक्त JOINs जुड़ने से डुप्लिकेट पंक्तियाँ बनने की संभावना हो, तो एग्रीगेशन को सही रखने के लिए distinct गिनती का उपयोग करें।
from django.db.models import Count
name_pool = set(item_names)
Outlet.objects.filter(
    volumes__name__in=name_pool
).annotate(
    match_count=Count('volumes', distinct=True)
).filter(
    match_count=len(name_pool)
)
इससे क्या हासिल होता है
यह रणनीति AND के अर्थ को एक ही रिलेशनल ऑपरेशन में व्यक्त करती है: पहले उम्मीदवार सेट पर फ़िल्टर करें, फिर सुनिश्चित करें कि अद्वितीय मेलों की संख्या अनुरोधित नामों की संख्या के बराबर हो। इससे Python में लूपिंग नहीं करनी पड़ती, प्रति-मान उप-क्वेरी से बचा जाता है, और इनपुट बड़ा होने पर भी दक्षता बनी रहती है। इनपुट को सेट में बदलने से डुप्लिकेट के बावजूद लक्ष्य गिनती सही रहती है, और अतिरिक्त JOINs होने पर distinct गिनती शुद्धता बनाए रखती है।
मुख्य बातें
Django में ManyToMany पर AND फ़िल्टर्स के लिए एग्रीगेशन-आधारित तरीका अपनाएँ। उम्मीदवार नामों पर फ़िल्टर करें, संबंधित पंक्तियों की गिनती annotate करें, और उसे डिडुप्लिकेटेड इनपुट के आकार से मिलाएँ। यदि क्वेरी के स्वरूप में अतिरिक्त JOINs आते हैं, तो गिनती को distinct करें। यह पैटर्न लॉजिक को डिक्लेरेटिव रखता है, क्वेरी को संक्षिप्त बनाता है, और इनपुट बढ़ने पर प्रदर्शन को पूर्वानुमेय रखता है।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Valkoinen) और willeM_ Van Onsem के उत्तर पर आधारित है।