Tại sao việc lặp lại một QuerySet Django lớn lại tiêu tốn một lượng lớn bộ nhớ?


111

Bảng được đề cập chứa khoảng mười triệu hàng.

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

Điều này làm cho việc sử dụng bộ nhớ tăng đều đặn lên 4 GB hoặc lâu hơn, tại thời điểm đó các hàng in nhanh chóng. Sự chậm trễ kéo dài trước khi hàng đầu tiên được in khiến tôi ngạc nhiên - tôi mong đợi nó sẽ in gần như ngay lập tức.

Tôi cũng đã thử cách Event.objects.iterator()ứng xử tương tự.

Tôi không hiểu Django đang tải gì vào bộ nhớ hoặc tại sao nó lại làm như vậy. Tôi đã mong đợi Django lặp lại các kết quả ở cấp cơ sở dữ liệu, có nghĩa là kết quả sẽ được in với tốc độ gần như không đổi (thay vì tất cả cùng một lúc sau một thời gian dài chờ đợi).

Tôi đã hiểu lầm điều gì?

(Tôi không biết liệu nó có liên quan hay không, nhưng tôi đang sử dụng PostgreSQL.)


6
Trên máy nhỏ này thậm chí có thể gây ra ngay lập tức "chết" vào vỏ django hoặc máy chủ
Stefano

Câu trả lời:


113

Nate C đã đóng, nhưng không hoàn toàn.

Từ các tài liệu :

Bạn có thể đánh giá QuerySet theo những cách sau:

  • Sự lặp lại. Một QuerySet có thể lặp lại và nó thực hiện truy vấn cơ sở dữ liệu của nó vào lần đầu tiên bạn lặp lại nó. Ví dụ: điều này sẽ in dòng tiêu đề của tất cả các mục nhập trong cơ sở dữ liệu:

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

Vì vậy, mười triệu hàng của bạn được truy xuất, tất cả cùng một lúc, khi bạn lần đầu tiên vào vòng lặp đó và nhận dạng lặp lại của bộ truy vấn. Sự chờ đợi mà bạn trải nghiệm là Django tải các hàng cơ sở dữ liệu và tạo các đối tượng cho từng hàng, trước khi trả về một thứ mà bạn thực sự có thể lặp lại. Sau đó, bạn có mọi thứ trong bộ nhớ, và kết quả tràn ra.

Từ việc đọc các tài liệu của tôi, iterator()không gì khác hơn là bỏ qua các cơ chế bộ nhớ đệm nội bộ của QuerySet. Tôi nghĩ rằng nó có thể hợp lý khi thực hiện từng việc một, nhưng ngược lại điều đó sẽ yêu cầu mười triệu lượt truy cập riêng lẻ trên cơ sở dữ liệu của bạn. Có lẽ không phải tất cả những gì mong muốn.

Lặp lại các tập dữ liệu lớn một cách hiệu quả là điều mà chúng tôi vẫn chưa hiểu đúng, nhưng có một số đoạn mã ngoài đó bạn có thể thấy hữu ích cho mục đích của mình:


1
Cảm ơn câu trả lời tuyệt vời, @eternicode. Cuối cùng, chúng tôi đã giảm xuống SQL thô để lặp lại mức cơ sở dữ liệu mong muốn.
davidchambers

2
@eternicode Câu trả lời rất hay, chỉ cần nhấn vào vấn đề này. Có bất kỳ cập nhật liên quan nào trong Django kể từ đó không?
Zólyomi István

2
Các tài liệu kể từ Django 1.11 nói rằng iterator () không sử dụng con trỏ phía máy chủ.
Jeff C Johnson

42

Có thể không phải là nhanh hơn hoặc hiệu quả nhất, nhưng là một giải pháp có sẵn, tại sao không sử dụng các đối tượng Trang và Paginator của django core được ghi lại ở đây:

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

Một cái gì đó như thế này:

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
Cải tiến nhỏ hiện có thể kể từ khi đăng. Paginatorbây giờ có một page_rangetài sản để tránh boilerplate. Nếu để tìm kiếm chi phí bộ nhớ tối thiểu, bạn có thể sử dụng cách object_list.iterator()này sẽ không điền vào bộ nhớ cache của bộ truy vấn . prefetch_related_objectssau đó được yêu cầu để tìm nạp trước
Ken Colton

28

Hành vi mặc định của Django là lưu vào bộ nhớ cache toàn bộ kết quả của QuerySet khi nó đánh giá truy vấn. Bạn có thể sử dụng phương thức trình lặp của QuerySet để tránh điều này vào bộ nhớ đệm:

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

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

Phương thức iterator () đánh giá bộ truy vấn và sau đó đọc kết quả trực tiếp mà không cần thực hiện bộ nhớ đệm ở cấp QuerySet. Phương pháp này mang lại hiệu suất tốt hơn và giảm đáng kể bộ nhớ khi lặp qua một số lượng lớn các đối tượng mà bạn chỉ cần truy cập một lần. Lưu ý rằng bộ nhớ đệm vẫn được thực hiện ở cấp cơ sở dữ liệu.

Sử dụng iterator () làm giảm mức sử dụng bộ nhớ đối với tôi, nhưng nó vẫn cao hơn tôi mong đợi. Sử dụng phương pháp phân trang do mpaf đề xuất sử dụng ít bộ nhớ hơn nhiều, nhưng chậm hơn 2-3 lần đối với trường hợp thử nghiệm của tôi.

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

Đây là từ tài liệu: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Không có hoạt động cơ sở dữ liệu nào thực sự xảy ra cho đến khi bạn làm gì đó để đánh giá bộ truy vấn.

Vì vậy, khi print eventchạy, truy vấn sẽ kích hoạt (quét toàn bộ bảng theo lệnh của bạn.) Và tải kết quả. Yêu cầu của bạn cho tất cả các đối tượng và không có cách nào để có được đối tượng đầu tiên mà không nhận được tất cả chúng.

Nhưng nếu bạn làm điều gì đó như:

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

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

Sau đó, nó sẽ thêm hiệu số và giới hạn cho sql nội bộ.


7

Đối với lượng lớn bản ghi, con trỏ cơ sở dữ liệu hoạt động tốt hơn. Bạn cần SQL thô trong Django, con trỏ Django là một cái gì đó khác với con trỏ SQL.

Phương pháp LIMIT - OFFSET do Nate C đề xuất có thể đủ tốt cho tình huống của bạn. Đối với một lượng lớn dữ liệu, nó chậm hơn con trỏ vì nó phải chạy đi chạy lại cùng một truy vấn và phải chuyển qua nhiều kết quả hơn.


4
Frank, đó là chắc chắn là một điểm tốt nhưng sẽ được tốt đẹp để xem một số chi tiết mã để di chuyển hướng tới một giải pháp ;-) (cũng câu hỏi này là khá cũ bây giờ ...)
Stefano

7

Django không có giải pháp tốt để tìm nạp các mục lớn từ cơ sở dữ liệu.

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

Danh sách giá trị có thể được sử dụng để tìm nạp tất cả các id trong cơ sở dữ liệu và sau đó tìm nạp từng đối tượng riêng biệt. Qua một thời gian, các đối tượng lớn sẽ được tạo trong bộ nhớ và sẽ không bị thu thập rác cho đến khi thoát khỏi vòng lặp for. Đoạn mã trên thực hiện việc thu gom rác thủ công sau mỗi lần tiêu thụ vật phẩm thứ 100.


StreamingHttpResponse có thể là một giải pháp không? stackoverflow.com/questions/15359768/…
ratata

2
Tuy nhiên, điều này sẽ dẫn đến các lần truy cập bằng nhau trong cơ sở dữ liệu như số lượng vòng lặp, tôi rất tiếc.
raratiru

5

Bởi vì theo cách đó các đối tượng cho toàn bộ bộ truy vấn được tải vào bộ nhớ cùng một lúc. Bạn cần chia bộ truy vấn của mình thành các bit nhỏ hơn có thể tiêu hóa được. Hình thức để làm điều này được gọi là cho ăn bằng thìa. Đây là một triển khai ngắn gọn.

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

Để sử dụng điều này, bạn viết một hàm thực hiện các hoạt động trên đối tượng của bạn:

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

và chạy chức năng đó trên bộ truy vấn của bạn:

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

Điều này có thể được cải thiện hơn nữa với đa xử lý để thực thi functrên nhiều đối tượng song song.


1
Có vẻ như điều này sẽ được tích hợp thành 1.12 với lần lặp (chunk_size = 1000)
Kevin Parker

3

Đây là một giải pháp bao gồm len và đế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)

Sử dụng:

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

0

Tôi thường sử dụng truy vấn thô MySQL thô thay vì Django ORM cho loại tác vụ này.

MySQL hỗ trợ chế độ phát trực tuyến để chúng ta có thể lặp lại tất cả các bản ghi một cách an toàn và nhanh chóng mà không bị lỗi bộ nhớ.

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

Tham khảo:

  1. Truy xuất hàng triệu hàng từ MySQL
  2. Việc truyền trực tuyến tập kết quả MySQL hoạt động như thế nào so với việc tìm nạp toàn bộ JDBC ResultSet cùng một lúc

Bạn vẫn có thể sử dụng Django ORM để tạo truy vấn. Chỉ cần sử dụng kết quả queryset.querycho việc thực thi của bạn.
Pol
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.