1. ホーム
  2. python

[解決済み] メモリ効率の良い組み込み SqlAlchemy イテレータ/ジェネレータ?

2023-01-20 15:03:27

質問

私は SqlAlchemy を使ってインタフェースしている、約 10M レコードの MySQL テーブルを持っています。 このテーブルの大きなサブセットに対するクエリは、データセットの一口サイズのチャンクをインテリジェントにフェッチする組み込みジェネレータを使用しているつもりでも、メモリを過剰に消費することがわかりました。

for thing in session.query(Things):
    analyze(thing)

これを避けるために、私はチャンクで噛み砕いた独自のイテレータを構築する必要があることに気づきました。

lastThingID = None
while True:
    things = query.filter(Thing.id < lastThingID).limit(querySize).all()
    if not rows or len(rows) == 0: 
        break
    for thing in things:
        lastThingID = row.id
        analyze(thing)

これは普通なのでしょうか、それともSA組み込みのジェネレータに関して何か見落としていることがあるのでしょうか?

の答えは この質問 は、メモリ消費量が想定外であることを示しているようです。

どのように解決するのですか?

ほとんどの DBAPI 実装は、行を取得する際に完全にバッファリングします。そのため、通常、 SQLAlchemy ORM が一つの結果を取得する前に、結果セット全体がメモリ内にあります。

しかし、それでは Query が動作する方法は、オブジェクトを返す前に、デフォルトで与えられた結果セットを完全にロードすることです。 ここでの根拠は、単純な SELECT ステートメント以上のクエリに関するものです。たとえば、1 つの結果セットで同じオブジェクト ID を複数回返す可能性のある他のテーブルへの結合 (イーガーローディングと共通) では、正しい結果を返すことができるように、行のフルセットがメモリ内にある必要があり、そうしないとコレクションなどが一部しか読み込まれていない可能性があります。

そのため Query には、この動作を変更するためのオプションが用意されています。 yield_per() . この呼び出しによって Query がバッチで行を生成するようになります。ここでバッチサイズを指定します。 ドキュメントにあるように、これはコレクションのイーガーローディングを行っていない場合にのみ適切で、基本的には自分が何をしているかを本当に知っている場合のみです。 また、基礎となる DBAPI が行をプリバッファリングする場合、メモリ オーバーヘッドが発生するため、このアプローチはそれを使用しない場合よりもわずかに良好にスケールするだけです。

私はほとんど yield_per() 代わりに、ウィンドウ関数を使用して、上記で提案された LIMIT アプローチのより良いバージョンを使用しています。 LIMIT と OFFSET には、OFFSET の値が非常に大きいと、N の OFFSET を使用すると N 行をページングするため、クエリがどんどん遅くなってしまうという大きな問題があります。 ウィンドウ関数のアプローチでは、選択したいテーブルのチャンクを参照する一連のquot;window"値をあらかじめフェッチしておくのです。 そして、一度にこれらのウィンドウの 1 つから取得する個々の SELECT ステートメントを発行します。

ウィンドウ関数のアプローチは にあるように であり、私はそれを使って大きな成功を収めています。

また、すべてのデータベースがウィンドウ関数をサポートしているわけではないことに注意してください:Postgresql、Oracle、またはSQL Serverが必要です。 IMHO では、少なくとも Postgresql を使用することは間違いなく価値があります。