web-dev-qa-db-ja.com

大きなDjango= QuerySetで大量のメモリを消費しているのはなぜですか?

問題のテーブルには、およそ1,000万行が含まれています。

_for event in Event.objects.all():
    print event
_

これにより、メモリ使用量が4 GB程度まで着実に増加し、その時点で行が急速に印刷されます。最初の行が印刷されるまでの長い遅延に驚きました。ほとんどすぐに印刷されると予想していました。

Event.objects.iterator()も試してみましたが、これは同じように動作します。

Djangoがメモリにロードしているもの、またはなぜこれを行っているのかがわかりません。 Djangoは、データベースレベルで結果を反復処理することを期待していました。つまり、結果はほぼ一定の速度で印刷されます(長い待機の後、一度にすべてが印刷されるわけではありません)。

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

(関連があるかどうかはわかりませんが、PostgreSQLを使用しています。)

93
davidchambers

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

ドキュメント から:

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

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

    _for e in Entry.objects.all():
        print e.headline
    _

そのため、ループを最初に入力して反復形式のクエリセットを取得すると、1,000万行が一度に取得されます。あなたが経験するのはDjangoデータベース行をロードし、各行のオブジェクトを作成してから、実際に反復することができるものを返すことです。

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

大規模なデータセットを効率的に反復処理することはまだ適切ではありませんが、目的に役立つスニペットがいくつかあります。

94
eternicode

速くも効率的でもないかもしれませんが、既製のソリューションとして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
36
mpaf

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

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

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

Iterator()メソッドはquerysetを評価し、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
24
Luke Moore

これはドキュメントからです: 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
nate c

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番目のアイテムが消費されるたびに手動でガベージコレクションを実行します。

6
Kracekumar

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

Nate Cによって提案されたLIMIT-OFFSETメソッドは、状況に応じて十分かもしれません。大量のデータの場合、同じクエリを何度も実行する必要があり、より多くの結果をジャンプする必要があるため、カーソルよりも低速です。

6
Frank Heikens

そのため、クエリセット全体のオブジェクトが一度にメモリにロードされるためです。クエリセットを小さな消化可能なビットに分割する必要があります。これを行うパターンは、スプーンフィードと呼ばれます。以下に簡単な実装を示します。

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を複数のオブジェクトで並行して実行することでさらに改善できます。

4
F. Malina

ここで、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
2
danius

通常、この種のタスクでは、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全体を一度に取得する方法と同じ
0
Tho