क्यों एक बड़ी Django क्वेरी के माध्यम से पुनरावृत्ति हो रही है जो भारी मात्रा में स्मृति का उपभोग करती है?


111

विचाराधीन तालिका में लगभग दस मिलियन पंक्तियाँ हैं।

for event in Event.objects.all():
    print event

इसके कारण मेमोरी उपयोग लगातार 4 जीबी तक बढ़ जाता है, जिस पर पंक्तियां तेजी से प्रिंट होती हैं। पहली पंक्ति छपने से पहले लम्बी देरी ने मुझे चौंका दिया - मुझे उम्मीद थी कि यह लगभग तुरंत छप जाएगी।

मैंने भी कोशिश की Event.objects.iterator()जो उसी तरह से व्यवहार करता है।

मुझे समझ नहीं आ रहा है कि Django मेमोरी में क्या लोड कर रहा है या यह क्यों कर रहा है। मुझे उम्मीद थी कि डेटाबेस स्तर पर परिणामों के माध्यम से Django को पुनरावृत्त करना होगा, जिसका अर्थ होगा कि परिणाम लगभग एक स्थिर दर पर मुद्रित किया जाएगा (बजाय एक लंबे इंतजार के बाद एक बार में)।

मुझे क्या गलत समझा है?

(मुझे नहीं पता कि यह प्रासंगिक है, लेकिन मैं PostgreSQL का उपयोग कर रहा हूं।)


6
छोटी मशीनों पर यह सीधे django शेल या सर्वर पर "Killed" का कारण बन सकता है
स्टेफानो

जवाबों:


113

नैट सी करीब था, लेकिन काफी नहीं।

से डॉक्स :

आप निम्न तरीकों से एक क्वेरीस का मूल्यांकन कर सकते हैं:

  • पुनरावृत्ति। एक QuerySet चलने योग्य है, और यह अपने डेटाबेस क्वेरी को पहली बार निष्पादित करता है जब आप उस पर पुनरावृति करते हैं। उदाहरण के लिए, यह डेटाबेस में सभी प्रविष्टियों के शीर्षक को प्रिंट करेगा:

    for e in Entry.objects.all():
        print e.headline

तो आपकी दस मिलियन पंक्तियाँ एक ही बार में, जब आप पहली बार उस लूप में प्रवेश करते हैं और क्वेरीसेट का पुनरावृत् त स्वरूप प्राप्त करते हैं। आप जिस प्रतीक्षा का अनुभव करते हैं वह है Django डेटाबेस पंक्तियों को लोड करना और प्रत्येक के लिए ऑब्जेक्ट बनाना, कुछ वापस करने से पहले आप वास्तव में इसे पुन: व्यवस्थित कर सकते हैं। तब आपके पास स्मृति में सब कुछ होता है, और परिणाम सामने आते हैं।

डॉक्स के मेरे पढ़ने से, iterator()QuerySet के आंतरिक कैशिंग तंत्र को बायपास करने से ज्यादा कुछ नहीं है। मुझे लगता है कि यह एक-एक करने के लिए इसके लिए समझ में आ सकता है, लेकिन इसके लिए आपके डेटाबेस में दस लाख व्यक्तिगत हिट की आवश्यकता होगी। शायद यह सब वांछनीय नहीं है।

बड़े डेटासेट पर कुशलतापूर्वक कुछ ऐसा करना जो अभी भी हमें सही नहीं लगा है, लेकिन वहाँ कुछ स्निपेट हैं जो आपको अपने उद्देश्यों के लिए उपयोगी लग सकते हैं:


1
महान उत्तर के लिए धन्यवाद, @eternicode। अंत में हम वांछित डेटाबेस-स्तरीय पुनरावृत्ति के लिए कच्चे एसक्यूएल के लिए नीचे आ गए।
davidchambers

2
@eternicode अच्छा जवाब, बस इस मुद्दे को मारा। क्या Django में इससे संबंधित कोई अपडेट है?
जूलोमी इस्तवान

2
Django 1.11 के बाद से डॉक्स का कहना है कि इटरेटर () सर्वर साइड कर्सर का उपयोग करता है।
जेफ सी जॉनसन

42

हो सकता है कि यह तेज़ या सबसे अधिक कुशल न हो, लेकिन एक तैयार समाधान के रूप में क्यों न यहां पर प्रलेखित django Core के पेजिनेटर और पेज ऑब्जेक्ट का उपयोग किया जाए:

https://docs.djangoproject.com/en/dev/topics/pagination/

कुछ इस तरह:

from django.core.paginator import Paginator
from djangoapp.models import model

paginator = Paginator(model.objects.all(), 1000) # chunks of 1000, you can 
                                                 # change this to desired chunk size

for page in range(1, paginator.num_pages + 1):
    for row in paginator.page(page).object_list:
        # here you can do whatever you want with the row
    print "done processing page %s" % page

3
पोस्ट के बाद अब छोटे सुधार संभव हैं। Paginatorअब page_rangeबॉयलरप्लेट से बचने के लिए एक संपत्ति है। यदि न्यूनतम मेमोरी ओवरहेड की खोज में, आप उपयोग कर सकते हैं object_list.iterator()जो क्वेरी कैश को आबाद नहीं करेगाprefetch_related_objectsइसके बाद प्रीफैच के लिए आवश्यक है
केन कॉल्टन

28

क्वेरी का मूल्यांकन करने पर Django का डिफ़ॉल्ट व्यवहार क्वेरीसेट के पूरे परिणाम को कैश करना है। इस कैशिंग से बचने के लिए आप QuerySet की पुनरावृत्ति विधि का उपयोग कर सकते हैं:

for event in Event.objects.all().iterator():
    print event

https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator

पुनरावृत्त () विधि क्वेरीसेट का मूल्यांकन करती है और फिर क्वेरीसेट स्तर पर कैशिंग किए बिना सीधे परिणाम पढ़ती है। इस पद्धति के परिणामस्वरूप बेहतर प्रदर्शन होता है और स्मृति में एक महत्वपूर्ण कमी आती है जब बड़ी संख्या में वस्तुओं की पुनरावृति होती है जिसे आपको केवल एक बार एक्सेस करने की आवश्यकता होती है। ध्यान दें कि अभी भी डेटाबेस स्तर पर कैशिंग किया जाता है।

इटरेटर () का उपयोग करना मेरे लिए मेमोरी उपयोग को कम करता है, लेकिन यह अभी भी मेरी अपेक्षा से अधिक है। एमपीएफ़ द्वारा सुझाए गए पेजिनेटर दृष्टिकोण का उपयोग करना बहुत कम मेमोरी का उपयोग करता है, लेकिन मेरे परीक्षण मामले के लिए 2-3x धीमा है।

from django.core.paginator import Paginator

def chunked_iterator(queryset, chunk_size=10000):
    paginator = Paginator(queryset, chunk_size)
    for page in range(1, paginator.num_pages + 1):
        for obj in paginator.page(page).object_list:
            yield obj

for event in chunked_iterator(Event.objects.all()):
    print event

8

यह डॉक्स से है: http://docs.djangoproject.com/en/dev/ref/models/querys.sp

डेटाबेस गतिविधि वास्तव में तब तक नहीं होती है जब तक आप क्वेरीसेट का मूल्यांकन करने के लिए कुछ नहीं करते हैं।

इसलिए जब print eventक्वेरी को चलाया जाता है (जो आपके आदेश के अनुसार एक पूर्ण तालिका स्कैन है।) और परिणामों को लोड करता है। आपका सभी वस्तुओं के लिए पूछना और उन सभी को प्राप्त किए बिना पहली वस्तु प्राप्त करने का कोई तरीका नहीं है।

लेकिन अगर आप कुछ ऐसा करते हैं:

Event.objects.all()[300:900]

http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets

फिर यह आंतरिक रूप से वर्ग और सीमा को जोड़ देगा।


7

बड़ी मात्रा में रिकॉर्ड के लिए, एक डेटाबेस कर्सर भी बेहतर प्रदर्शन करता है। आपको Django में कच्चे SQL की आवश्यकता है, Django- कर्सर SQL कर्सर से कुछ अलग है।

Nate C द्वारा सुझाया गया LIMIT - OFFSET तरीका आपकी स्थिति के लिए काफी अच्छा हो सकता है। बड़ी मात्रा में डेटा के लिए यह एक कर्सर की तुलना में धीमा है क्योंकि इसे एक ही क्वेरी को बार-बार चलाना पड़ता है और अधिक से अधिक परिणामों पर कूदना पड़ता है।


4
फ्रैंक, यह निश्चित रूप से एक अच्छा बिंदु है, लेकिन एक समाधान की ओर इशारा करने के लिए कुछ कोड विवरण देखना अच्छा होगा;; (अच्छी तरह से यह प्रश्न अब काफी पुराना है ...)
स्टेफानो

7

डेटाबेस से बड़ी वस्तुओं को लाने के लिए Django के पास अच्छा समाधान नहीं है।

import gc
# Get the events in reverse order
eids = Event.objects.order_by("-id").values_list("id", flat=True)

for index, eid in enumerate(eids):
    event = Event.object.get(id=eid)
    # do necessary work with event
    if index % 100 == 0:
       gc.collect()
       print("completed 100 items")

values_list का उपयोग डेटाबेस में सभी आईडी लाने के लिए और फिर प्रत्येक ऑब्जेक्ट को अलग से लाने के लिए किया जा सकता है। एक समय में बड़ी वस्तुओं को स्मृति में बनाया जाएगा और कचरा एकत्र नहीं किया जाएगा ताकि लूप बाहर निकल जाए। उपरोक्त कोड हर 100 वें आइटम के उपभोग के बाद मैन्युअल कचरा संग्रह करता है।


क्या StreamHttpResponse एक समाधान हो सकता है? stackoverflow.com/questions/15359768/…
चूहा

2
हालांकि, यह डेटाबेस में समान हिट के परिणामस्वरूप लूप की संख्या के रूप में होगा, मुझे डर है।
रतिरु ०

5

क्योंकि इस तरह से पूरे क्वेरीसेट के लिए ऑब्जेक्ट एक ही बार में मेमोरी में लोड हो जाते हैं। आपको अपने क्वेरी को छोटे सुपाच्य बिट्स में बदलने की आवश्यकता है। ऐसा करने के लिए पैटर्न को चम्मचिंग कहा जाता है। यहाँ एक संक्षिप्त कार्यान्वयन है।

def spoonfeed(qs, func, chunk=1000, start=0):
    ''' Chunk up a large queryset and run func on each item.

    Works with automatic primary key fields.

    chunk -- how many objects to take on at once
    start -- PK to start from

    >>> spoonfeed(Spam.objects.all(), nom_nom)
    '''
    while start < qs.order_by('pk').last().pk:
        for o in qs.filter(pk__gt=start, pk__lte=start+chunk):
            yeild func(o)
        start += chunk

इसका उपयोग करने के लिए आप एक फ़ंक्शन लिखते हैं जो आपके ऑब्जेक्ट पर संचालन करता है:

def set_population_density(town):
    town.population_density = calculate_population_density(...)
    town.save()

और आपके क्वेरी पर चलने वाले फ़ंक्शन से अधिक:

spoonfeed(Town.objects.all(), set_population_density)

इसे funcकई वस्तुओं पर समानांतर में निष्पादित करने के लिए मल्टीप्रोसेसिंग के साथ आगे सुधार किया जा सकता है ।


1
ऐसा लगता है कि 1.12 में iterate (chunk_size = 1000) के साथ बनाया जा रहा है
केविन पार्कर

3

यहाँ लेन और गिनती सहित एक समाधान:

class GeneratorWithLen(object):
    """
    Generator that includes len and count for given queryset
    """
    def __init__(self, generator, length):
        self.generator = generator
        self.length = length

    def __len__(self):
        return self.length

    def __iter__(self):
        return self.generator

    def __getitem__(self, item):
        return self.generator.__getitem__(item)

    def next(self):
        return next(self.generator)

    def count(self):
        return self.__len__()

def batch(queryset, batch_size=1024):
    """
    returns a generator that does not cache results on the QuerySet
    Aimed to use with expected HUGE/ENORMOUS data sets, no caching, no memory used more than batch_size

    :param batch_size: Size for the maximum chunk of data in memory
    :return: generator
    """
    total = queryset.count()

    def batch_qs(_qs, _batch_size=batch_size):
        """
        Returns a (start, end, total, queryset) tuple for each batch in the given
        queryset.
        """
        for start in range(0, total, _batch_size):
            end = min(start + _batch_size, total)
            yield (start, end, total, _qs[start:end])

    def generate_items():
        queryset.order_by()  # Clearing... ordering by id if PK autoincremental
        for start, end, total, qs in batch_qs(queryset):
            for item in qs:
                yield item

    return GeneratorWithLen(generate_items(), total)

उपयोग:

events = batch(Event.objects.all())
len(events) == events.count()
for event in events:
    # Do something with the Event

0

मैं आमतौर पर इस तरह के कार्य के लिए Django ORM के बजाय कच्चे MySQL कच्ची क्वेरी का उपयोग करता हूं।

MySQL स्ट्रीमिंग मोड को सपोर्ट करता है इसलिए हम मेमोरी एरर के बिना सभी रिकॉर्ड्स को सुरक्षित और तेजी से लूप कर सकते हैं।

import MySQLdb
db_config = {}  # config your db here
connection = MySQLdb.connect(
        host=db_config['HOST'], user=db_config['USER'],
        port=int(db_config['PORT']), passwd=db_config['PASSWORD'], db=db_config['NAME'])
cursor = MySQLdb.cursors.SSCursor(connection)  # SSCursor for streaming mode
cursor.execute("SELECT * FROM event")
while True:
    record = cursor.fetchone()
    if record is None:
        break
    # Do something with record here

cursor.close()
connection.close()

संदर्भ:

  1. MySQL से लाखों पंक्तियाँ प्राप्त करना
  2. MySQL परिणाम सेट स्ट्रीमिंग प्रदर्शन बनाम पूरे JDBC परिणाम को एक ही बार में लाने के लिए कैसे करता है

क्वेरी बनाने के लिए आप अभी भी Django ORM का उपयोग कर सकते हैं। बस queryset.queryअपने निष्पादन में जिसके परिणामस्वरूप का उपयोग करें।
पोल
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.