大規模なDjango QuerySetを繰り返し処理すると、大量のメモリが消費されるのはなぜですか?


111

問題のテーブルには、約1000万行が含まれています。

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

これにより、メモリ使用量が着実に4 GB程度に増加し、その時点で行が急速に印刷されます。最初の行が印刷される前の長い遅延は私を驚かせました-私はそれがほぼ瞬時に印刷されると思っていました。

私もEvent.objects.iterator()同じように振る舞いました。

Djangoがメモリに読み込んでいるものや、なぜDjangoがこれを行っているのか理解できません。私はDjangoがデータベースレベルで結果を反復処理することを期待していました。つまり、結果は(一定の待機後に一度にすべてではなく)ほぼ一定の速度で出力されます。

私は何を誤解しましたか?

(それが適切かどうかはわかりませんが、PostgreSQLを使用しています。)


6
小規模なマシンでは、これにより、djangoシェルまたはサーバーがすぐに「強制終了」される可能性があります
Stefano

回答:


113

ネイトCは近かったが、完全ではなかった。

ドキュメントから:

QuerySetは次の方法で評価できます。

  • 反復。QuerySetは反復可能であり、最初に反復するときにデータベースクエリを実行します。たとえば、これはデータベース内のすべてのエントリの見出しを出力します:

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

したがって、最初にそのループに入ってquerysetの反復形式を取得すると、1,000万行が一度に取得されます。Djangoが実際に繰り返し処理できるものを返す前に、Djangoがデータベース行をロードし、各行のオブジェクトを作成するまで待機します。その後、すべてが記憶に残り、結果がこぼれてきます。

ドキュメントを読んだところ、iterator()QuerySetの内部キャッシュメカニズムをバイパスするだけです。1つずつ実行するのは理にかなっていると思いますが、逆にデータベースで1,000万回のヒットが必要になります。たぶん、それほど望ましいことではありません。

大規模なデータセットを効率的に反復することは、まだ完全には正しくありませんが、目的に役立つと思われるスニペットがいくつかあります。


1
素晴らしい回答をありがとう、@ eternicode。最後に、目的のデータベースレベルの反復のために、生のSQLにドロップダウンしました。
davidchambers

2
@eternicodeいい答えです、この問題にぶつかってください。それ以来、Djangoに関連する更新はありますか?
–ZólyomiIstván2014

2
Django 1.11以降のドキュメントでは、iterator()はサーバー側カーソルを使用するとしています。
ジェフCジョンソン

42

高速でも効率的でもないかもしれませんが、既製のソリューションとして、ここに記載されているdjangoコアのPaginatorおよびPageオブジェクトを使用しないのはなぜですか。

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その後、プリフェッチに必要
Ken Colton 2017

28

Djangoのデフォルトの動作は、クエリを評価するときにQuerySetの結果全体をキャッシュすることです。QuerySetの反復子メソッドを使用して、このキャッシュを回避できます。

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

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

iterator()メソッドはクエリセットを評価し、クエリセットレベルでキャッシュを行わずに結果を直接読み取ります。この方法を使用すると、一度アクセスするだけでよい多数のオブジェクトを繰り返し処理するときに、パフォーマンスが向上し、メモリが大幅に削減されます。キャッシュは引き続きデータベースレベルで行われることに注意してください。

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

次に、内部的にSQLにオフセットと制限を追加します。


7

大量のレコードの場合、データベースカーソルのパフォーマンスはさらに向上します。Djangoには生のSQLが必要です。DjangoカーソルはSQL cursurとは異なるものです。

Nate Cによって提案されたLIMIT-OFFSETメソッドは、状況に応じて十分かもしれません。大量のデータの場合、同じクエリを何度も実行しなければならず、さらに多くの結果を飛び越えなければならないため、カーソルよりも低速です。


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を使用すると、データベース内のすべてのIDをフェッチしてから、各オブジェクトを個別にフェッチできます。時間の経過とともに、ラージオブジェクトがメモリに作成され、forループが終了するまでガベージコレクションされません。上記のコードは、100番目のアイテムが消費されるたびに手動のガベージコレクションを実行します。


StreamingHttpResponseは解決策になりますか?stackoverflow.com/questions/15359768/...
ratata

2
ただし、これにより、ループの数と同じ数のヒットがデータベースで発生します。
raratiru 2016年

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
これは、iterate(chunk_size = 1000)を使用して1.12に組み込まれるようです
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

通常、この種のタスクには、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 ResultSet全体を一度に実行するのとどのように実行するのですか?

引き続きDjango ORMを使用してクエリを生成できます。queryset.query実行で結果を使用するだけです。
ポール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.