web-dev-qa-db-ja.com

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

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組み込みジェネレーターに関して欠落しているものがありますか?

この質問 に対する答えは、メモリ消費が予想されないことを示しているようです。

71
Paul

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

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

したがって、Queryには yield_per() を使用してこの動作を変更するオプションがあります。この呼び出しにより、Queryはバッチで行を生成し、そこでバッチサイズを指定します。ドキュメントが述べているように、これはコレクションのどんな種類の熱心なロードもしていない場合にのみ適切であるため、基本的にはあなたが何をしているかを本当に知っている場合です。また、基礎となるDBAPIが行をプリバッファリングする場合、そのメモリオーバーヘッドが依然として存在するため、このアプローチは、使用しない場合よりもわずかに優れたスケーリングしか行いません。

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

ウィンドウ関数アプローチは wiki上 であり、私はそれを大成功で使用しています。

また、すべてのデータベースがウィンドウ機能をサポートしているわけではありません。 Postgresql、Oracle、またはSQL Serverが必要です。少なくともPostgresqlを使用しているIMHOは間違いなく価値があります。リレーショナルデータベースを使用している場合は、最善のものを使用することもできます。

108
zzzeek

私はSQLAlchemyによる効率的なトラバーサル/ページングを検討してきましたが、この回答を更新したいと思います。

スライス呼び出しを使用してクエリの範囲を適切に制限でき、効率的に再利用できると思います。

例:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1
13
Joel

私はデータベースの専門家ではありませんが、SQLAlchemyを単純なPython抽象化レイヤー(つまり、ORM Queryオブジェクトを使用しない)として使用すると、300M-メモリ使用量を爆発させない行テーブル...

ダミーの例を次に示します。

_from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)
_

次に、SQLAlchemy fetchmany()メソッドを使用して、無限のwhileループで結果を反復処理します。

_while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

    for row in batch:
        # Do your stuff here...

proxy.close()
_

この方法により、危険なメモリオーバーヘッドなしであらゆる種類のデータ集約を行うことができました。

NOTE_stream_results_はPostgresおよび_pyscopg2_アダプターで動作しますが、DBAPIやデータベースドライバーでは動作しないと思います...

これには興味深いユースケースがあります ブログ投稿 私の上記の方法に影響を与えました。

8
edouardtheron

ジョエルの答えの精神で、私は以下を使用します:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if things is None:
            break
        for thing in things:
            yield(thing)
        start += WINDOW_SIZE
6

前にすべての{OFFSET}列を見つける必要があるため、LIMIT/OFFSETの使用は適切ではありません。そのため、大きいほどOFFSETになり、リクエストが長くなります。また、ウィンドウクエリを使用すると、大量のデータを含む大きなテーブルで悪い結果が得られます(最初の結果を長時間待つので、私の場合はチャンクWeb応答には適していません)。

ここに与えられた最良のアプローチ https://stackoverflow.com/a/27169302/4501 。私の場合、datetimeフィールドでインデックスを使用し、datetime> = previous_datetimeで次のクエリをフェッチするだけで問題を解決しました。以前はさまざまなケースでそのインデックスを使用していましたが、すべてのデータウィンドウクエリをフェッチする方が良いと考えていたため、愚かです。私の場合、私は間違っていました。

3
Victor Gavro

知る限り、最初のバリアントはテーブルからすべてのタプルを(1つのSQLクエリを使用して)取得しますが、反復時に各エンティティのORMプレゼンテーションを構築します。したがって、反復する前にすべてのエンティティのリストを作成するよりも効率的ですが、すべての(生の)データをメモリにフェッチする必要があります。

したがって、巨大なテーブルでLIMITを使用するのは良いアイデアのように思えます。

2
Pankrat