เพิ่มประสิทธิภาพ SQLAlchemy โดยทำความเข้าใจความแตกต่างที่สำคัญระหว่าง lazy loading และ eager loading คู่มือนี้ครอบคลุมกลยุทธ์ select, selectin, joined และ subquery พร้อมตัวอย่างเพื่อแก้ปัญหา N+1
การแมปความสัมพันธ์ใน SQLAlchemy ORM: เจาะลึก Lazy Loading และ Eager Loading
ในโลกของการพัฒนาซอฟต์แวร์ สะพานที่เชื่อมระหว่างโค้ดเชิงวัตถุที่เราเขียนกับฐานข้อมูลเชิงสัมพันธ์ที่จัดเก็บข้อมูลของเรานั้น ถือเป็นจุดเชื่อมต่อที่สำคัญอย่างยิ่งต่อประสิทธิภาพ สำหรับนักพัฒนา Python แล้ว SQLAlchemy เปรียบเสมือนยักษ์ใหญ่ที่มอบ Object-Relational Mapper (ORM) อันทรงพลังและยืดหยุ่น ช่วยให้เราสามารถโต้ตอบกับตารางในฐานข้อมูลราวกับว่าเป็นเพียงอ็อบเจกต์ Python ธรรมดา โดยลดความซับซ้อนของ SQL ดิบออกไปได้มาก
แต่ความสะดวกสบายนี้มาพร้อมกับคำถามที่ลึกซึ้ง: เมื่อคุณเข้าถึงข้อมูลที่เกี่ยวข้องของอ็อบเจกต์—เช่น หนังสือที่เขียนโดยนักเขียน หรือคำสั่งซื้อที่ลูกค้าสั่ง—ข้อมูลเหล่านั้นถูกดึงมาจากฐานข้อมูลอย่างไรและเมื่อใด? คำตอบอยู่ในกลยุทธ์การโหลดความสัมพันธ์ (relationship loading strategies) ของ SQLAlchemy การเลือกระหว่างกลยุทธ์เหล่านี้อาจหมายถึงความแตกต่างระหว่างแอปพลิเคชันที่เร็วปานสายฟ้ากับแอปพลิเคชันที่หยุดชะงักเมื่อมีภาระงานสูง
คู่มือฉบับสมบูรณ์นี้จะไขความกระจ่างเกี่ยวกับปรัชญาหลักสองประการของการโหลดข้อมูล: Lazy Loading และ Eager Loading เราจะสำรวจ "ปัญหา N+1" อันโด่งดังที่ lazy loading สามารถก่อให้เกิด และเจาะลึกกลยุทธ์ eager loading ต่างๆ—joinedload, selectinload, และ subqueryload—ที่ SQLAlchemy มีให้เพื่อแก้ปัญหานี้ เมื่ออ่านจบ คุณจะมีความรู้ในการตัดสินใจอย่างมีข้อมูลและเขียนโค้ดฐานข้อมูลที่มีประสิทธิภาพสูงสำหรับผู้ใช้งานทั่วโลก
พฤติกรรมเริ่มต้น: ทำความเข้าใจ Lazy Loading
โดยค่าเริ่มต้น เมื่อคุณกำหนดความสัมพันธ์ใน SQLAlchemy มันจะใช้กลยุทธ์ที่เรียกว่า "lazy loading" ชื่อของมันก็สื่อความหมายได้ค่อนข้างดี: ORM จะ 'ขี้เกียจ' และจะไม่ดึงข้อมูลที่เกี่ยวข้องใดๆ จนกว่าคุณจะร้องขออย่างชัดเจน
Lazy Loading คืออะไร?
Lazy loading โดยเฉพาะกลยุทธ์ select จะชะลอการโหลดอ็อบเจกต์ที่เกี่ยวข้องออกไป เมื่อคุณคิวรีหาอ็อบเจกต์แม่ (เช่น Author) ในตอนแรก SQLAlchemy จะดึงข้อมูลเฉพาะสำหรับนักเขียนคนนั้นเท่านั้น คอลเลกชันที่เกี่ยวข้อง (เช่น books ของนักเขียน) จะยังไม่ถูกแตะต้อง จนกว่าโค้ดของคุณจะพยายามเข้าถึงแอตทริบิวต์ author.books เป็นครั้งแรก SQLAlchemy จึงจะตื่นขึ้นมา เชื่อมต่อกับฐานข้อมูล และส่ง SQL query ใหม่ เพื่อดึงหนังสือที่เกี่ยวข้อง
ลองนึกภาพเหมือนการสั่งสารานุกรมหลายเล่ม ด้วย lazy loading คุณจะได้รับเล่มแรกก่อน คุณจะร้องขอและได้รับเล่มที่สองก็ต่อเมื่อคุณพยายามจะเปิดมันจริงๆ
อันตรายที่ซ่อนอยู่: ปัญหา "N+1 Selects"
แม้ว่า lazy loading อาจมีประสิทธิภาพหากคุณไม่ค่อยต้องการข้อมูลที่เกี่ยวข้อง แต่มันก็มีกับดักด้านประสิทธิภาพที่ฉาวโฉ่ที่รู้จักกันในชื่อ ปัญหา N+1 Selects ปัญหานี้เกิดขึ้นเมื่อคุณวนซ้ำคอลเลกชันของอ็อบเจกต์แม่และเข้าถึงแอตทริบิวต์ที่โหลดแบบ lazy สำหรับแต่ละรายการ
ลองดูตัวอย่างคลาสสิก: การดึงนักเขียนทั้งหมดและพิมพ์ชื่อหนังสือของพวกเขา
- คุณส่งหนึ่งคิวรีเพื่อดึงนักเขียน N คน (1 คิวรี)
- จากนั้นคุณวนลูปผ่านนักเขียน N คนเหล่านี้ในโค้ด Python ของคุณ
- ภายในลูป สำหรับนักเขียนคนแรก คุณเข้าถึง
author.booksSQLAlchemy จะส่งคิวรีใหม่เพื่อดึงหนังสือของนักเขียนคนนั้นโดยเฉพาะ - สำหรับนักเขียนคนที่สอง คุณเข้าถึง
author.booksอีกครั้ง SQLAlchemy จะส่งคิวรีอีกอันสำหรับหนังสือของนักเขียนคนที่สอง - กระบวนการนี้จะดำเนินต่อไปสำหรับนักเขียนทั้ง N คน (N คิวรี)
ผลลัพธ์คืออะไร? มีคิวรีทั้งหมด 1 + N รายการถูกส่งไปยังฐานข้อมูลของคุณ หากคุณมีนักเขียน 100 คน คุณกำลังทำการเชื่อมต่อฐานข้อมูลแยกกันถึง 101 ครั้ง! ซึ่งสร้างความหน่วงแฝง (latency) อย่างมีนัยสำคัญและสร้างภาระที่ไม่จำเป็นให้กับฐานข้อมูลของคุณ ทำให้ประสิทธิภาพของแอปพลิเคชันลดลงอย่างรุนแรง
ตัวอย่างการใช้งาน Lazy Loading
ลองดูสิ่งนี้ในโค้ดกันก่อนอื่น เรากำหนดโมเดลของเรา:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import sessionmaker, declarative_base, relationship
Base = declarative_base()
class Author(Base):
__tablename__ = 'authors'
id = Column(Integer, primary_key=True)
name = Column(String)
# This relationship defaults to lazy='select'
books = relationship("Book", back_populates="author")
class Book(Base):
__tablename__ = 'books'
id = Column(Integer, primary_key=True)
title = Column(String)
author_id = Column(Integer, ForeignKey('authors.id'))
author = relationship("Author", back_populates="books")
# Setup engine and session (use echo=True to see generated SQL)
engine = create_engine('sqlite:///:memory:', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# ... (code to add some authors and books)
ตอนนี้ เรามาลองทำให้เกิดปัญหา N+1 กัน:
# 1. Fetch all authors (1 query)
print("--- Fetching Authors ---")
authors = session.query(Author).all()
# 2. Loop and access books for each author (N queries)
print("--- Accessing Books for Each Author ---")
for author in authors:
# This line triggers a new SELECT query for each author!
book_titles = [book.title for book in author.books]
print(f"{author.name}'s books: {book_titles}")
ถ้าคุณรันโค้ดนี้โดยตั้งค่า echo=True คุณจะเห็นรูปแบบต่อไปนี้ในล็อกของคุณ:
--- Fetching Authors ---
SELECT authors.id AS authors_id, authors.name AS authors_name FROM authors
--- Accessing Books for Each Author ---
SELECT books.id AS books_id, ... FROM books WHERE ? = books.author_id
SELECT books.id AS books_id, ... FROM books WHERE ? = books.author_id
SELECT books.id AS books_id, ... FROM books WHERE ? = books.author_id
...
เมื่อไหร่ที่ Lazy Loading เป็นความคิดที่ดี?
แม้จะมีกับดัก N+1 แต่ lazy loading ก็ไม่ได้เลวร้ายเสมอไป มันเป็นเครื่องมือที่มีประโยชน์เมื่อนำไปใช้อย่างถูกต้อง:
- ข้อมูลที่ไม่จำเป็นต้องใช้บ่อย (Optional Data): เมื่อข้อมูลที่เกี่ยวข้องจำเป็นต้องใช้ในสถานการณ์เฉพาะที่ไม่เกิดขึ้นบ่อย ตัวอย่างเช่น การโหลดโปรไฟล์ผู้ใช้ แต่จะดึงข้อมูลบันทึกกิจกรรมโดยละเอียดก็ต่อเมื่อผู้ใช้คลิกปุ่ม "ดูประวัติ" โดยเฉพาะ
- ในบริบทของอ็อบเจกต์เดียว (Single Object Context): เมื่อคุณทำงานกับอ็อบเจกต์แม่เพียงรายการเดียว ไม่ใช่คอลเลกชัน การดึงผู้ใช้หนึ่งคนแล้วเข้าถึงที่อยู่ของพวกเขา (
user.addresses) จะส่งผลให้มีคิวรีเพิ่มขึ้นเพียงหนึ่งครั้ง ซึ่งมักจะเป็นที่ยอมรับได้
ทางออก: การใช้ Eager Loading
Eager loading เป็นทางเลือกเชิงรุกของ lazy loading มันสั่งให้ SQLAlchemy ดึงข้อมูลที่เกี่ยวข้องในเวลาเดียวกับอ็อบเจกต์แม่ โดยใช้กลยุทธ์การคิวรีที่มีประสิทธิภาพมากกว่า วัตถุประสงค์หลักของมันคือการกำจัดปัญหา N+1 โดยลดจำนวนคิวรีลงให้เหลือน้อยและคาดการณ์ได้ (มักจะแค่หนึ่งหรือสองครั้ง)
SQLAlchemy มีกลยุทธ์ eager loading ที่ทรงพลังหลายอย่าง ซึ่งกำหนดค่าโดยใช้ตัวเลือกของคิวรี (query options) เรามาสำรวจกลยุทธ์ที่สำคัญที่สุดกัน
กลยุทธ์ที่ 1: joined Loading
Joined loading อาจเป็นกลยุทธ์ eager loading ที่เข้าใจง่ายที่สุด มันบอกให้ SQLAlchemy ใช้ SQL JOIN (โดยเฉพาะ LEFT OUTER JOIN) เพื่อดึงข้อมูลแม่และข้อมูลลูกที่เกี่ยวข้องทั้งหมดในคิวรีฐานข้อมูลขนาดใหญ่เพียงครั้งเดียว
- วิธีการทำงาน: มันจะรวมคอลัมน์ของตารางแม่และตารางลูกเข้าเป็นชุดผลลัพธ์ (result set) ที่กว้างขึ้นชุดเดียว จากนั้น SQLAlchemy จะจัดการลบข้อมูลอ็อบเจกต์แม่ที่ซ้ำซ้อนออกใน Python และเติมข้อมูลในคอลเลกชันลูก
- วิธีใช้: ใช้ตัวเลือกคิวรี
joinedload
from sqlalchemy.orm import joinedload
# Fetch all authors and their books in a single query
authors = session.query(Author).options(joinedload(Author.books)).all()
for author in authors:
# No new query is triggered here!
book_titles = [book.title for book in author.books]
print(f"{author.name}'s books: {book_titles}")
SQL ที่ถูกสร้างขึ้นจะมีลักษณะประมาณนี้:
SELECT authors.id, authors.name, books.id, books.title, books.author_id
FROM authors LEFT OUTER JOIN books ON authors.id = books.author_id
ข้อดีของ `joinedload`:
- การเชื่อมต่อฐานข้อมูลเพียงครั้งเดียว (Single Database Round Trip): ข้อมูลที่จำเป็นทั้งหมดจะถูกดึงมาในคราวเดียว ซึ่งช่วยลดความหน่วงของเครือข่าย
- มีประสิทธิภาพมาก: สำหรับความสัมพันธ์แบบ many-to-one หรือ one-to-one มักจะเป็นตัวเลือกที่เร็วที่สุด
ข้อเสียของ `joinedload`:
- ผลคูณคาร์ทีเซียน (Cartesian Product): สำหรับความสัมพันธ์แบบ one-to-many อาจนำไปสู่ข้อมูลที่ซ้ำซ้อน หากนักเขียนคนหนึ่งมีหนังสือ 20 เล่ม ข้อมูลของนักเขียน (ชื่อ, ID, ฯลฯ) จะถูกทำซ้ำ 20 ครั้งในชุดผลลัพธ์ที่ส่งจากฐานข้อมูลมายังแอปพลิเคชันของคุณ ซึ่งอาจเพิ่มการใช้หน่วยความจำและแบนด์วิดท์เครือข่าย
- ปัญหากับ LIMIT/OFFSET: การใช้
limit()กับคิวรีที่มีjoinedloadบนคอลเลกชันอาจให้ผลลัพธ์ที่ไม่คาดคิด เนื่องจาก limit จะถูกนำไปใช้กับจำนวนแถวที่ถูก join ทั้งหมด ไม่ใช่จำนวนของอ็อบเจกต์แม่
กลยุทธ์ที่ 2: selectin Loading (ตัวเลือกยอดนิยมในปัจจุบัน)
selectin loading เป็นกลยุทธ์ที่ทันสมัยกว่าและมักจะดีกว่าสำหรับการโหลดคอลเลกชันแบบ one-to-many มันสร้างสมดุลที่ยอดเยี่ยมระหว่างความเรียบง่ายของคิวรีกับประสิทธิภาพ โดยหลีกเลี่ยงข้อเสียหลักๆ ของ `joinedload`
- วิธีการทำงาน: มันจะทำการโหลดในสองขั้นตอน:
- ขั้นแรก มันจะรันคิวรีสำหรับอ็อบเจกต์แม่ (เช่น `authors`)
- จากนั้น มันจะรวบรวม primary key ของอ็อบเจกต์แม่ทั้งหมดที่โหลดมา และส่งคิวรีที่ สอง เพื่อดึงอ็อบเจกต์ลูกที่เกี่ยวข้องทั้งหมด (เช่น `books`) โดยใช้คำสั่ง
WHERE ... IN (...)ที่มีประสิทธิภาพสูง
- วิธีใช้: ใช้ตัวเลือกคิวรี
selectinload
from sqlalchemy.orm import selectinload
# Fetch authors, then fetch all their books in a second query
authors = session.query(Author).options(selectinload(Author.books)).all()
for author in authors:
# Still no new query per author!
book_titles = [book.title for book in author.books]
print(f"{author.name}'s books: {book_titles}")
สิ่งนี้จะสร้าง SQL query ที่สะอาดและแยกกันสองชุด:
-- Query 1: Get the parents
SELECT authors.id AS authors_id, authors.name AS authors_name FROM authors
-- Query 2: Get all related children at once
SELECT books.id AS books_id, ... FROM books WHERE books.author_id IN (?, ?, ?, ...)
ข้อดีของ `selectinload`:
- ไม่มีข้อมูลซ้ำซ้อน: มันหลีกเลี่ยงปัญหาผลคูณคาร์ทีเซียนได้อย่างสมบูรณ์ ข้อมูลแม่และลูกจะถูกถ่ายโอนอย่างสะอาด
- ทำงานร่วมกับ LIMIT/OFFSET ได้: เนื่องจากคิวรีของแม่แยกจากกัน คุณจึงสามารถใช้
limit()และoffset()ได้โดยไม่มีปัญหา - SQL ที่ง่ายกว่า: คิวรีที่สร้างขึ้นมักจะง่ายกว่าสำหรับฐานข้อมูลในการปรับให้เหมาะสม (optimize)
- ตัวเลือกที่ดีที่สุดสำหรับวัตถุประสงค์ทั่วไป: สำหรับความสัมพันธ์แบบ to-many ส่วนใหญ่ นี่คือกลยุทธ์ที่แนะนำ
ข้อเสียของ `selectinload`:
- การเชื่อมต่อฐานข้อมูลหลายครั้ง: มันต้องการคิวรีอย่างน้อยสองครั้งเสมอ แม้จะมีประสิทธิภาพ แต่ในทางเทคนิคแล้วนี่คือการเชื่อมต่อที่มากกว่า `joinedload`
- ข้อจำกัดของ `IN` Clause: ฐานข้อมูลบางแห่งมีข้อจำกัดเกี่ยวกับจำนวนพารามิเตอร์ใน `IN` clause แต่ SQLAlchemy ก็ฉลาดพอที่จะจัดการปัญหานี้ได้โดยการแบ่งการทำงานออกเป็นหลายคิวรีหากจำเป็น แต่มันก็เป็นปัจจัยที่ควรตระหนักไว้
กลยุทธ์ที่ 3: subquery Loading
subquery loading เป็นกลยุทธ์พิเศษที่ทำหน้าที่เป็นลูกผสมระหว่าง `lazy` และ `joined` loading มันถูกออกแบบมาเพื่อแก้ปัญหาเฉพาะของการใช้ joinedload ร่วมกับ limit() หรือ offset()
- วิธีการทำงาน: มันยังคงใช้
JOINเพื่อดึงข้อมูลทั้งหมดในคิวรีเดียว อย่างไรก็ตาม มันจะรันคิวรีสำหรับอ็อบเจกต์แม่ (รวมถึงLIMIT/OFFSET) ภายใน subquery ก่อน จากนั้นจึง join ตารางที่เกี่ยวข้องเข้ากับผลลัพธ์ของ subquery นั้น - วิธีใช้: ใช้ตัวเลือกคิวรี
subqueryload
from sqlalchemy.orm import subqueryload
# Get the first 5 authors and all their books
authors = session.query(Author).options(subqueryload(Author.books)).limit(5).all()
SQL ที่สร้างขึ้นจะซับซ้อนกว่า:
SELECT ...
FROM (SELECT authors.id AS authors_id, authors.name AS authors_name
FROM authors LIMIT 5) AS anon_1
LEFT OUTER JOIN books ON anon_1.authors_id = books.author_id
ข้อดีของ `subqueryload`:
- วิธีที่ถูกต้องในการ Join กับ LIMIT/OFFSET: มันใช้ limit กับอ็อบเจกต์แม่ได้อย่างถูกต้องก่อนที่จะ join ทำให้คุณได้ผลลัพธ์ที่คาดหวัง
- การเชื่อมต่อฐานข้อมูลเพียงครั้งเดียว: เช่นเดียวกับ `joinedload` มันดึงข้อมูลทั้งหมดในครั้งเดียว
ข้อเสียของ `subqueryload`:
- ความซับซ้อนของ SQL: SQL ที่สร้างขึ้นอาจซับซ้อน และประสิทธิภาพของมันอาจแตกต่างกันไปในแต่ละระบบฐานข้อมูล
- ยังคงมีปัญหาผลคูณคาร์ทีเซียน: มันยังคงประสบปัญหาข้อมูลซ้ำซ้อนเช่นเดียวกับ `joinedload`
ตารางเปรียบเทียบ: การเลือกกลยุทธ์ของคุณ
นี่คือตารางอ้างอิงฉบับย่อเพื่อช่วยคุณตัดสินใจว่าจะใช้กลยุทธ์การโหลดแบบใด
| กลยุทธ์ | วิธีการทำงาน | จำนวนคิวรี | เหมาะสำหรับ | ข้อควรระวัง |
|---|---|---|---|---|
lazy='select' (ค่าเริ่มต้น) |
ส่งคำสั่ง SELECT ใหม่เมื่อมีการเข้าถึงแอตทริบิวต์ครั้งแรก | 1 + N | การเข้าถึงข้อมูลที่เกี่ยวข้องสำหรับอ็อบเจกต์เดียว; เมื่อข้อมูลที่เกี่ยวข้องไม่ค่อยถูกใช้งาน | มีความเสี่ยงสูงที่จะเกิดปัญหา N+1 ในลูป |
joinedload |
ใช้ LEFT OUTER JOIN เพียงครั้งเดียวเพื่อดึงข้อมูลแม่และลูกพร้อมกัน | 1 | ความสัมพันธ์แบบ Many-to-one หรือ one-to-one เมื่อต้องการให้มีคิวรีเพียงครั้งเดียวเป็นสิ่งสำคัญที่สุด | ทำให้เกิดผลคูณคาร์ทีเซียนกับคอลเลกชันแบบ to-many; ทำให้ limit()/offset() ทำงานผิดพลาด |
selectinload |
ส่ง SELECT ครั้งที่สองพร้อมกับ IN clause สำหรับ ID ของแม่ทั้งหมด |
2+ | ตัวเลือกเริ่มต้นที่ดีที่สุดสำหรับคอลเลกชันแบบ one-to-many ทำงานได้อย่างสมบูรณ์แบบกับ limit()/offset() |
ต้องการการเชื่อมต่อฐานข้อมูลมากกว่าหนึ่งครั้ง |
subqueryload |
ครอบคิวรีของแม่ไว้ใน subquery จากนั้นจึง JOIN ตารางลูก | 1 | การใช้ limit() หรือ offset() กับคิวรีที่ต้องการ eager load คอลเลกชันผ่าน JOIN |
สร้าง SQL ที่ซับซ้อน; ยังคงมีปัญหาผลคูณคาร์ทีเซียน |
เทคนิคการโหลดขั้นสูง
นอกเหนือจากกลยุทธ์หลักแล้ว SQLAlchemy ยังมีการควบคุมการโหลดความสัมพันธ์ที่ละเอียดมากยิ่งขึ้น
ป้องกันการเกิด Lazy Loads โดยไม่ได้ตั้งใจด้วย raiseload
หนึ่งในรูปแบบการเขียนโปรแกรมเชิงป้องกันที่ดีที่สุดใน SQLAlchemy คือการใช้ raiseload กลยุทธ์นี้จะแทนที่ lazy loading ด้วยการโยน exception หากโค้ดของคุณพยายามเข้าถึงความสัมพันธ์ที่ไม่ได้ถูก eager-load อย่างชัดเจนในคิวรี SQLAlchemy จะโยน InvalidRequestError
from sqlalchemy.orm import raiseload
# Query for an author but explicitly forbid lazy-loading of their books
author = session.query(Author).options(raiseload(Author.books)).first()
# This line will now raise an exception, preventing a hidden N+1 query!
print(author.books)
สิ่งนี้มีประโยชน์อย่างยิ่งในระหว่างการพัฒนาและทดสอบ การตั้งค่าเริ่มต้นเป็น raiseload สำหรับความสัมพันธ์ที่สำคัญ จะบังคับให้นักพัฒนาต้องตระหนักถึงความต้องการในการโหลดข้อมูลของตน ซึ่งช่วยกำจัดความเป็นไปได้ที่ปัญหา N+1 จะหลุดรอดไปถึงเวอร์ชันโปรดักชันได้อย่างมีประสิทธิภาพ
การละเว้นความสัมพันธ์ด้วย noload
บางครั้ง คุณต้องการให้แน่ใจว่าความสัมพันธ์นั้นไม่เคยถูกโหลดเลย ตัวเลือก noload จะบอกให้ SQLAlchemy ปล่อยให้แอตทริบิวต์นั้นว่างไว้ (เช่น รายการว่างหรือ None) สิ่งนี้มีประโยชน์สำหรับการทำ data serialization (เช่น การแปลงเป็น JSON) ซึ่งคุณต้องการยกเว้นบางฟิลด์ออกจากผลลัพธ์โดยไม่ทำให้เกิดการคิวรีฐานข้อมูลใดๆ
การจัดการคอลเลกชันขนาดใหญ่ด้วย Dynamic Loading
จะเกิดอะไรขึ้นถ้านักเขียนคนหนึ่งเขียนหนังสือหลายพันเล่ม? การโหลดทั้งหมดลงในหน่วยความจำด้วย `selectinload` อาจไม่มีประสิทธิภาพ สำหรับกรณีเหล่านี้ SQLAlchemy มีกลยุทธ์การโหลดแบบ dynamic ซึ่งกำหนดค่าได้โดยตรงบนความสัมพันธ์
class Author(Base):
# ...
# Use lazy='dynamic' for very large collections
books = relationship("Book", back_populates="author", lazy='dynamic')
แทนที่จะส่งคืนเป็น list แอตทริบิวต์ที่มี lazy='dynamic' จะส่งคืนเป็นอ็อบเจกต์คิวรีแทน ซึ่งช่วยให้คุณสามารถทำการกรอง, จัดลำดับ, หรือแบ่งหน้า (pagination) เพิ่มเติมได้ก่อนที่ข้อมูลจะถูกโหลดจริงๆ
author = session.query(Author).first()
# author.books is now a query object, not a list
# No books have been loaded yet!
# Count the books without loading them
book_count = author.books.count()
# Get the first 10 books, ordered by title
first_ten_books = author.books.order_by(Book.title).limit(10).all()
คำแนะนำเชิงปฏิบัติและแนวทางปฏิบัติที่ดีที่สุด
- วัดผล อย่าเดา: กฎทองของการเพิ่มประสิทธิภาพคือการวัดผล ใช้แฟล็ก
echo=Trueของ engine ใน SQLAlchemy หรือเครื่องมือที่ซับซ้อนกว่าเช่น SQLAlchemy-Debugbar เพื่อตรวจสอบ SQL query ที่ถูกสร้างขึ้นจริง ระบุคอขวดก่อนที่คุณจะพยายามแก้ไข - ตั้งค่าเริ่มต้นอย่างปลอดภัย แก้ไขอย่างชัดเจน: รูปแบบที่ยอดเยี่ยมคือการตั้งค่าเริ่มต้นเชิงป้องกันบนโมเดลของคุณ เช่น
lazy='raiseload'ซึ่งจะบังคับให้ทุกคิวรีต้องระบุอย่างชัดเจนว่าต้องการอะไร จากนั้นในแต่ละฟังก์ชัน repository หรือเมธอดใน service layer ให้ใช้query.options()เพื่อระบุกลยุทธ์การโหลดที่แน่นอน (selectinload,joinedload, ฯลฯ) ที่จำเป็นสำหรับกรณีการใช้งานนั้นๆ - เชื่อมต่อการโหลดของคุณ: สำหรับความสัมพันธ์ที่ซ้อนกัน (เช่น การโหลด Author, Books ของพวกเขา, และ Reviews ของแต่ละ Book) คุณสามารถเชื่อมต่อตัวเลือก loader ของคุณได้:
options(selectinload(Author.books).selectinload(Book.reviews)) - รู้จักข้อมูลของคุณ: ตัวเลือกที่ถูกต้องขึ้นอยู่กับรูปแบบข้อมูลและรูปแบบการเข้าถึงของแอปพลิเคชันของคุณเสมอ มันเป็นความสัมพันธ์แบบ one-to-one หรือ one-to-many? คอลเลกชันมีขนาดเล็กหรือใหญ่? คุณต้องการข้อมูลนั้นเสมอหรือไม่ หรือแค่บางครั้ง? การตอบคำถามเหล่านี้จะนำคุณไปสู่กลยุทธ์ที่เหมาะสมที่สุด
สรุป: จากมือใหม่สู่มือโปรด้านประสิทธิภาพ
การทำความเข้าใจกลยุทธ์การโหลดความสัมพันธ์ของ SQLAlchemy เป็นทักษะพื้นฐานสำหรับนักพัฒนาทุกคนที่สร้างแอปพลิเคชันที่แข็งแกร่งและขยายขนาดได้ เราได้เดินทางจากค่าเริ่มต้น lazy='select' และกับดักประสิทธิภาพ N+1 ที่ซ่อนอยู่ ไปสู่การควบคุมที่ทรงพลังและชัดเจนที่นำเสนอโดยกลยุทธ์ eager loading เช่น selectinload และ joinedload
ประเด็นสำคัญคือ: จงมีเจตนาที่ชัดเจน อย่าพึ่งพาพฤติกรรมเริ่มต้นเมื่อประสิทธิภาพเป็นเรื่องสำคัญ ทำความเข้าใจว่าแอปพลิเคชันของคุณต้องการข้อมูลอะไรสำหรับงานที่กำหนด และเขียนคิวรีของคุณเพื่อดึงข้อมูลนั้นอย่างแม่นยำด้วยวิธีที่มีประสิทธิภาพที่สุดเท่าที่จะเป็นไปได้ การเชี่ยวชาญกลยุทธ์การโหลดเหล่านี้จะทำให้คุณก้าวไปไกลกว่าแค่การทำให้ ORM ทำงานได้ แต่เป็นการทำให้มันทำงานเพื่อคุณ สร้างแอปพลิเคชันที่ไม่เพียงแต่ใช้งานได้ แต่ยังรวดเร็วและมีประสิทธิภาพเป็นพิเศษอีกด้วย