iterator / generator built-in SqlAlchemy yang hemat memori?

90

Saya memiliki ~ 10M record tabel MySQL yang saya antarmuka dengan menggunakan SqlAlchemy. Saya telah menemukan bahwa kueri pada subset besar dari tabel ini akan menghabiskan terlalu banyak memori meskipun saya pikir saya menggunakan generator bawaan yang secara cerdas mengambil potongan set data berukuran gigitan:

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

Untuk menghindari ini, saya merasa saya harus membangun iterator saya sendiri yang menggigit menjadi beberapa bagian:

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)

Apakah ini normal atau ada sesuatu yang saya lewatkan tentang generator built-in SA?

Jawaban atas pertanyaan ini tampaknya menunjukkan bahwa konsumsi memori tidak diharapkan.

Paul
sumber
Saya punya sesuatu yang sangat mirip, hanya saja itu menghasilkan "benda". Bekerja lebih baik dari semua solusi lainnya
iElectric
2
Bukankah Thing.id> lastThingID? Dan apa itu "baris"?
sinergis

Jawaban:

118

Sebagian besar implementasi DBAPI sepenuhnya buffer baris saat diambil - jadi biasanya, bahkan sebelum SQLAlchemy ORM mendapatkan satu hasil, seluruh kumpulan hasil ada di memori.

Tapi kemudian, cara Querykerjanya adalah itu sepenuhnya memuat hasil yang diberikan set secara default sebelum mengembalikan kepada Anda objek Anda. Alasan di sini menganggap kueri yang lebih dari sekadar pernyataan SELECT. Misalnya, dalam penggabungan ke tabel lain yang mungkin mengembalikan identitas objek yang sama beberapa kali dalam satu set hasil (umum dengan eager loading), set baris lengkap perlu berada dalam memori sehingga hasil yang benar dapat dikembalikan jika tidak koleksi dan semacamnya. mungkin hanya terisi sebagian.

Jadi Querymenawarkan opsi untuk mengubah perilaku ini yield_per(). Panggilan ini akan menyebabkan Queryuntuk menghasilkan baris dalam kelompok, di mana Anda memberinya ukuran kelompok. Seperti yang dinyatakan oleh dokumen, ini hanya sesuai jika Anda tidak melakukan pemuatan koleksi apa pun sehingga pada dasarnya Anda benar-benar tahu apa yang Anda lakukan. Selain itu, jika baris pra-buffer DBAPI yang mendasari, masih akan ada overhead memori sehingga pendekatannya hanya berskala sedikit lebih baik daripada tidak menggunakannya.

Saya hampir tidak pernah menggunakan yield_per(); sebaliknya, saya menggunakan versi yang lebih baik dari pendekatan LIMIT yang Anda sarankan di atas menggunakan fungsi jendela. LIMIT dan OFFSET memiliki masalah besar sehingga nilai OFFSET yang sangat besar menyebabkan kueri menjadi lebih lambat dan lebih lambat, karena OFFSET dari N menyebabkannya halaman melalui baris N - ini seperti melakukan kueri yang sama lima puluh kali daripada satu, setiap kali membaca a jumlah baris yang lebih besar dan lebih besar. Dengan pendekatan fungsi jendela, saya mengambil terlebih dahulu satu set nilai "jendela" yang mengacu pada potongan tabel yang ingin saya pilih. Saya kemudian memancarkan pernyataan SELECT individu yang masing-masing menarik dari salah satu jendela itu pada satu waktu.

Pendekatan fungsi jendela ada di wiki dan saya menggunakannya dengan sangat sukses.

Perhatikan juga: tidak semua database mendukung fungsi jendela; Anda membutuhkan Postgresql, Oracle, atau SQL Server. IMHO menggunakan setidaknya Postgresql pasti sepadan - jika Anda menggunakan database relasional, Anda mungkin juga menggunakan yang terbaik.

zzzeek
sumber
Anda menyebutkan Query instanciates segalanya untuk membandingkan identitas. Bisakah ini dihindari dengan mengurutkan pada kunci utama, dan hanya membandingkan hasil yang berurutan?
Tobu
Masalahnya adalah jika Anda menghasilkan instance dengan identitas X, aplikasi memegangnya, lalu membuat keputusan berdasarkan entitas ini, dan bahkan mungkin memutasinya. Kemudian, mungkin (sebenarnya biasanya) bahkan di baris berikutnya, identitas yang sama muncul kembali di hasil, mungkin untuk menambahkan lebih banyak konten ke koleksinya. Oleh karena itu, aplikasi menerima objek dalam keadaan tidak lengkap. penyortiran tidak membantu di sini karena masalah terbesar adalah cara kerja eager loading - baik pemuatan "bergabung" dan "subkueri" memiliki masalah yang berbeda.
zzzeek
Saya memahami hal "baris berikutnya memperbarui koleksi", dalam hal ini Anda hanya perlu melihat ke depan dengan satu baris db untuk mengetahui kapan koleksi selesai. Implementasi eager loading harus bekerja sama dengan semacam itu, sehingga pembaruan koleksi selalu dilakukan pada baris yang berdekatan.
Tobu
opsi yield_per () selalu ada saat Anda yakin kueri yang Anda keluarkan kompatibel dengan pengiriman kumpulan hasil parsial. Saya menghabiskan sesi beberapa hari maraton mencoba mengaktifkan perilaku ini dalam semua kasus, selalu ada yang tidak jelas, yaitu, sampai program Anda menggunakan salah satunya, tepi yang gagal. Secara khusus, mengandalkan pemesanan tidak dapat diasumsikan. Seperti biasa, saya menyambut kontribusi kode yang sebenarnya.
zzzeek
1
Karena saya menggunakan postgres sepertinya dimungkinkan untuk menggunakan transaksi read-only Read-only yang dapat diulang dan menjalankan semua kueri berjendela dalam transaksi itu.
schatten
24

Saya bukan ahli database, tetapi ketika menggunakan SQLAlchemy sebagai lapisan abstraksi Python sederhana (yaitu, tidak menggunakan objek ORM Query), saya telah menemukan solusi yang memuaskan untuk meminta tabel baris 300M tanpa ledakan penggunaan memori ...

Berikut adalah contoh dummy:

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)

Kemudian, saya menggunakan fetchmany()metode SQLAlchemy untuk mengulang hasil dalam whileloop tak terbatas :

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()

Metode ini memungkinkan saya melakukan semua jenis agregasi data tanpa overhead memori yang berbahaya.

NOTE yang stream_resultsbekerja dengan Postgres dan pyscopg2adaptor, tapi saya kira itu tidak akan bekerja dengan DBAPI apapun, juga dengan database driver ...

Ada kasus penggunaan yang menarik dalam posting blog ini yang menginspirasi metode saya di atas.

edouardtheron.dll
sumber
1
Jika seseorang bekerja pada postgres atau mysql (dengan pymysql), ini harus menjadi jawaban yang diterima IMHO.
Yuki Inoue
1
Menyelamatkan hidup saya, melihat pertanyaan saya berjalan semakin lambat. Saya telah menginstrumentasi di atas pada pyodbc (dari sql server ke postgres) dan itu berjalan seperti mimpi.
Ed Baker
Ini bagi saya pendekatan terbaik. Karena saya menggunakan ORM, saya perlu mengkompilasi SQL ke dialek saya (Postgres) dan kemudian mengeksekusi langsung dari koneksi (bukan dari sesi) seperti yang ditunjukkan di atas. Kompilasi "bagaimana" saya temukan di pertanyaan lain stackoverflow.com/questions/4617291 . Peningkatan kecepatannya besar. Perubahan dari JOINS ke SUBQUERIES juga meningkatkan performa secara signifikan. Juga rekomendasikan untuk menggunakan sqlalchemy_mixins, menggunakan smart_query banyak membantu untuk membangun kueri yang paling efisien. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves
14

Saya telah mencari traversal / paging yang efisien dengan SQLAlchemy dan ingin memperbarui jawaban ini.

Saya rasa Anda dapat menggunakan panggilan slice untuk membatasi cakupan kueri dengan benar dan Anda dapat menggunakannya kembali secara efisien.

Contoh:

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
Joel
sumber
Ini sepertinya sangat sederhana dan cepat. Saya tidak yakin .all()itu perlu. Saya perhatikan kecepatannya meningkat pesat setelah panggilan pertama.
hamx0r
@ hamx0r Saya menyadari ini adalah komentar lama jadi biarkan saja untuk anak cucu. Tanpa .all()variabel hal-hal adalah kueri yang tidak mendukung len ()
David
9

Dalam semangat jawaban Joel, saya menggunakan yang berikut ini:

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
Pietro Battiston
sumber
things = query.slice (start, stop) .all () akan kembali [] di akhir dan while loop tidak akan pernah putus
Reguly Martin
4

Menggunakan LIMIT / OFFSET itu buruk, karena Anda perlu menemukan semua kolom {OFFSET} sebelumnya, jadi semakin besar OFFSET - semakin lama permintaan yang Anda dapatkan. Menggunakan kueri berjendela untuk saya juga memberikan hasil yang buruk pada tabel besar dengan sejumlah besar data (Anda menunggu hasil pertama terlalu lama, itu tidak baik dalam kasus saya untuk respons web yang terpotong).

Pendekatan terbaik diberikan di sini https://stackoverflow.com/a/27169302/450103 . Dalam kasus saya, saya menyelesaikan masalah hanya dengan menggunakan indeks pada bidang datetime dan mengambil kueri berikutnya dengan datetime> = before_datetime. Bodoh, karena saya menggunakan indeks itu dalam kasus yang berbeda sebelumnya, tetapi berpikir bahwa untuk mengambil semua data jendela kueri akan lebih baik. Dalam kasus saya, saya salah.

Victor Gavro
sumber
3

AFAIK, varian pertama masih mendapatkan semua tupel dari tabel (dengan satu kueri SQL) tetapi membangun presentasi ORM untuk setiap entitas saat melakukan iterasi. Jadi, ini lebih efisien daripada membuat daftar semua entitas sebelum melakukan iterasi, tetapi Anda masih harus mengambil semua data (mentah) ke dalam memori.

Jadi, menggunakan LIMIT pada tabel besar sepertinya ide yang bagus bagi saya.

Pankrat
sumber