स्मृति-निर्मित अंतर्निहित SqlAlchemy पुनरावृत्ति / जनरेटर?


90

मेरे पास एक ~ 10M रिकॉर्ड MySQL टेबल है जिसे मैं SqlAlchemy का उपयोग करके इंटरफ़ेस करता हूं। मैंने पाया है कि इस तालिका के बड़े उपसमुच्चय पर क्वेरीज़ बहुत अधिक मेमोरी का उपभोग करेंगी, जबकि मुझे लगा कि मैं एक बिल्ट-इन जनरेटर का उपयोग कर रहा था, जो कि बुद्धिमानी से डेटासेट के बिट्स के आकार का हिस्सा है।

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

2
क्या यह Thing.id> lastThingID नहीं है? और "पंक्तियाँ" क्या है?
synergetic

जवाबों:


118

अधिकांश DBAPI कार्यान्वयन पूरी तरह से बफ़र पंक्तियों के रूप में वे लाए जाते हैं - इसलिए, आमतौर पर SQLAlchemy ORM से पहले भी एक परिणाम की एक पकड़ मिलती है, पूरा परिणाम सेट स्मृति में होता है।

लेकिन फिर, जिस तरह से Queryकाम करता है वह यह है कि यह आपकी वस्तुओं पर लौटने से पहले डिफ़ॉल्ट रूप से दिए गए परिणाम को पूरी तरह से लोड करता है। यहाँ का तर्क उन प्रश्नों का संबंध है जो सरल चयन कथनों से अधिक हैं। उदाहरण के लिए, अन्य तालिकाओं में शामिल होते हैं जो एक ही परिणाम सेट में कई बार एक ही वस्तु की पहचान लौटा सकते हैं (उत्सुक लोडिंग के साथ सामान्य), पंक्तियों के पूर्ण सेट को स्मृति में रखने की आवश्यकता होती है ताकि सही परिणाम अन्यथा संग्रह और जैसे वापस आ सकें केवल आंशिक रूप से आबादी हो सकती है।

इसलिए Queryइस व्यवहार को बदलने का एक विकल्प प्रदान करता है yield_per()। यह कॉल Queryबैचों में पंक्तियों को उत्पन्न करने का कारण होगा , जहाँ आप इसे बैच आकार देते हैं। डॉक्स स्थिति के रूप में, यह केवल तभी उचित है जब आप किसी भी प्रकार के उत्सुक लोडिंग संग्रह नहीं कर रहे हैं, तो यह मूल रूप से है यदि आप वास्तव में जानते हैं कि आप क्या कर रहे हैं। इसके अलावा, यदि अंतर्निहित DBAPI पूर्व-बफ़र्स पंक्तियाँ हैं, तो अभी भी मेमोरी ओवरहेड होगी, इसलिए दृष्टिकोण केवल इसका उपयोग न करने की तुलना में थोड़ा बेहतर होता है।

मैं शायद ही कभी उपयोग करता हूं yield_per(); इसके बजाय, मैं लिमिट दृष्टिकोण का एक बेहतर संस्करण उपयोग करता हूं जो आप विंडो फ़ंक्शन का उपयोग करके ऊपर सुझाते हैं। LIMIT और OFFSET में एक बहुत बड़ी समस्या है कि बहुत बड़े OFFSET मान क्वेरी को धीमा और धीमा करने का कारण बनते हैं, क्योंकि OFFSET N के कारण N पंक्तियों के माध्यम से पृष्ठ बनाता है - यह एक के बजाय एक ही क्वेरी को पचास बार करने की तरह है, प्रत्येक पढ़ने के लिए पंक्तियों की बड़ी और बड़ी संख्या। विंडो-फंक्शन एप्रोच के साथ, मैं "विंडो" मानों का एक सेट पहले से लाती हूं जो उस तालिका के भाग को संदर्भित करता है जिसे मैं चुनना चाहता हूं। मैं तब व्यक्तिगत सेलेक्ट स्टेटमेंट का उत्सर्जन करता हूं जो प्रत्येक एक समय में उन खिड़कियों में से एक से खींचता है।

विंडो फ़ंक्शन का दृष्टिकोण विकी पर है और मैं इसे बड़ी सफलता के साथ उपयोग करता हूं।

यह भी ध्यान दें: सभी डेटाबेस विंडो फ़ंक्शन का समर्थन नहीं करते हैं; आपको Postgresql, Oracle, या SQL Server की आवश्यकता है। कम से कम Postgresql का उपयोग कर IMHO निश्चित रूप से इसके लायक है - यदि आप एक रिलेशनल डेटाबेस का उपयोग कर रहे हैं, तो आप सबसे अच्छा उपयोग कर सकते हैं।


पहचान की तुलना करने के लिए आप सब कुछ उल्लेख करते हैं। क्या प्राथमिक कुंजी पर छँटनी से बचा जा सकता है, और केवल लगातार परिणामों की तुलना कर सकता है?
तोबू

मुद्दा यह है कि यदि आप पहचान एक्स के साथ एक उदाहरण देते हैं, तो एप्लिकेशन को इसकी एक पकड़ मिलती है, और फिर इस इकाई के आधार पर निर्णय लेता है, और शायद इसे उत्परिवर्तित भी करता है। बाद में, शायद (वास्तव में आमतौर पर) यहां तक ​​कि बहुत ही अगली पंक्ति में, एक ही पहचान परिणाम में वापस आती है, शायद इसके संग्रह में अधिक सामग्री जोड़ने के लिए। इसलिए आवेदन को अपूर्ण अवस्था में वस्तु प्राप्त हुई। सॉर्टिंग यहां मदद नहीं करती है क्योंकि सबसे बड़ा मुद्दा उत्सुक लोडिंग का कामकाज है - दोनों "शामिल हुए" और "सबक्वेरी" लोडिंग में अलग-अलग मुद्दे हैं।
zzzeek

मुझे समझ में आया "अगली पंक्ति संग्रह को अपडेट करती है" बात, इस मामले में आपको केवल यह जानने के लिए एक db पंक्ति से आगे देखना होगा कि संग्रह कब पूरा होगा। उत्सुक लोडिंग के कार्यान्वयन को सॉर्ट के साथ सहयोग करना होगा, ताकि संग्रह अपडेट हमेशा आसन्न पंक्तियों पर किया जाए।
तोबू

उपज_पर () विकल्प हमेशा होता है जब आप आश्वस्त होते हैं कि आप जिस क्वेरी को छोड़ रहे हैं वह आंशिक परिणाम सेट देने के साथ संगत है। मैंने मैराथन को कई दिनों के सत्र में बिताया, जो सभी मामलों में इस व्यवहार को सक्षम करने की कोशिश कर रहा था, हमेशा अस्पष्ट थे, अर्थात, जब तक कि आपका कार्यक्रम उनमें से एक का उपयोग नहीं करता, तब तक जो असफल रहे। विशेष रूप से, आदेश देने पर भरोसा नहीं किया जा सकता है। हमेशा की तरह, मैं वास्तविक कोड योगदान में स्वागत करता हूं।
zzzeek

1
चूंकि मैं पोस्टग्रेज का उपयोग कर रहा हूं, ऐसा लगता है कि रिपीटेबल रीड-ओनली ट्रांजैक्शन का उपयोग करना संभव है और उस लेनदेन में सभी विंडो किए गए प्रश्नों को चलाएं।
Schatten

24

मैं एक डेटाबेस विशेषज्ञ नहीं हूं, लेकिन जब SQLAlchemy का उपयोग एक साधारण पायथन अमूर्त परत के रूप में किया जाता है (यानी, ORM क्वेरी ऑब्जेक्ट का उपयोग नहीं कर रहा है) मैं एक संतोषजनक समाधान के साथ आया हूं जिसमें मेमोरी उपयोग के बिना 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)

फिर, मैं fetchmany()अनंत whileलूप में परिणामों पर पुनरावृति करने के लिए SQLAlchemy विधि का उपयोग करता हूं :

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 साथ काम नहीं करेंगे, और न ही किसी भी डेटाबेस ड्राइवर के साथ ...

इस ब्लॉग पोस्ट में एक दिलचस्प usecase है जिसने मेरी उपरोक्त विधि को प्रेरित किया।


1
यदि कोई पोस्टग्रेज या मायस्कल (साथ pymysql) पर काम कर रहा है , तो यह स्वीकृत उत्तर आईएमएचओ होना चाहिए।
युकी इनूए

1
मेरे जीवन को बचाया, मेरे प्रश्नों को धीमी और धीमी गति से चल रहा था। मैंने pyodbc (sql सर्वर से पोस्टग्रेज तक) पर उपरोक्त लिख दिया है और यह एक सपने की तरह चल रहा है।
एड बेकर

यह मेरे लिए सबसे अच्छा तरीका था। जैसा कि मैं ओआरएम का उपयोग कर रहा हूं, मुझे एसक्यूएल को अपनी बोली (पोस्टग्रेज) पर संकलित करने की आवश्यकता है और फिर ऊपर दिखाए गए अनुसार सीधे कनेक्शन (सत्र से नहीं) से निष्पादित करें। संकलन "कैसे" मैं इस अन्य प्रश्न stackoverflow.com/questions/4617291 में पाया । वेग में सुधार बड़ा था। JOINS से ​​SUBQUERIES में परिवर्तन प्रदर्शन में भी एक बड़ी वृद्धि थी। इसके अलावा sqlalchemy_mixins का उपयोग करने की सलाह देते हैं, smart_query के उपयोग से सबसे कुशल क्वेरी बनाने में बहुत मदद मिली। github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

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 मुझे एहसास है कि यह एक पुरानी टिप्पणी है, इसलिए इसे सिर्फ पोस्टरिटी के लिए छोड़ देना चाहिए। .all()चीजों के बिना चर एक क्वेरी है जो लेन का समर्थन नहीं करता है ()
डेविड

9

जोएल के उत्तर की भावना में, मैं निम्नलिखित का उपयोग करता हूं:

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

चीजें = query.slice (शुरू, रोक) (.all) () वापस आ जाएगी [] अंत में और जबकि लूप कभी नहीं टूटेगा
मार्टिन रेगुल

4

LIMIT / OFFSET का उपयोग करना बुरा है, क्योंकि आपको पहले सभी {OFFSET} कॉलम खोजने की आवश्यकता है, इसलिए जितना बड़ा OFFSET है - उतना लंबा अनुरोध। मेरे लिए विंडो की गई क्वेरी का उपयोग करना बड़ी मात्रा में डेटा के साथ बड़ी तालिका पर खराब परिणाम देता है (आप बहुत लंबे समय के लिए पहले परिणाम की प्रतीक्षा करते हैं, कि यह मेरे लिए मेरे विचार से अच्छा नहीं है)।

बेस्ट यहाँ दी दृष्टिकोण https://stackoverflow.com/a/27169302/450103 । मेरे मामले में मैंने केवल डेटाइम टाइम पर इंडेक्स का उपयोग करके और डेटटाइम> = पिछले_डाइमटाइम के साथ अगली क्वेरी लाने के लिए समस्या का समाधान किया। मूर्ख, क्योंकि मैंने पहले अलग-अलग मामलों में उस सूचकांक का उपयोग किया था, लेकिन सोचा था कि सभी डेटा विंडो क्वेरी को लाने के लिए बेहतर होगा। मेरे मामले में मैं गलत था।


3

AFAIK, पहला संस्करण अभी भी तालिका से सभी टुपल्स (एक SQL क्वेरी के साथ) प्राप्त करता है, लेकिन पुनरावृत्ति होने पर प्रत्येक इकाई के लिए ORM प्रस्तुति बनाता है। इसलिए यह पुनरावृत्ति करने से पहले सभी संस्थाओं की सूची बनाने से अधिक कुशल है लेकिन आपको अभी भी सभी (कच्चे) डेटा को स्मृति में लाना है।

इस प्रकार, विशाल तालिकाओं पर लिमिट का उपयोग करना मेरे लिए एक अच्छा विचार है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.