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


90

SqlAlchemyを使用してインターフェースする最大1,000万レコードの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組み込みジェネレーターに関して私が見逃しているものがありますか?

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


私はそれが「もの」を生み出すことを除いて、非常に似たものを持っています。他のすべてのソリューションよりもうまく機能します
iElectric 2013年

2
Thing.id> lastThingIDではありませんか?そして、「行」とは何ですか?
相乗効果のある2013年

回答:


118

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

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

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

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

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

また、すべてのデータベースがウィンドウ関数をサポートしているわけではありません。Postgresql、Oracle、またはSQLServerが必要です。少なくともPostgresqlを使用しているIMHOは、間違いなく価値があります。リレーショナルデータベースを使用している場合は、最高のものを使用したほうがよいでしょう。


あなたは、クエリがアイデンティティを比較するためにすべてをインスタンス化すると述べています。主キーで並べ替え、連続した結果のみを比較することで、これを回避できますか?
東武

問題は、IDがXのインスタンスを生成すると、アプリケーションがそのインスタンスを取得し、このエンティティに基づいて決定を下し、場合によっては変更することです。後で、おそらく(実際には通常)次の行でも、同じIDが結果に戻ってきて、おそらくコレクションにコンテンツを追加します。したがって、アプリケーションはオブジェクトを不完全な状態で受け取りました。最大の問題は積極的な読み込みの動作であるため、並べ替えはここでは役に立ちません。「結合」読み込みと「サブクエリ」読み込みの両方に異なる問題があります。
zzzeek 2012年

「次の行がコレクションを更新する」ことを理解しました。この場合、コレクションがいつ完了するかを知るには、1つのdb行だけ先を見る必要があります。積極的な読み込みの実装は、並べ替えと連携する必要があるため、コレクションの更新は常に隣接する行で行われます。
東武

送信しているクエリが部分的な結果セットの配信と互換性があると確信できる場合は、yield_per()オプションが常に存在します。私はすべての場合にこの動作を有効にしようと数日間のセッションをマラソンに費やしましたが、常にあいまいでした。つまり、プログラムがそれらの1つを使用するまで、エッジが失敗しました。特に、注文に依存することは想定できません。いつものように、私は実際のコードの貢献を歓迎します。
zzzeek 2012年

1
私はpostgresを使用しているので、繰り返し可能な読み取り読み取り専用トランザクションを使用して、そのトランザクションですべてのウィンドウクエリを実行できるようです。
schatten 2014年

24

私はデータベースの専門家ではありませんが、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)

次に、SQLAlchemyfetchmany()メソッドを使用して、結果を無限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_resultsPostgresとpyscopg2アダプタで動作しますが、DBAPIでもデータベースドライバでも動作しないと思います...

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


1
postgresまたはmysql(with pymysql)で作業している場合、これは受け入れられた回答IMHOである必要があります。
井上

1
私の命を救い、私のクエリがどんどん遅くなるのを見ていました。上記をpyodbc(SQLサーバーからpostgresまで)にインストルメントしましたが、夢のように実行されています。
エドベイカー

これは私にとって最良のアプローチでした。ORMを使用しているので、SQLを方言(Postgres)にコンパイルしてから、上記のように接続から(セッションからではなく)直接実行する必要がありました。この他の質問stackoverflow.com/questions/4617291で見つけたコンパイルの「ハウツー」。速度の向上は大きかった。JOINSからSUBQUERIESへの変更も、パフォーマンスの大幅な向上でした。また、sqlalchemy_mixinsを使用することをお勧めします。smart_queryを使用すると、最も効率的なクエリを作成するのに大いに役立ちました。github.com/absent1706/sqlalchemy-mixins
グスタボ・ゴンサルベス

14

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

これは非常に単純で速いようです。.all()必要かどうかわかりません。最初の呼び出しの後、速度が大幅に向上したことに気付きました。
hamx0r 2015年

@ hamx0rこれは古いコメントなので、後世のために残しておきます。.all()things変数がないと、len()をサポートしないクエリになります
David

9

Joelの答えの精神で、私は以下を使用します。

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

things = query.slice(start、stop).all()は最後に[]を返し、whileループは決して壊れません
Martin Reguly

4

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

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


3

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.