bellek açısından verimli yerleşik SqlAlchemy yineleyici / oluşturucu?


91

SqlAlchemy kullanarak arabirim oluşturduğum bir ~ 10M kayıt MySQL tablom var. Bu tablonun büyük alt kümelerindeki sorguların, veri kümesinin bit büyüklüğünde parçalarını akıllıca getiren yerleşik bir jeneratör kullandığımı düşünmeme rağmen çok fazla bellek tüketeceğini buldum:

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

Bundan kaçınmak için, parçalar halinde ısıran kendi yineleyicimi oluşturmam gerektiğini anlıyorum:

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)

Bu normal mi yoksa SA yerleşik jeneratörleriyle ilgili eksik olduğum bir şey mi var?

Cevabı bu soruya bellek tüketimi beklenen olmadığını belirtmek gibi görünüyor.


"Şey" vermesi dışında çok benzer bir şeyim var. Diğer tüm çözümlerden daha iyi çalışıyor
iElectric

2
Thing.id> lastThingID değil mi? Ve "satırlar" nedir?
2013 1

Yanıtlar:


118

Çoğu DBAPI uygulaması, getirilirken satırları tam olarak arabelleğe alır - bu nedenle genellikle, SQLAlchemy ORM bir sonucu tutmadan önce, tüm sonuç kümesi bellekte bulunur.

Ancak daha sonra, Querysize nesnelerinize dönmeden önce varsayılan olarak verilen sonuç kümesini tamamen yüklemesinin yolu çalışır. Buradaki mantık, basit SELECT ifadelerinden daha fazlası olan sorgularla ilgilidir. Örneğin, aynı nesne kimliğini bir sonuç kümesinde birden çok kez döndürebilen diğer tablolarla birleştirmelerde (istekli yüklemeyle ortaktır), tüm satır kümesinin bellekte olması gerekir, böylece doğru sonuçlar, aksi takdirde koleksiyonlar ve benzeri döndürülebilir. yalnızca kısmen nüfuslu olabilir.

Bu nedenle Query, bu davranışı değiştirme seçeneği sunar yield_per(). Bu çağrı Query, toplu iş boyutunu verdiğiniz toplu işlerde satırlar vermesine neden olacaktır . Dokümanların belirttiği gibi, bu yalnızca herhangi bir istekli koleksiyon yüklemesi yapmıyorsanız uygundur, bu nedenle temelde ne yaptığınızı gerçekten biliyorsanız. Ayrıca, temeldeki DBAPI satırları önceden tamponlarsa, yine de bu bellek yükü olacaktır, bu nedenle yaklaşım, kullanmamaktan biraz daha iyi ölçeklenir.

Neredeyse hiç kullanmıyorum yield_per(); bunun yerine, pencere işlevlerini kullanarak yukarıda önerdiğiniz LIMIT yaklaşımının daha iyi bir sürümünü kullanıyorum. LIMIT ve OFFSET, çok büyük OFFSET değerlerinin sorgunun daha yavaş ve daha yavaş olmasına neden olduğu büyük bir soruna sahiptir, çünkü N'nin OFFSET'i N satırdan geçmesine neden olur - aynı sorguyu bir yerine elli kez yapmak, her seferinde a daha büyük ve daha fazla sayıda satır. Pencere işlevi yaklaşımıyla, seçmek istediğim tablonun parçalarına atıfta bulunan bir dizi "pencere" değerini önceden getiriyorum. Daha sonra, her biri bir seferde bu pencerelerin birinden çekilen ayrı SELECT ifadeleri yayınlıyorum.

Pencere işlevi yaklaşımı wiki'de ve onu büyük bir başarıyla kullanıyorum.

Ayrıca, tüm veritabanları pencere işlevlerini desteklemez; Postgresql, Oracle veya SQL Server'a ihtiyacınız var. En azından Postgresql kullanan IMHO kesinlikle buna değer - ilişkisel bir veritabanı kullanıyorsanız, en iyisini kullanabilirsiniz.


Query'nin kimlikleri karşılaştırmak için her şeyi temsil ettiğini söylüyorsunuz. Bu, birincil anahtara göre sıralama yapılarak ve yalnızca ardışık sonuçları karşılaştırarak önlenebilir mi?
Tobu

sorun şu ki, X kimliğine sahip bir örnek verirseniz, uygulama onu ele geçirir ve sonra bu varlığa göre kararlar verir ve hatta onu değiştirebilir. Daha sonra, belki (aslında genellikle) bir sonraki satırda bile, aynı kimlik, belki de koleksiyonlarına daha fazla içerik eklemek için sonuçta geri gelir. Bu nedenle başvuru, nesneyi eksik bir durumda aldı. sıralama burada yardımcı olmaz çünkü en büyük sorun, istekli yüklemenin çalışmasıdır - hem "birleştirilmiş" hem de "alt sorgu" yüklemesinin farklı sorunları vardır.
zzzeek

"Sonraki satır koleksiyonları günceller" olayını anladım, bu durumda koleksiyonların ne zaman tamamlandığını bilmek için yalnızca bir db satırı ileriye bakmanız gerekir. İstekli yüklemenin uygulanmasının sıralama ile işbirliği yapması gerekir, böylece koleksiyon güncellemeleri her zaman bitişik satırlarda yapılır.
Tobu

verim_per () seçeneği, gönderdiğiniz sorgunun kısmi sonuç kümeleri teslim etmekle uyumlu olduğundan emin olduğunuzda her zaman oradadır. Her durumda bu davranışı etkinleştirmeye çalışarak birkaç günlük bir maraton seansı geçirdim, her zaman belirsizdi, yani programınız bunlardan birini kullanana kadar, başarısız olan kenarlar. Özellikle, siparişe güvenmek varsayılamaz. Her zaman olduğu gibi, gerçek kod katkılarından memnuniyet duyarım.
zzzeek

1
Postgres kullandığım için Repeatable Read salt okunur işlemi kullanmak ve bu işlemdeki tüm pencereli sorguları çalıştırmak mümkün gibi görünüyor.
schatten

25

Ben bir veritabanı uzmanı değilim, ancak SQLAlchemy'yi basit bir Python soyutlama katmanı olarak kullanırken (yani, ORM Sorgu nesnesini kullanmadan), bellek kullanımını patlatmadan 300M satırlık bir tabloyu sorgulamak için tatmin edici bir çözüm buldum ...

İşte sahte bir örnek:

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)

Ardından, fetchmany()sonuçları sonsuz bir whiledöngüde yinelemek için SQLAlchemy yöntemini kullanıyorum :

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

Bu yöntem, herhangi bir tehlikeli bellek ek yükü olmadan her türlü veri toplamayı yapmama izin verdi.

NOTE stream_resultsPostgres ve çalışır pyscopg2adaptör, ama olmaz herhangi dbapi ile iş, ne de herhangi bir veritabanı sürücüsü ile galiba ...

Bu blog yazısında yukarıdaki yöntemime ilham veren ilginç bir kullanım alanı var .


1
Postgres veya mysql (with pymysql) üzerinde çalışıyorsanız , bu kabul edilen cevap IMHO olmalıdır.
Yuki Inoue

1
Hayatımı kurtardı, sorgularımın gittikçe yavaşladığını görüyorum Yukarıdakileri pyodbc'de (sql sunucusundan postgres'e) uyguladım ve bir rüya gibi çalışıyor.
Ed Baker

Bu benim için en iyi yaklaşımdı. ORM kullandığım için, SQL'i kendi lehçeme (Postgres) derlemem ve ardından yukarıda gösterildiği gibi doğrudan bağlantıdan (oturumdan değil) çalıştırmam gerekiyordu. Bu diğer soru stackoverflow.com/questions/4617291'de bulduğum "nasıl yapılır" derlemesi . Hızdaki artış büyüktü. JOINS'den SUBQUERIES'e geçiş, performansta da büyük bir artış oldu. Ayrıca sqlalchemy_mixins'i kullanmanızı öneririz, smart_query kullanmak en verimli sorguyu oluşturmak için çok yardımcı oldu. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

14

SQLAlchemy ile verimli geçiş / sayfalama araştırıyorum ve bu yanıtı güncellemek istiyorum.

Bir sorgunun kapsamını uygun şekilde sınırlandırmak için dilim çağrısını kullanabileceğinizi ve onu verimli bir şekilde yeniden kullanabileceğinizi düşünüyorum.

Misal:

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

Bu çok basit ve hızlı görünüyor. Bunun .all()gerekli olduğundan emin değilim . İlk aramadan sonra hızın çok arttığını fark ettim.
hamx0r

@ hamx0r Bunun eski bir yorum olduğunu anlıyorum, bu yüzden onu gelecek nesillere bırakıyorum. Things .all()değişkeni, len () 'i desteklemeyen bir sorgudur
David

9

Joel'in cevabının ruhuna uygun olarak şunu kullanıyorum:

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 () sonunda [] döndürür ve döngü asla
kesilmez

4

LIMIT / OFFSET kullanmak kötüdür çünkü daha önce tüm {OFFSET} sütunlarını bulmanız gerekir, bu nedenle ne kadar büyükse OFFSET olur - o kadar uzun istek alırsınız. Benim için pencereli sorgu kullanmak, büyük miktarda veriye sahip büyük tabloda da kötü sonuçlar veriyor (ilk sonuçları çok uzun süre bekliyorsunuz, bu benim durumumda yığınlanmış web yanıtı için iyi değil).

Burada verilen en iyi yaklaşım https://stackoverflow.com/a/27169302/450103 . Benim durumumda sorunu sadece datetime alanındaki indeksi kullanarak ve datetime> = previous_datetime ile sonraki sorguyu getirerek çözdüm. Aptalca, çünkü bu dizini daha önce farklı durumlarda kullandım, ancak pencereli tüm verileri getirmek için sorgunun daha iyi olacağını düşündüm. Benim durumumda yanılmışım.


3

AFAIK, ilk varyant hala tablodan (tek bir SQL sorgusuyla) tüm tupl'leri alır, ancak yineleme sırasında her varlık için ORM sunumunu oluşturur. Bu nedenle, yinelemeden önce tüm varlıkların bir listesini oluşturmaktan daha etkilidir, ancak yine de tüm (ham) verileri belleğe almanız gerekir.

Bu nedenle, LIMIT'i büyük masalarda kullanmak bana iyi bir fikir gibi geliyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.