ตัววนซ้ำ / เครื่องกำเนิดไฟฟ้า SqlAlchemy ในตัวที่มีประสิทธิภาพหน่วยความจำ?


91

ฉันมีตารางบันทึก MySQL ~ 10M ที่ฉันเชื่อมต่อโดยใช้ 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ทำงานก็คือโหลดผลลัพธ์ที่กำหนดตามค่าเริ่มต้นอย่างสมบูรณ์ก่อนที่จะส่งคืนวัตถุให้คุณ เหตุผลในที่นี้เกี่ยวกับการสืบค้นที่มากกว่าคำสั่ง SELECT แบบธรรมดา ตัวอย่างเช่นในการรวมเข้ากับตารางอื่นที่อาจส่งคืนข้อมูลประจำตัวของอ็อบเจ็กต์เดียวกันหลายครั้งในชุดผลลัพธ์หนึ่งชุด (โดยทั่วไปกับการโหลดอย่างกระตือรือร้น) แถวทั้งหมดจะต้องอยู่ในหน่วยความจำเพื่อให้สามารถส่งคืนผลลัพธ์ที่ถูกต้องได้มิฉะนั้นจะมีการรวบรวม อาจมีเพียงบางส่วนเท่านั้น

ดังนั้นมีตัวเลือกที่จะเปลี่ยนพฤติกรรมนี้ผ่านQuery yield_per()การเรียกนี้จะทำให้เกิดQueryแถวเป็นแบตช์โดยที่คุณกำหนดขนาดแบทช์ ตามสถานะของเอกสารสิ่งนี้จะเหมาะสมก็ต่อเมื่อคุณไม่ได้ทำการโหลดคอลเลกชันใด ๆ อย่างกระตือรือร้นดังนั้นโดยพื้นฐานแล้วถ้าคุณรู้ว่าคุณกำลังทำอะไร นอกจากนี้หากแถวก่อนบัฟเฟอร์ DBAPI ที่อยู่ภายใต้จะยังคงมีค่าใช้จ่ายของหน่วยความจำนั้นดังนั้นวิธีการจะปรับขนาดได้ดีกว่าการไม่ใช้

ฉันแทบไม่เคยใช้yield_per(); แต่ฉันใช้แนวทาง LIMIT เวอร์ชันที่ดีกว่าที่คุณแนะนำข้างต้นโดยใช้ฟังก์ชันหน้าต่างแทน LIMIT และ OFFSET มีปัญหาใหญ่ที่ค่า OFFSET ที่ใหญ่มากทำให้การสืบค้นช้าลงและช้าลงเนื่องจาก OFFSET ของ N ทำให้หน้าผ่าน N แถว - เหมือนกับการทำแบบสอบถามเดียวกันห้าสิบครั้งแทนที่จะเป็นหนึ่งในแต่ละครั้งที่อ่าน a จำนวนแถวที่มากขึ้นและมากขึ้น ด้วยวิธีการทำงานของหน้าต่างฉันจะดึงชุดของค่า "หน้าต่าง" ไว้ล่วงหน้าซึ่งอ้างถึงส่วนของตารางที่ฉันต้องการเลือก จากนั้นฉันจะปล่อยคำสั่ง SELECT แต่ละรายการที่ดึงจากหน้าต่างเหล่านั้นทีละรายการ

วิธีการทำงานของหน้าต่างอยู่ในวิกิและฉันใช้มันอย่างประสบความสำเร็จ

หมายเหตุ: ฐานข้อมูลบางส่วนไม่รองรับฟังก์ชันหน้าต่าง คุณต้องใช้ Postgresql, Oracle หรือ SQL Server IMHO ที่ใช้ Postgresql เป็นอย่างน้อยก็คุ้มค่าอย่างแน่นอน - หากคุณใช้ฐานข้อมูลเชิงสัมพันธ์คุณอาจใช้สิ่งที่ดีที่สุดเช่นกัน


คุณกล่าวถึง Query เพื่อเปรียบเทียบทุกสิ่งเพื่อเปรียบเทียบตัวตน สิ่งนี้สามารถหลีกเลี่ยงได้โดยการจัดเรียงคีย์หลักและเปรียบเทียบผลลัพธ์ที่ต่อเนื่องกันเท่านั้นหรือไม่?
Tobu

ปัญหาคือถ้าคุณให้อินสแตนซ์ที่มีเอกลักษณ์ X แอปพลิเคชันจะยึดมันไว้แล้วทำการตัดสินใจตามเอนทิตีนี้และอาจกลายพันธุ์ด้วยซ้ำ ในภายหลังบางที (โดยปกติแล้ว) แม้ในแถวถัดไปข้อมูลประจำตัวเดียวกันจะกลับมาในผลลัพธ์บางทีอาจจะเพิ่มเนื้อหาเพิ่มเติมในคอลเล็กชัน แอปพลิเคชันจึงได้รับวัตถุในสถานะที่ไม่สมบูรณ์ การเรียงลำดับไม่ได้ช่วยในที่นี้เนื่องจากปัญหาใหญ่ที่สุดคือการทำงานของการโหลดอย่างกระตือรือร้น - ทั้งการโหลด "เข้าร่วม" และ "เคียวรีย่อย" มีปัญหาที่แตกต่างกัน
zzzeek

ฉันเข้าใจสิ่งที่ "แถวถัดไปอัปเดตคอลเลกชัน" ซึ่งในกรณีนี้คุณจะต้องมองไปข้างหน้าทีละแถวฐานข้อมูลเพื่อให้ทราบเมื่อการรวบรวมเสร็จสมบูรณ์ การดำเนินการโหลดอย่างกระตือรือร้นจะต้องร่วมมือกับการจัดเรียงเพื่อให้การอัปเดตคอลเลกชันเสร็จสิ้นในแถวที่อยู่ติดกัน
Tobu

ตัวเลือก yield_per () จะมีให้เสมอเมื่อคุณมั่นใจว่าข้อความค้นหาที่คุณเปล่งออกมานั้นเข้ากันได้กับการส่งชุดผลลัพธ์บางส่วน ฉันใช้เวลาในการวิ่งมาราธอนหลายวันในการพยายามเปิดใช้งานพฤติกรรมนี้ในทุกกรณีมีความคลุมเครืออยู่เสมอนั่นคือจนกว่าโปรแกรมของคุณจะใช้หนึ่งในนั้นขอบที่ล้มเหลว โดยเฉพาะอย่างยิ่งการพึ่งพาการสั่งซื้อไม่สามารถสันนิษฐานได้ และเช่นเคยฉันยินดีต้อนรับสู่การสนับสนุนโค้ดจริง
zzzeek

1
เนื่องจากฉันใช้ postgres ดูเหมือนว่าจะสามารถใช้ธุรกรรมอ่านอย่างเดียวแบบอ่านซ้ำได้และเรียกใช้แบบสอบถามที่มีหน้าต่างทั้งหมดในธุรกรรมนั้น
schatten

25

ฉันไม่ใช่ผู้เชี่ยวชาญด้านฐานข้อมูล แต่เมื่อใช้ 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)

จากนั้นฉันใช้fetchmany()วิธีSQLAlchemy เพื่อวนซ้ำผลลัพธ์ใน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 ใด ๆ หรือกับไดรเวอร์ฐานข้อมูลใด ๆ ...

มี usecase ที่น่าสนใจในบล็อกโพสต์นี้ซึ่งเป็นแรงบันดาลใจให้กับวิธีการข้างต้นของฉัน


1
หากมีคนทำงานเกี่ยวกับ postgres หรือ mysql (with pymysql) นี่ควรเป็นคำตอบที่ยอมรับ IMHO
Yuki Inoue

1
ช่วยชีวิตฉันได้เห็นข้อความค้นหาของฉันทำงานช้าลงและช้าลง ฉันใช้เครื่องมือข้างต้นใน pyodbc (จากเซิร์ฟเวอร์ sql ไปจนถึง postgres) และมันก็ทำงานเหมือนฝัน
Ed Baker

นี่เป็นแนวทางที่ดีที่สุดสำหรับฉัน ขณะที่ฉันใช้ ORM ฉันจำเป็นต้องรวบรวม SQL เป็นภาษาถิ่นของฉัน (Postgres) จากนั้นดำเนินการโดยตรงจากการเชื่อมต่อ (ไม่ใช่จากเซสชัน) ดังที่แสดงด้านบน คอมไพล์ "วิธีการ" ผมพบว่าในนี้คำถามอื่น ๆ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

@ hamx0r ฉันรู้ว่านี่เป็นความคิดเห็นเก่าดังนั้นเพียงแค่ปล่อยให้ลูกหลาน หากไม่มี.all()ตัวแปร
David

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

things = query.slice (start, stop) .all () จะกลับมา [] ในตอนท้ายและในขณะที่ลูปจะไม่มีวันแตก
Martin Reguly

4

การใช้ LIMIT / OFFSET นั้นไม่ดีเนื่องจากคุณต้องหาคอลัมน์ {OFFSET} ทั้งหมดก่อนดังนั้นยิ่งมีขนาดใหญ่ OFFSET ก็จะยิ่งได้รับคำขอนานขึ้น การใช้คิวรีแบบมีหน้าต่างสำหรับฉันยังให้ผลลัพธ์ที่ไม่ดีบนตารางขนาดใหญ่ที่มีข้อมูลจำนวนมาก (คุณรอผลลัพธ์แรกนานเกินไปซึ่งมันไม่ดีในกรณีของฉันสำหรับการตอบสนองทางเว็บที่เป็นก้อน)

วิธีที่ดีที่สุดให้ที่นี่https://stackoverflow.com/a/27169302/450103 ในกรณีของฉันฉันแก้ไขปัญหาเพียงแค่ใช้ดัชนีในฟิลด์วันที่และเวลาและเรียกแบบสอบถามถัดไปด้วย datetime> = previous_datetime โง่เพราะฉันเคยใช้ดัชนีนั้นในกรณีต่างๆมาก่อน แต่คิดว่าการดึงข้อมูลแบบสอบถามที่มีหน้าต่างทั้งหมดจะดีกว่า ในกรณีของฉันฉันผิด


3

AFAIK ตัวแปรแรกยังคงได้รับ tuples ทั้งหมดจากตาราง (ด้วยแบบสอบถาม SQL เดียว) แต่สร้างงานนำเสนอ ORM สำหรับแต่ละเอนทิตีเมื่อทำซ้ำ ดังนั้นจึงมีประสิทธิภาพมากกว่าการสร้างรายการของเอนทิตีทั้งหมดก่อนที่จะทำซ้ำ แต่คุณยังต้องดึงข้อมูล (ดิบ) ทั้งหมดไปยังหน่วยความจำ

ดังนั้นการใช้ LIMIT บนโต๊ะขนาดใหญ่จึงเป็นความคิดที่ดีสำหรับฉัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.