42

私は非常に大きなテーブルを持っています。現在、MySQL データベースにあります。私はジャンゴを使用しています。

特定のデータを事前に計算するために、テーブルの各要素を反復処理する必要があります (おそらく、私の方が優れていれば別の方法で実行できますが、それは重要ではありません)。

メモリを一定に使用して、反復をできるだけ高速に保ちたいと思います。

*Large* Django QuerySet でのメモリ使用の制限と、大規模な Django QuerySet を反復処理すると大量のメモリが消費されるのはなぜですか? 、djangoのすべてのオブジェクトに対する単純な反復は、データベースからすべてのオブジェクトを取得するため、マシンを強制終了します。

解決に向けて

まず第一に、メモリ消費を減らすには、DEBUG が False であることを確認する必要があります (または、カーソルにモンキー パッチを適用します: settings.DEBUG? を保持しながら SQL ロギングをオフにします) connections

でもそれにしても、

for model in Model.objects.all()

はダメです。

わずかに改善されたフォームでさえありません:

for model in Model.objects.all().iterator()

を使用iterator()すると、キャッシュの結果を内部に保存しないため、メモリを節約できます (必ずしも PostgreSQL である必要はありません!)。ただし、データベースからオブジェクト全体を取得するようです。

素朴な解決策

最初の質問の解決策は、カウンターに基づいて結果を a でスライスすることchunk_sizeです。書き方はいくつかありますが、基本的にはすべてOFFSET + LIMITSQL でのクエリになります。

何かのようなもの:

qs = Model.objects.all()
counter = 0
count = qs.count()
while counter < count:     
    for model in qs[counter:counter+count].iterator()
        yield model
    counter += chunk_size

これはメモリ効率に優れていますが (一定chunk_sizeのメモリ使用量は に比例します)、速度の点では非常に貧弱です: OFFSET が大きくなると、MySQL と PostgreSQL の両方 (およびおそらくほとんどの DB) が窒息し始め、速度が低下します。

より良い解決策

Thierry Schellenbach によるこの投稿で、より良い解決策を利用できます。PK でフィルタリングします。これは、オフセットよりもはるかに高速です (速度はおそらく DB に依存します)。

pk = 0
last_pk = qs.order_by('-pk')[0].pk
queryset = qs.order_by('pk')
while pk < last_pk:
    for row in qs.filter(pk__gt=pk)[:chunksize]:
        pk = row.pk
        yield row
    gc.collect()

これは満足のいくものになり始めています。現在、メモリ = O(C)、速度 ~= O(N)

「より良い」ソリューションの問題

より優れたソリューションは、PK が QuerySet で使用可能な場合にのみ機能します。残念ながら、常にそうであるとは限りません。特に、QuerySet に個別の (group_by) および/または値 (ValueQuerySet) の組み合わせが含まれている場合はそうです。

そのような状況では、「より良い解決策」は使用できません。

もっとうまくやれるでしょうか?

PK のない QuerySets に関する問題を回避して、より速く進めることができるかどうか疑問に思っています。他の回答で見つけたものを使用している可能性がありますが、純粋な SQL でのみ: using cursors .

私は生の SQL、特に Django が苦手なので、本当の質問は次のとおりです。

大きなテーブル用のより良い Django QuerySet Iterator を構築するにはどうすればよいですか

私が読んだことからの私の見解は、サーバー側のカーソルを使用する必要があるということです (明らかに (参照を参照) 標準の Django カーソルを使用しても同じ結果は得られません。これは、デフォルトで python-MySQL コネクタと psycopg コネクタの両方が結果をキャッシュするためです)。

これは本当に高速な (および/またはより効率的な) ソリューションでしょうか?

djangoで生のSQLを使用してこれを行うことはできますか? それとも、データベース コネクタに応じて特定の Python コードを記述する必要がありますか?

PostgreSQLおよびMySQLのサーバー側カーソル

とりあえず手に入るのはここまで…

ジャンゴchunked_iterator()

もちろん、このメソッドがqueryset.iterator()ではなくとして機能し、 iterate(queryset)django コアまたは少なくともプラグ可能なアプリの一部になるのが最善です。

更新いくつかの追加情報を含むdjangoチケットを見つけてくれたコメントの「T」に感謝します。chunkedコネクタの動作の違いにより、おそらく最善の解決策は、透過的に拡張するのではなく、特定のメソッドを作成することiteratorです (私にとっては良いアプローチのように思えます)。実装スタブは存在しますが、この1 年間は何の作業も行われておらず、作成者はまだそれに飛びつく準備ができていないようです。

追加の参照:

  1. MYSQL の LIMIT オフセットが高いとクエリが遅くなるのはなぜですか?
  2. LIMIT 句で大きなオフセットを使用して MySQL クエリを高速化するにはどうすればよいですか?
  3. http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
  4. postgresql: オフセット + 制限が非常に遅くなる
  5. PostgreSQL での OFFSET パフォーマンスの向上
  6. http://www.depesz.com/2011/05/20/pagination-with-fixed-order/
  7. MySQL の Python Server Side Cursor で行ごとの MySQL ResultSet を取得する方法

編集:

Django 1.6 は永続的なデータベース接続を追加しています

Django データベースの永続的な接続

これにより、状況によっては、カーソルの使用が容易になります。それでも、そのようなソリューションを実装する方法は、私の現在のスキル(および学習する時間)の範囲外です..

また、「より良い解決策」はすべての状況で確実に機能するわけではなく、一般的なアプローチとして使用することはできず、ケースバイケースで適応されるスタブのみです...

4

3 に答える 3

3

簡単な答え

特別なことをせずにテーブル自体を繰り返し処理する必要がある場合、Django には組み込みの iteratorが付属しています。

queryset.iterator()

これにより、Django は自身のキャッシュをクリーンアップしてメモリ使用量を削減します。非常に大きなテーブルの場合、これでは不十分な場合があることに注意してください。


複雑な答え

各オブジェクトでより複雑なことを行っている場合、または大量のデータがある場合は、独自に作成する必要があります。以下は、クエリセットをチャンクに分割し、基本的なイテレータよりもそれほど遅くないクエリセット イテレータです (1 ではなく線形数のデータベース クエリになりますが、1,000 行ごとに 1 つのクエリのみになります)。ほとんどの SQL データベースではオフセットは線形時間操作であるため、この関数は主キーでページングします。これは効率的な実装に必要です。

def queryset_iterator(queryset, page_size=1000):
    if not queryset:
        return
    max_pk = queryset.order_by("-pk")[0].pk
    # Scale the page size up by the average density of primary keys in the queryset
    adjusted_page_size = int(page_size * max_pk / queryset.count())
    
    pages = int(max_pk / adjusted_page_size) + 1
    for page_num in range(pages):
        lower = page_num * adjusted_page_size
        page = queryset.filter(pk__gte=lower, pk__lt=lower+page_size)
        for obj in page:
            yield obj

使い方は次のようになります:

for obj in queryset_iterator(Model.objects.all()):
    # do stuff

このコードには、次の 3 つの前提があります。

  1. 主キーは整数です (これは UUID 主キーでは機能しません)。
  2. クエリセットの主キーは、少なくともある程度均一に分散されています。これが当てはまらない場合、adjusted_page_size缶が大きくなりすぎて、反復の一部として 1 つまたは複数の大量のページを取得する可能性があります。

オーバーヘッドを把握するために、40,000 エントリの Postgres テーブルでこれをテストしました。queryset_iterator は、反復時間と生の反復に約 80% を追加します (2.2 秒対 1.2 秒)。このオーバーヘッドは、ページ サイズが 200 から 10,000 の間で大きく変化しませんが、200 を下回ると上昇し始めます。

于 2013-12-12T21:24:36.540 に答える
0

利用可能な別のオプションがあります。反復が速くなることはありませんが(実際には遅くなる可能性があります)、使用するメモリがはるかに少なくなります。ニーズによっては、これが適切な場合があります。

large_qs = MyModel.objects.all().values_list("id", flat=True)
for model_id in large_qs:
    model_object = MyModel.objects.get(id=model_id)
    # do whatever you need to do with the model here

ID のみがメモリにロードされ、オブジェクトは必要に応じて取得および破棄されます。データベースの負荷が増加し、ランタイムが遅くなることに注意してください。どちらもメモリ使用量の削減とのトレードオフです。

ワーカー インスタンスで非同期のスケジュールされたタスクを実行するときにこれを使用しましたが、遅いかどうかは問題ではありませんが、あまりにも多くのメモリを使用しようとすると、インスタンスがクラッシュし、プロセスが中止される可能性があります。

于 2013-01-07T18:25:24.940 に答える