เหตุใดการวนซ้ำผ่าน Django QuerySet ขนาดใหญ่จึงใช้หน่วยความจำจำนวนมาก


111

ตารางที่เป็นปัญหามีประมาณสิบล้านแถว

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

สิ่งนี้ทำให้การใช้หน่วยความจำเพิ่มขึ้นเรื่อย ๆ เป็น 4 GB หรือมากกว่านั้นซึ่งทำให้แถวต่างๆพิมพ์อย่างรวดเร็ว ความล่าช้าที่ยาวนานก่อนที่แถวแรกจะพิมพ์ทำให้ฉันประหลาดใจ - ฉันคาดว่ามันจะพิมพ์ได้เกือบจะในทันที

ฉันยังพยายามEvent.objects.iterator()ที่มีพฤติกรรมแบบเดียวกัน

ฉันไม่เข้าใจว่า Django กำลังโหลดอะไรลงในหน่วยความจำหรือเหตุใดจึงทำเช่นนี้ ฉันคาดว่า Django จะวนซ้ำผลลัพธ์ในระดับฐานข้อมูลซึ่งหมายความว่าผลลัพธ์จะถูกพิมพ์ในอัตราคงที่โดยประมาณ (แทนที่จะเป็นทั้งหมดในครั้งเดียวหลังจากรอเป็นเวลานาน)

ฉันเข้าใจผิดอะไร

(ฉันไม่รู้ว่ามันเกี่ยวข้องหรือเปล่า แต่ฉันใช้ PostgreSQL)


6
ในเครื่องขนาดเล็กอาจทำให้เกิด "Killed" ไปยัง django shell หรือเซิร์ฟเวอร์ได้ทันที
Stefano

คำตอบ:


113

Nate C ใกล้ แต่ไม่มาก

จากเอกสาร :

คุณสามารถประเมิน QuerySet ได้ด้วยวิธีต่อไปนี้:

  • การทำซ้ำ QuerySet สามารถทำซ้ำได้และดำเนินการสืบค้นฐานข้อมูลในครั้งแรกที่คุณทำซ้ำ ตัวอย่างเช่นจะพิมพ์บรรทัดแรกของรายการทั้งหมดในฐานข้อมูล:

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

ดังนั้นสิบล้านแถวของคุณจะถูกเรียกทั้งหมดพร้อมกันเมื่อคุณเข้าสู่ลูปนั้นเป็นครั้งแรกและรับรูปแบบการวนซ้ำของชุดแบบสอบถาม สิ่งที่คุณต้องรอคือ Django กำลังโหลดแถวฐานข้อมูลและสร้างออบเจ็กต์สำหรับแต่ละอันก่อนที่จะส่งคืนสิ่งที่คุณสามารถทำซ้ำได้ จากนั้นคุณมีทุกอย่างในความทรงจำและผลลัพธ์ก็ล้นออกมา

จากการอ่านเอกสารของฉันiterator()ไม่มีอะไรมากไปกว่าการข้ามกลไกการแคชภายในของ QuerySet ฉันคิดว่ามันอาจจะสมเหตุสมผลที่จะทำทีละรายการ แต่ในทางกลับกันต้องมีการเข้าชม 10 ล้านครั้งในฐานข้อมูลของคุณ อาจจะไม่ได้เป็นที่ต้องการทั้งหมด

การทำซ้ำชุดข้อมูลขนาดใหญ่อย่างมีประสิทธิภาพเป็นสิ่งที่เรายังทำได้ไม่ถูกต้องนัก แต่มีตัวอย่างบางส่วนที่คุณอาจพบว่ามีประโยชน์สำหรับวัตถุประสงค์ของคุณ:


1
ขอบคุณสำหรับคำตอบที่ดี @eternicode ในที่สุดเราก็ทิ้ง SQL ดิบสำหรับการทำซ้ำระดับฐานข้อมูลที่ต้องการ
davidchambers

2
@eternicode คำตอบที่ดีเพียงแค่ตีปัญหานี้ มีการอัปเดตที่เกี่ยวข้องใน Django ตั้งแต่นั้นมาหรือไม่?
ZólyomiIstván

2
เอกสารตั้งแต่ Django 1.11 กล่าวว่า iterator () ใช้เคอร์เซอร์ฝั่งเซิร์ฟเวอร์
Jeff C Johnson

42

อาจไม่ใช่วิธีที่เร็วกว่าหรือมีประสิทธิภาพมากที่สุด แต่เป็นโซลูชันสำเร็จรูปทำไมไม่ใช้ Paginator และวัตถุหน้าของ 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() queryset prefetch_related_objectsจำเป็นสำหรับการดึงข้อมูลล่วงหน้า
Ken Colton

28

ลักษณะการทำงานเริ่มต้นของ Django คือการแคชผลลัพธ์ทั้งหมดของ QuerySet เมื่อประเมินแบบสอบถาม คุณสามารถใช้เมธอดตัววนซ้ำของ QuerySet เพื่อหลีกเลี่ยงการแคชนี้:

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

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

iterator () วิธีการประเมินชุดแบบสอบถามจากนั้นอ่านผลลัพธ์โดยตรงโดยไม่ต้องทำการแคชที่ระดับ QuerySet วิธีนี้ส่งผลให้ประสิทธิภาพดีขึ้นและหน่วยความจำลดลงอย่างมากเมื่อทำซ้ำบนวัตถุจำนวนมากที่คุณต้องการเข้าถึงเพียงครั้งเดียว โปรดทราบว่าการแคชยังคงดำเนินการในระดับฐานข้อมูล

การใช้ iterator () ช่วยลดการใช้หน่วยความจำสำหรับฉัน แต่ก็ยังสูงกว่าที่ฉันคาดไว้ การใช้วิธีการแบ่งส่วนที่แนะนำโดย mpaf ใช้หน่วยความจำน้อยกว่ามาก แต่ช้ากว่า 2-3 เท่าสำหรับกรณีทดสอบของฉัน

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/querysets/

ไม่มีกิจกรรมฐานข้อมูลเกิดขึ้นจริงจนกว่าคุณจะทำบางอย่างเพื่อประเมินชุดข้อมูล

ดังนั้นเมื่อprint eventเรียกใช้แบบสอบถามจะเริ่มทำงาน (ซึ่งเป็นการสแกนแบบเต็มตารางตามคำสั่งของคุณ) และโหลดผลลัพธ์ คุณขอวัตถุทั้งหมดและไม่มีทางที่จะได้รับวัตถุชิ้นแรกโดยไม่ได้รับวัตถุทั้งหมด

แต่ถ้าคุณทำสิ่งที่ชอบ:

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

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

จากนั้นจะเพิ่มออฟเซ็ตและขีด จำกัด ให้กับ sql ภายใน


7

สำหรับบันทึกจำนวนมากเคอร์เซอร์ฐานข้อมูลจะทำงานได้ดียิ่งขึ้น คุณต้องการ SQL ดิบใน Django เคอร์เซอร์ Django เป็นสิ่งที่แตกต่างจากเคอร์เซอร์ SQL

วิธี LIMIT - OFFSET ที่ Nate C แนะนำอาจดีพอสำหรับสถานการณ์ของคุณ สำหรับข้อมูลจำนวนมากจะช้ากว่าเคอร์เซอร์เนื่องจากต้องเรียกใช้แบบสอบถามเดียวกันซ้ำแล้วซ้ำอีกและต้องข้ามผลลัพธ์มากขึ้นเรื่อย ๆ


4
แฟรงค์นั่นเป็นจุดที่ดีอย่างแน่นอน แต่จะเป็นการดีที่จะเห็นรายละเอียดโค้ดบางอย่างเพื่อเขยิบไปสู่การแก้ปัญหา ;-) (ตอนนี้คำถามนี้ค่อนข้างเก่าแล้ว ... )
Stefano

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 รายการ


StreamingHttpResponse เป็นทางออกได้หรือไม่? stackoverflow.com/questions/15359768/…
ratata

2
อย่างไรก็ตามสิ่งนี้จะส่งผลให้มีการเข้าชมในฐานข้อมูลเท่ากันตามจำนวนลูปฉันรู้สึกไม่สบายใจ
raratiru

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 ด้วยซ้ำ (chunk_size = 1000)
Kevin Parker

3

นี่คือวิธีแก้ปัญหารวมถึง len และ count:

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

ฉันมักจะใช้แบบสอบถามดิบ MySQL แบบดิบแทน Django ORM สำหรับงานประเภทนี้

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 ResultSet ทั้งหมดในครั้งเดียว

คุณยังคงสามารถใช้ Django ORM เพื่อสร้างแบบสอบถามได้ เพียงใช้ผลลัพธ์queryset.queryในการดำเนินการของคุณ
พล
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.