Neden büyük bir Django QuerySet aracılığıyla yineleme yapmak büyük miktarda bellek tüketiyor?


111

Söz konusu tablo kabaca on milyon satır içermektedir.

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

Bu, bellek kullanımının istikrarlı bir şekilde 4 GB'ye çıkmasına neden olur ve bu noktada satırlar hızla yazdırılır. İlk satırın yazdırılmasından önceki uzun gecikme beni şaşırttı - neredeyse anında yazdırmasını bekliyordum.

Event.objects.iterator()Hangisinin de aynı şekilde davrandığını denedim .

Django'nun belleğe ne yüklediğini veya bunu neden yaptığını anlamıyorum. Django'nun veritabanı düzeyinde sonuçları yinelemesini bekliyordum, bu da sonuçların kabaca sabit bir oranda yazdırılacağı anlamına geliyordu (uzun bir beklemeden sonra hepsi birden yerine).

Neyi yanlış anladım?

(Alakalı olup olmadığını bilmiyorum ama PostgreSQL kullanıyorum.)


6
Daha küçük makinelerde bu, django kabuğunun veya sunucunun hemen "Öldürülmesine" neden olabilir
Stefano

Yanıtlar:


113

Nate C yakındı, ama tam olarak değil.

Gönderen docs :

Bir QuerySet'i aşağıdaki şekillerde değerlendirebilirsiniz:

  • Yineleme. Bir QuerySet yinelenebilir ve üzerinde yinelediğiniz ilk seferde veritabanı sorgusunu yürütür. Örneğin, bu, veritabanındaki tüm girişlerin başlığını yazdıracaktır:

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

Böylece, bu döngüye ilk girdiğinizde ve sorgu kümesinin yinelenen biçimini aldığınızda, on milyon satırınız bir kerede alınır. Tecrübe ettiğiniz bekleme, Django'nun veritabanı satırlarını yüklemesi ve her biri için nesneler yaratmasıdır, aslında üzerinde yineleyebileceğiniz bir şeyi döndürmeden önce. Sonra her şeyi hafızanızda tutarsınız ve sonuçlar dökülür.

Belgeleri okumamdan, iterator()QuerySet'in dahili önbelleğe alma mekanizmalarını atlamaktan başka bir şey yapmıyor. Tek tek bir şey yapmanın mantıklı olabileceğini düşünüyorum, ancak bunun tersine veritabanınızda on milyon tekil isabet gerektirecektir. Belki o kadar arzu edilen bir şey değil.

Büyük veri kümeleri üzerinde verimli bir şekilde yineleme yapmak, hala tam olarak doğru yapamadığımız bir şeydir, ancak orada, amaçlarınız için yararlı bulabileceğiniz bazı parçacıklar vardır:


1
Harika yanıt için teşekkürler, @eternicode. Sonunda, istenen veritabanı düzeyinde yineleme için ham SQL'e düştük.
davidchambers

2
@eternicode Güzel cevap, sadece bu sorunu çöz. O zamandan beri Django'da ilgili bir güncelleme var mı?
Zólyomi István

2
Django 1.11'den beri dokümanlar, iterator () 'un sunucu tarafı imleçleri kullandığını söylüyor.
Jeff C Johnson

42

Daha hızlı veya en verimli olmayabilir, ancak hazır bir çözüm olarak neden django core'un burada belgelenen Paginator ve Page nesnelerini kullanmıyorsunuz:

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

Bunun gibi bir şey:

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
Gönderiden bu yana küçük iyileştirmeler artık mümkün. Paginatorartık standart page_rangemetinden kaçınmak için bir özelliğe sahiptir . Minimum bellek ek yükü arayışındaysanız object_list.iterator(), sorgu kümesi önbelleğini doldurmayacak olanı kullanabilirsiniz . prefetch_related_objectsSonra prefetch için gereklidir
Ken Colton

28

Django'nun varsayılan davranışı, sorguyu değerlendirirken QuerySet'in tüm sonucunu önbelleğe almaktır. Bu önbelleğe almayı önlemek için QuerySet'in yineleyici yöntemini kullanabilirsiniz:

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

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

İterator () yöntemi, sorgu kümesini değerlendirir ve ardından sonuçları QuerySet düzeyinde önbelleğe alma yapmadan doğrudan okur. Bu yöntem, yalnızca bir kez erişmeniz gereken çok sayıda nesne üzerinde yineleme yaparken daha iyi performans ve bellekte önemli bir azalma sağlar. Önbelleğe alma işleminin hala veritabanı düzeyinde yapıldığını unutmayın.

İterator () kullanmak benim için bellek kullanımını azaltıyor, ancak yine de beklediğimden daha yüksek. Mpaf tarafından önerilen sayfalayıcı yaklaşımını kullanmak çok daha az bellek kullanır, ancak test durumum için 2-3 kat daha yavaştır.

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

Bu, dokümanlardan alınmıştır: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Siz sorgu kümesini değerlendirmek için bir şey yapana kadar hiçbir veritabanı etkinliği gerçekleşmez.

Bu nedenle print eventçalıştırıldığında sorgu tetiklenir (komutunuza göre tam bir tablo taramasıdır) ve sonuçları yükler. Tüm nesneleri istiyorsun ve hepsini almadan ilk nesneyi almanın bir yolu yok.

Ama şöyle bir şey yaparsanız:

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

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

Daha sonra dahili olarak sql'ye ofsetler ve limitler ekleyecektir.


7

Büyük miktarda kayıt için, bir veritabanı imleci daha da iyi performans gösterir. Django'da ham SQL'e ihtiyacınız var, Django imleci SQL imlecinden farklı bir şey.

Nate C tarafından önerilen LIMIT - OFFSET yöntemi durumunuz için yeterince iyi olabilir. Büyük miktarda veri için imleçten daha yavaştır, çünkü aynı sorguyu tekrar tekrar çalıştırması ve daha fazla sonucun üzerinden atlaması gerekir.


4
Frank, bu kesinlikle iyi bir nokta, ancak bir çözüme doğru ilerlemek için bazı kod ayrıntılarını görmek güzel olurdu ;-) (pekala, bu soru artık oldukça eski ...)
Stefano

7

Django, veritabanından büyük öğeleri almak için iyi bir çözüme sahip değil.

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 , veritabanlarındaki tüm kimlikleri almak ve ardından her nesneyi ayrı ayrı almak için kullanılabilir. Bir süre sonra bellekte büyük nesneler oluşturulacak ve döngüden çıkılıncaya kadar çöp toplanmayacaktır. Yukarıdaki kod, her 100. madde tüketildikten sonra manuel çöp toplama yapar.


StreamingHttpResponse bir çözüm olabilir mi? stackoverflow.com/questions/15359768/…
ratata

2
Ancak bu, veritabanında döngü sayısı kadar eşit isabetlere neden olacak, korkarım.
raratiru

5

Çünkü bu şekilde bütün bir sorgu kümesi için nesneler bir kerede belleğe yüklenir. Sorgu kümenizi daha küçük sindirilebilir parçalara ayırmanız gerekir. Bunu yapmak için kullanılan modele kaşıkla besleme denir. İşte kısa bir uygulama.

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

Bunu kullanmak için nesneniz üzerinde işlem yapan bir işlev yazarsınız:

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

ve daha sonra bu işlevi sorgu kümenizde çalıştırın:

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

Bu, funcbirden çok nesne üzerinde paralel olarak yürütmek için çoklu işlem ile daha da geliştirilebilir .


1
Görünüşe göre bu, iterate ile 1.12'ye eklenecek (chunk_size = 1000)
Kevin Parker

3

İşte len ve count içeren bir çözüm:

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)

Kullanımı:

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

0

Bu tür bir görev için genellikle Django ORM yerine ham MySQL ham sorgusu kullanırım.

MySQL, akış modunu destekler, böylece tüm kayıtlarda bellek hatası olmadan güvenli ve hızlı bir şekilde döngü yapabiliriz.

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()

Ref:

  1. MySQL'den milyonlarca satır alma
  2. MySQL sonuç kümesi akışı, tüm JDBC ResultSet'i aynı anda getirmeye kıyasla nasıl performans gösteriyor?

Sorgu oluşturmak için yine de Django ORM kullanabilirsiniz. Sadece queryset.queryyürütmenizde sonuç kullanın .
Pol
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.