Mengapa mengulang melalui Django QuerySet yang besar memakan banyak memori?

111

Tabel tersebut berisi kira-kira sepuluh juta baris.

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

Hal ini menyebabkan penggunaan memori terus meningkat hingga 4 GB atau lebih, pada saat itu baris dicetak dengan cepat. Penundaan yang lama sebelum baris pertama dicetak mengejutkan saya - saya berharap itu dicetak hampir seketika.

Saya juga mencoba Event.objects.iterator()yang berperilaku sama.

Saya tidak mengerti apa yang sedang dimuat Django ke dalam memori atau mengapa ia melakukan ini. Saya mengharapkan Django mengulang melalui hasil pada tingkat basis data, yang berarti hasil akan dicetak secara kasar pada tingkat yang konstan (daripada sekaligus setelah menunggu lama).

Apa yang telah saya salah paham?

(Saya tidak tahu apakah itu relevan, tetapi saya menggunakan PostgreSQL.)

davidchambers.dll
sumber
6
Pada mesin yang lebih kecil ini bahkan dapat menyebabkan langsung "Dibunuh" ke shell atau server django
Stefano

Jawaban:

113

Nate C memang dekat, tapi tidak sepenuhnya.

Dari dokumen :

Anda dapat mengevaluasi QuerySet dengan cara berikut:

  • Pengulangan. QuerySet bersifat iterable, dan menjalankan kueri database-nya saat pertama kali Anda mengulanginya. Misalnya, ini akan mencetak informasi utama dari semua entri dalam database:

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

Jadi sepuluh juta baris Anda diambil, semuanya sekaligus, saat Anda pertama kali memasuki loop itu dan mendapatkan bentuk iterasi dari queryset. Penantian yang Anda alami adalah Django memuat baris basis data dan membuat objek untuk masing-masing, sebelum mengembalikan sesuatu yang sebenarnya Anda dapat mengulanginya. Kemudian Anda memiliki segalanya dalam ingatan, dan hasilnya akan keluar.

Dari saya membaca dokumen, iterator()tidak lebih dari melewati mekanisme cache internal QuerySet. Saya pikir mungkin masuk akal untuk melakukan satu per satu hal, tetapi sebaliknya itu akan membutuhkan sepuluh juta klik individu di database Anda. Mungkin tidak semua yang diinginkan.

Mengulangi kumpulan data besar secara efisien adalah sesuatu yang masih belum kami lakukan dengan benar, tetapi ada beberapa cuplikan di luar sana yang mungkin berguna untuk tujuan Anda:

eternicode
sumber
1
Terima kasih atas jawaban yang bagus, @eternicode. Pada akhirnya kami beralih ke SQL mentah untuk iterasi tingkat database yang diinginkan.
davidchambers
2
@eternicode Jawaban yang bagus, langsung saja ke masalah ini. Apakah ada pemutakhiran terkait di Django sejak itu?
Zólyomi István
2
Dokumen sejak Django 1.11 mengatakan iterator () memang menggunakan kursor sisi server.
Jeff C Johnson
42

Mungkin bukan yang lebih cepat atau paling efisien, tetapi sebagai solusi siap pakai mengapa tidak menggunakan Paginator dan objek Halaman dari django core yang didokumentasikan di sini:

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

Sesuatu seperti ini:

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
mpaf
sumber
3
Perbaikan kecil sekarang mungkin dilakukan sejak posting. Paginatorsekarang memiliki page_rangeproperti untuk menghindari boilerplate. Jika mencari overhead memori minimal, Anda dapat menggunakan object_list.iterator()yang tidak akan mengisi cache queryset . prefetch_related_objectskemudian diperlukan untuk pengambilan sebelumnya
Ken Colton
28

Perilaku standar Django adalah menyimpan seluruh hasil QuerySet saat mengevaluasi query. Anda bisa menggunakan metode iterator QuerySet untuk menghindari caching ini:

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

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

Metode iterator () mengevaluasi queryset dan kemudian membaca hasilnya secara langsung tanpa melakukan caching pada level QuerySet. Metode ini menghasilkan kinerja yang lebih baik dan pengurangan memori yang signifikan saat melakukan iterasi pada sejumlah besar objek yang hanya perlu Anda akses sekali. Perhatikan bahwa caching masih dilakukan di tingkat database.

Menggunakan iterator () mengurangi penggunaan memori untuk saya, tetapi masih lebih tinggi dari yang saya harapkan. Menggunakan pendekatan paginator yang disarankan oleh mpaf menggunakan lebih sedikit memori, tetapi 2-3x lebih lambat untuk kasus pengujian saya.

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
Luke Moore
sumber
8

Ini dari dokumen: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Tidak ada aktivitas database yang benar-benar terjadi hingga Anda melakukan sesuatu untuk mengevaluasi queryset.

Jadi ketika print eventdijalankan, kueri akan aktif (yang merupakan pemindaian tabel lengkap sesuai dengan perintah Anda.) Dan memuat hasilnya. Permintaan Anda untuk semua objek dan tidak ada cara untuk mendapatkan objek pertama tanpa mendapatkan semuanya.

Tetapi jika Anda melakukan sesuatu seperti:

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

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

Kemudian itu akan menambahkan offset dan batas ke sql secara internal.

nate c
sumber
7

Untuk rekaman dalam jumlah besar, kursor database bekerja lebih baik. Anda memang membutuhkan SQL mentah di Django, kursor-Django adalah sesuatu yang berbeda dari kursor SQL.

Metode LIMIT - OFFSET yang disarankan oleh Nate C mungkin cukup baik untuk situasi Anda. Untuk data dalam jumlah besar, ini lebih lambat daripada kursor karena harus menjalankan kueri yang sama berulang kali dan harus melompati lebih banyak hasil.

Frank Heikens
sumber
4
Frank, itu jelas poin yang bagus tetapi alangkah baiknya melihat beberapa detail kode untuk mendorong menuju solusi ;-) (yah pertanyaan ini sudah cukup lama sekarang ...)
Stefano
7

Django tidak mempunyai solusi yang baik untuk mengambil item besar dari database.

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 dapat digunakan untuk mengambil semua id dalam database dan kemudian mengambil setiap objek secara terpisah. Seiring waktu, objek besar akan dibuat dalam memori dan tidak akan dikumpulkan sampah sampai loop keluar. Kode di atas melakukan pengumpulan sampah secara manual setelah setiap item ke-100 dikonsumsi.

Kracekumar
sumber
Dapatkah streamingHttpResponse menjadi solusi? stackoverflow.com/questions/15359768/…
ratata
2
Namun, ini akan menghasilkan hit yang sama dalam database sebagai jumlah loop, saya khawatir.
raratiru
5

Karena dengan cara itu objek untuk seluruh queryset dimuat di memori sekaligus. Anda perlu membagi queryset Anda menjadi bit yang lebih kecil dan mudah dicerna. Pola untuk melakukan ini disebut pemberian makan sendok. Berikut implementasi singkatnya.

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

Untuk menggunakan ini, Anda menulis fungsi yang melakukan operasi pada objek Anda:

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

dan kemudian menjalankan fungsi itu di queryset Anda:

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

Hal ini dapat ditingkatkan lebih lanjut dengan multiprocessing untuk mengeksekusi funcbeberapa objek secara paralel.

fmalina.dll
sumber
1
Sepertinya ini akan dibangun menjadi 1,12 dengan iterasi (chunk_size = 1000)
Kevin Parker
3

Berikut solusi termasuk len dan hitung:

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)

Pemakaian:

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

Saya biasanya menggunakan kueri mentah MySQL mentah daripada Django ORM untuk tugas semacam ini.

MySQL mendukung mode streaming sehingga kami dapat melakukan loop melalui semua catatan dengan aman dan cepat tanpa kesalahan kehabisan memori.

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. Mengambil jutaan baris dari MySQL
  2. Bagaimana hasil MySQL mengatur kinerja streaming vs mengambil seluruh JDBC ResultSet sekaligus
Tho
sumber
Anda masih dapat menggunakan Django ORM untuk menghasilkan kueri. Cukup gunakan hasil queryset.queryuntuk eksekusi Anda.
Pol