126

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

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

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

Event.objects.iterator()同じように動作するものも試しました。

Django が何をメモリにロードしているのか、なぜこれを行っているのかわかりません。私は、Django がデータベース レベルで結果を繰り返し処理することを期待していました。これは、結果がほぼ一定の速度で出力されることを意味します (長時間待機した後にすべてを一度に出力するのではなく)。

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

(関係あるかわかりませんがPostgreSQLを使っています。)

4

9 に答える 9

123

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

ドキュメントから:

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

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

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

したがって、最初にそのループに入り、クエリセットの反復形式を取得すると、1,000万行が一度に取得されます。発生する待機は、Djangoがデータベース行をロードし、各行のオブジェクトを作成してから、実際に反復できるものを返すことです。そうすれば、すべてが記憶に残り、結果が溢れ出てきます。

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

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

于 2010-11-19T05:44:27.873 に答える
32

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

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
于 2015-07-20T20:18:56.157 に答える
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 にオフセットと制限を内部的に追加します。

于 2010-11-19T05:17:49.017 に答える
8

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

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

于 2010-11-19T07:57:14.797 に答える
6

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

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):
            yield 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これは、マルチプロセッシングを使用して複数のオブジェクトを並行して実行することでさらに改善できます。

于 2015-04-04T20:22:03.643 に答える
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
于 2015-10-29T08:07:28.477 に答える