Ачааллж байна...
Өмнөх хичээлийн Жишээ 2-т бид жижиг ажиглалт хийсэн: Батын гурван зээлийн номын нэрийг хэвлэх энгийн давталт сервер рүү таван query илгээсэн. Гурван зээлд энэ нь анзаарагдахгүй. Энэ хичээлд яг ижил кодыг мянган гишүүнтэй өгөгдөл дээр ажиллуулж, тэр жижиг ажиглалт хэрхэн мянга гаруй query болж хувирдгийг өөрийн нүдээр харна.
Энэ асуудлыг N+1 гэж нэрлэдэг: жагсаалтыг авах нэг query, дараа нь жагсаалтын N мөр бүрт нэг нэмэлт query. Энэ бол ORM ашигладаг програмуудын хамгийн түгээмэл гүйцэтгэлийн алдаа, мөн хамгийн далд нь — код алдаа өгдөггүй, үр дүн зөв, туршилтын жижиг өгөгдөл дээр хурдан. Програм бодит хэрэглэгчидтэй болж, хүснэгтүүд өсөхөд л хуудас удааширч эхэлдэг бөгөөд шалтгааныг кодоос харахад хэцүү, учир нь Python-д нэг ч query бичигдээгүй.
Энэ хичээлд бид гурван зүйл хийнэ. Нэгдүгээрт, асуудлыг хэмжинэ: query-ний тоог тоолох жижиг хэрэгсэл бичиж, тоо болон хугацааг бүртгэнэ. Хоёрдугаарт, SQLAlchemy-ийн eager loading — "урьдчилсан ачаалал" — хоёр аргаар засна: selectinload ба joinedload, тус бүрийн цаад SQL-ийг уншиж, хэзээ алийг нь сонгохыг ойлгоно. Гуравдугаарт, N+1-ийг програм бичих явцад эрт илрүүлэх аргуудыг сурна.
Өмнөх хичээлийн туслах ажилтныг санаарай: та зээлийн хуудас бариад "энэ хэний зээл вэ" гэж асуухад тэр гишүүдийн дэвтэр рүү гүйж очоод хариуг авчирдаг. Одоо дарга мянган гишүүний бүх зээлийн тайлан гаргахыг даалгалаа. Та гишүүдийн жагсаалтыг аваад, гишүүн бүрийн хувьд "энэ хүний зээлүүдийг авчир" гэж туслахыг явуулна — мянган удаа. Тэгээд зээл бүрийн хувьд "энэ номын нэр юу вэ" гэж дахин явуулна. Туслах өдөржин гүйнэ, та өдөржин хүлээнэ.
Ухаалаг арга: туслахад нэг удаа "энэ мянган хүний бүх зээлийг авчир" гэсэн жагсаалт өгөх. Тэр зээлийн дэвтэр рүү нэг удаа очоод бүгдийг авчирна. Дараа нь "эдгээр зээлүүдэд гарч байгаа бүх номын нэрийг авчир" гэж дахин нэг жагсаалт өгнө. Хоёр гүйлт, мянга гаруйн оронд. Ажлын хэмжээ ижил — ижил хуудаснууд хуулагдсан — гэвч замын зардал асар их буурсан. Сервер рүү илгээх query бүр яг ийм "гүйлт": сүлжээгээр явж, server-ийг хүлээж, буцаж ирэх тогтмол зардалтай.
N+1-ийг харахын тулд олон мөр хэрэгтэй. Бүлэг 9-ийн index хичээлд SQLite-д big.db үүсгэсэн шиг PostgreSQL-д туршилтын өгөгдлийн сан үүсгэе. library-ийг хөндөхгүйн тулд тусдаа n1_demo санг ашиглана. pg-practice хавтсанд n1_seed.sql (VS Code, UTF-8):
sql
CREATE TABLE members (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
city TEXT
);
CREATE TABLE books (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL,
year INTEGER,
copies INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE loans (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
book_id INTEGER NOT NULL REFERENCES books (id),
member_id INTEGER NOT NULL REFERENCES members (id),
loaned_on DATE NOT NULL,
due_on DATE NOT NULL,
returned_on DATE
);
INSERT INTO books (title, year, copies)
SELECT 'Ном ' || n, 2000 + n % 25, 1 + n % 5
FROM generate_series(1, 200) AS n;
INSERT INTO members (name, city)
SELECT 'Гишүүн ' || n, 'Улаанбаатар'
FROM generate_series(1, 1000) AS n;
INSERT INTO loans (book_id, member_id, loaned_on, due_on)
SELECT 1 + (m * 7 + k * 13) % 200, m, DATE '2026-09-01' + k, DATE '2026-09-15' + k
FROM generate_series(1, 1000) AS m, generate_series(1, 5) AS k;Хүснэгтүүд нь library-ийн members, books, loans-ийн хялбаршуулсан хувилбар. Сүүлийн гурван INSERT нь SELECT-ээс өгөгдөл авдаг — Бүлэг 3-ын INSERT-ийн шинэ хэлбэр. generate_series(1, 200) нь PostgreSQL-ийн функц бөгөөд 1-ээс 200 хүртэлх тоонуудыг мөр болгон үүсгэдэг. Эхний INSERT юу гэж хэлж байна вэ гэвэл: “1-ээс 200 хүртэлх тоо бүрт 'Ном 1', 'Ном 2' гэх мэт нэртэй ном нэм.” Сүүлийн INSERT нь FROM-д хоёр generate_series-ийг таслалаар бичсэн — Бүлэг 7-ийн ON-гүй JOIN, мөр бүрийг мөр бүртэй хослуулах. Энэ тохиолдолд санаатайгаар: 1000 гишүүн × 5 = 5000 зээл, гишүүн бүрт таван зээл. 1 + (m * 7 + k * 13) % 200 томъёо зээл бүрийг 200 номын аль нэгэнд тарааж оноодог. Үр дүн: 200 ном, 1000 гишүүн, 5000 зээл, бүх 200 ном зээлэгдсэн.
PowerShell-д (кирилл тохиргоотой):
powershell
psql -U postgres -c "CREATE DATABASE n1_demo"
psql -U postgres -d n1_demo -f n1_seed.sql
$env:DATABASE_URL = "postgresql+psycopg://postgres:PASSWORD@localhost:5432/n1_demo"Өмнөх хичээлийн library_models.py энэ санд шууд ажиллана — хүснэгт, баганын нэрс ижил.
Echo гаралт 1000 гаруй query-г хэвлэвэл уншихын аргагүй. Тиймээс SQLAlchemy-ийн event системээр тоологч бичье. query_counter.py:
python
from sqlalchemy import event
class QueryCounter:
def __init__(self, engine):
self.count = 0
event.listen(engine, "before_cursor_execute", self._on_query)
def _on_query(self, conn, cursor, statement, parameters, context, executemany):
self.count += 1Энэ код юу хийж байна вэ гэвэл: “engine сервер рүү SQL илгээх болгонд урьдаар count-ийг нэгээр нэмэгдүүл.” before_cursor_execute бол SQLAlchemy query бүрийг сервер рүү илгээхийн өмнөхөн дуудагддаг цэг — echo-гийн хэвлэдэг газар. Бид хэвлэхийн оронд тоолж байна.
report_naive.py:
python
import os
import time
from sqlmodel import Session, create_engine, select
from library_models import Member
from query_counter import QueryCounter
engine = create_engine(os.environ["DATABASE_URL"])
counter = QueryCounter(engine)
start = time.perf_counter()
lines = 0
with Session(engine) as session:
members = session.exec(select(Member).order_by(Member.id)).all()
for member in members:
for loan in member.loans:
title = loan.book.title
lines += 1
elapsed = time.perf_counter() - start
print(f"Мөр: {lines}, query: {counter.count}, хугацаа: {elapsed:.2f} сек")Энэ код юу хийж байна вэ гэвэл: "бүх гишүүнийг ав; гишүүн бүрийн зээл бүрийн номын нэрийг ав; хэдэн мөр, хэдэн query, хэдэн секунд болсныг хэвлэ." Энэ бол өмнөх хичээлийн Жишээ 2 — Батын оронд бүх гишүүн. Python-д бол хэн ч бичихэд байгалийн, цэвэрхэн код. Ажиллуулна:
Мөр: 5000, query: 1201, хугацаа: 0.94 секТаны хугацаа компьютераас хамааран өөр байх, харин query-ний тоо яг ижил байх ёстой. 1201 тоог задалъя — энэ бол хичээлийн гол санаа:
1 — бүх гишүүнийг авах SELECT ... FROM members ORDER BY members.id.
1000 — гишүүн бүрийн хувьд member.loans руу анх хандахад SELECT ... FROM loans WHERE ... = loans.member_id — өмнөх хичээлийн Жишээ 1-ийн query, мянган удаа.
200 — зээл бүрийн хувьд loan.book руу хандахад SELECT ... FROM books WHERE books.id = .... 5000 зээл байгаа ч зөвхөн 200 query явсан нь өмнөх хичээлийн identity map-ийн ачаар: ном бүр анх удаа л уншигдаж, дараагийн 24 удаа Session-ий бүртгэлээс авагдсан.
1 + 1000 + 200 = 1201. Нэр нь эндээс гаралтай: "жагсаалтад 1, мөр бүрт N." Бидний тохиолдолд хоёр давхар алхаж байгаа тул хоёр давхар N.
Нэг query-ийн "гүйлт" дунджаар 0.78 миллисекунд орчим — бага мэт. Гэвч бид өөрийн компьютер дээрх server-тэй ярьж байна. Бодит програмд өгөгдлийн сан ихэвчлэн өөр машин дээр — Бүлэг 10-ын эхний хичээлд дурдсан Neon шиг үүлэн үйлчилгээ — байж, query бүр сүлжээгээр хэдэн миллисекунд зарцуулдаг. Query бүрт 5 миллисекунд гэвэл 1201 query ойролцоогоор 6 секунд болно — нэг хуудас ачаалахад. Тэгээд хэрэглэгч олон болох тусам гишүүдийн тоо, тиймээс query-ний тоо өснө.
Мөрийг 1 гишүүнтэй болгож туршвал (select(Member).where(Member.id == 1)) query-ний тоо 7 гарна (1 + 1 + 5). Хөгжүүлэгч туршилтын жижиг өгөгдөл дээр хурдан хуудсыг хараад сэтгэл хангалуун байдаг — N+1 үргэлж бодит өгөгдөл дээр л илэрдэг шалтгаан энэ.
Жишээ 1: selectinload — "энэ жагсаалтын бүгдийг авчир"
Зүйрлэлийн ухаалаг аргыг SQLAlchemy-д хэлье. report_selectin.py — гэнэн хувилбараас зөвхөн query өөрчлөгдөнө:
python
from sqlalchemy.orm import selectinload
from library_models import Loan, Member
# ... engine, counter ижил ...
with Session(engine) as session:
statement = (
select(Member)
.order_by(Member.id)
.options(selectinload(Member.loans).selectinload(Loan.book))
)
members = session.exec(statement).all()
for member in members:
for loan in member.loans:
title = loan.book.title
lines += 1.options(selectinload(Member.loans).selectinload(Loan.book)) юу гэж хэлж байна вэ гэвэл: "гишүүдийг уншихдаа тэдний зээлүүдийг урьдчилан ачаал; тэр зээлүүдийн номуудыг ч урьдчилан ачаал." Давталт огт өөрчлөгдөөгүй — member.loans, loan.book хэвээр. Өөрчлөгдсөн нь зөвхөн хэзээ, хэрхэн уншигдах. Ажиллуулна:
Мөр: 5000, query: 4, хугацаа: 0.11 сек1201-ээс 4 болж, ойролцоогоор найм дахин хурдассан. Echo-г түр асааж (echo=True) query-нүүдийг харъя:
SELECT members.id, members.name, members.email, members.city
FROM members ORDER BY members.id
SELECT loans.member_id, loans.id, loans.book_id, loans.loaned_on, loans.due_on, loans.returned_on
FROM loans
WHERE loans.member_id IN (__[POSTCOMPILE_primary_keys])
[...] {'primary_keys': [1, 2, 3, 4, 5, ... ]}
SELECT loans.member_id, loans.id, ...
FROM loans
WHERE loans.member_id IN (__[POSTCOMPILE_primary_keys])
[...] {'primary_keys': [501, 502, 503, ... ]}
SELECT books.id, books.title, books.year, books.copies
FROM books
WHERE books.id IN (__[POSTCOMPILE_primary_keys])
[...] {'primary_keys': [8, 21, 34, ... ]}(товчилсон.) Хоёр дахь query юу гэж хэлж байна вэ гэвэл: “loans хүснэгтээс member_id нь энэ жагсаалтын аль нэг байгаа бүх зээлийг ав.” Энэ бол Бүлэг 2-ын IN — WHERE member_id IN (1, 2, 3, ...) — SQLAlchemy гишүүдийн id-уудыг цуглуулж нэг жагсаалт болгосон. __[POSTCOMPILE_primary_keys] нь "энд жагсаалтыг параметр болгон дэлгэнэ" гэсэн тэмдэглэгээ — утгууд бүгд параметр хэвээр, injection-оос хамгаалагдсан.
Зээлийн query яагаад хоёр удаа явсан бэ? selectinload IN жагсаалтыг анхдагчаар 500 утгаар хэсэглэдэг — нэг query-д хэт урт жагсаалт илгээхээс сэргийлэх зорилгоор. 1000 гишүүн → хоёр хэсэг. Сүүлийн query 200 номын id-аар нэг удаа явсан. Нийт: 1 + 2 + 1 = 4. Query-ний тоо одоо мөрийн тооноос биш, хүснэгтийн тооноос хамаарна — гишүүн 10 000 болвол 20 хэсэг, гэвч 10 001 биш.
Нэг ажиглалт: SQLAlchemy зээлүүдийг гишүүн тус бүрт хэрхэн хуваарилсан бэ? Хоёр дахь query-ний эхэнд loans.member_id баганыг тусгайлан авсныг анзаараарай — тэр утгаар мөр бүрийг харгалзах гишүүний loans жагсаалт руу Python-д хуваарилдаг. Хэрэв та Next.js + Drizzle дээр “id-уудыг цуглуулж inArray-аар нэг query, дараа нь Map-аар хуваарилах” загвар харсан бол — энэ яг тэр, SQLAlchemy автоматаар хийж байна.
Жишээ 2: joinedload — нэг том JOIN
Хоёр дахь арга бол Бүлэг 7-ийн JOIN. report_selectin.py-ийн .options(...)-ийг солино:
python
from sqlalchemy.orm import joinedload
# ...
.options(joinedload(Member.loans).joinedload(Loan.book))Ажиллуулахад алдаа:
sqlalchemy.exc.InvalidRequestError: The unique() method must be invoked on this Result, as it contains results that include joined eager loads against collectionsЭнэ мессеж юу гэж хэлж байна вэ гэвэл: "энэ үр дүн жагсаалт (collection)-ийн эсрэг JOIN-оор урьдчилан ачаалсан мөрүүд агуулж байгаа тул unique() аргыг дуудах шаардлагатай." Шалтгаан нь Бүлэг 7-ийн мөр үржих анхааруулга: гишүүдийг зээлүүдтэй нь JOIN хийхэд гишүүн бүр таван мөрөнд давтагдана. SQLAlchemy бидэнд "давхардсан гишүүдийг арилгах ёстойгоо мэдэж байгаа гэдгээ батал" гэж хүсэж байна. Засвар:
python
members = session.exec(statement).unique().all()Ажиллуулна:
Мөр: 5000, query: 1, хугацаа: 0.16 секНэг query. Echo-оос:
SELECT members.id, members.name, members.email, members.city,
books_1.id, books_1.title, books_1.year, books_1.copies,
loans_1.id, loans_1.book_id, loans_1.member_id, loans_1.loaned_on, loans_1.due_on, loans_1.returned_on
FROM members
LEFT OUTER JOIN loans AS loans_1 ON members.id = loans_1.member_id
LEFT OUTER JOIN books AS books_1 ON books_1.id = loans_1.book_id
ORDER BY members.idЭнэ query юу гэж хэлж байна вэ гэвэл: "гишүүн бүрийг зээлүүдтэй нь, зээл бүрийг номтой нь холбож, зээлгүй гишүүдийг ч хадгалан бүх баганыг ав." Бүлэг 7-ийн гурван хүснэгтийн LEFT JOIN — гинжин дүрэм ч хадгалагдсан (хоёр дахь JOIN ч LEFT), alias-уудтай. SQLAlchemy LEFT сонгосон нь зөв: зээлгүй гишүүн тайлангаас алга болох ёсгүй — Бүлэг 7-ийн LEFT JOIN хичээлийн гол санаа.
Нэг query байхад яагаад selectinload-оос удаан бэ? Сервер 5000 мөр буцаасан бөгөөд мөр бүрт гишүүний мэдээлэл (5 удаа давтагдсан) ба номын мэдээлэл (25 удаа давтагдсан) бий. Сүлжээгээр давхардсан өгөгдөл зөөгдөж, Python давхардлыг цэвэрлэх ажил хийсэн. selectinload тус бүрийг нэг л удаа авчирсан: 1000 гишүүн, 5000 зээл, 200 ном.
Жишээ 3: аль нь хэзээ
Хоёр аргын ялгаанаас практик дүрэм гарна, мөн SQLAlchemy-ийн баримт бичиг ч мөн адил зөвлөдөг:
Жагсаалт тал (нэг → олон, member.loans) — selectinload. JOIN нь "нэг" талын мөрийг "олон"-ий тоогоор үржүүлдэг; IN query тэгдэггүй.
Нэг объект тал (олон → нэг, loan.book) — joinedload ихэвчлэн тохиромжтой. Зээл бүр нэг л номтой тул JOIN мөрийг үржүүлэхгүй, зүгээр л багана нэмнэ; нэмэлт query шаардахгүй.
Хослуулж болно:
python
select(Member).options(selectinload(Member.loans).joinedload(Loan.book))"Гишүүдийн зээлүүдийг IN-ээр, тэдгээр зээлийн номыг тэр query-д JOIN-оор." Хоёр query (зээлийн хэсэглэлтэй гурав), давхардалгүй. Энэ хослолыг туршаад тоог нь бүртгээрэй — тэр бол энэ тайлангийн хувьд хамгийн тэнцвэртэй сонголт.
Мөн хамгийн чухал зөвлөгөө: урьдчилан ачаалал нь "хамгийн хурдан" сонголт гэсэн үг биш, “N+1 биш” гэсэн үг. Хэмжээгүй бол аль нь хурдан гэдгийг мэдэхгүй. Энэ хичээлийн тоологч тэр хэмжилтэд зориулагдсан.
Жишээ 4: зөвхөн тайлан хэрэгтэй бол — объектгүй query
Бидний тайлан гишүүний нэр, номын нэрээс өөр юу ч хэрэглэдэггүй. Тэгвэл бүтэн Member, Loan, Book объектууд хэрэгтэй юу? Бүлэг 7-ийн JOIN-ийг SQLModel-ийн хэлээр шууд бичье:
python
from library_models import Book, Loan, Member
with Session(engine) as session:
statement = (
select(Member.name, Book.title)
.join(Loan, Loan.member_id == Member.id)
.join(Book, Book.id == Loan.book_id)
.order_by(Member.id)
)
for name, title in session.exec(statement):
lines += 1Энэ query юу гэж хэлж байна вэ гэвэл: "гишүүдийг зээлтэй нь, зээлийг номтой нь холбож, зөвхөн гишүүний нэр ба номын нэрийг гишүүний id-ийн дарааллаар ав." Үр дүн:
Мөр: 5000, query: 1, хугацаа: 0.04 секХамгийн хурдан — нэг query, зөвхөн хоёр багана, объект үүсгэх ажилгүй. Үр дүн нь объект биш, (name, title) хос — member.loans гэх мэт алхах боломжгүй. Энэ нь INNER JOIN тул зээлгүй гишүүд тайланд орохгүйг санаарай (энэ өгөгдөлд тийм гишүүн байхгүй). Объектууд хэрэгтэй үед — засварлах, хадгалах, API-д бүтэн бүтэц буцаах — eager loading; зөвхөн уншиж харуулах тайланд — шууд багана сонгох. Энэ сэдвийг дараагийн хичээлд гүнзгий авч үзнэ.
Жишээ 5: N+1-ийг эрт илрүүлэх
N+1 алдаа өгдөггүй тул түүнийг идэвхтэй хайх хэрэгтэй. Гурван арга.
Echo-д давтагдах хэлбэр. Хөгжүүлэх үед echo=True-ээр нэг хуудсыг ачаалаад гаралтыг гүйлгэж харахад ижил SELECT өөр өөр параметрээр олон удаа давтагдвал — тэр бол N+1. Хэдэн арав давтагдсан мөр анзаарахад амархан.
Тоологч. Энэ хичээлийн QueryCounter-ийг endpoint эсвэл функцэд түр хавсаргаж, query-ний тоо өгөгдлийн хэмжээтэй хамт өсөж байгаа эсэхийг шалга. Тоо тогтмол байвал зүгээр; мөртэй хамт өсөж байвал N+1.
Lazy loading-ийг хориглох. Хамгийн хатуу арга: relationship-ийг залхуу ачаалал огт хийхгүй болгох. library_models.py-д:
python
loans: list["Loan"] = Relationship(
back_populates="member", sa_relationship_kwargs={"lazy": "raise"}
)sa_relationship_kwargs нь SQLModel-ээс цаадах SQLAlchemy-д нэмэлт тохиргоо дамжуулдаг. "lazy": "raise" юу гэж хэлж байна вэ гэвэл: "энэ relationship-ийг хэзээ ч залхуугаар бүү ачаал; оронд нь алдаа өг." Одоо гэнэн тайланг ажиллуулбал:
sqlalchemy.exc.InvalidRequestError: 'Member.loans' is not available due to lazy='raise'Энэ мессеж юу гэж хэлж байна вэ гэвэл: “lazy='raise' тохиргооны улмаас Member.loans боломжгүй.” N+1 чимээгүй удааширлаас чанга алдаа болж хувирсан — query бүрт урьдчилан ачааллыг ил тод бичихийг албаддаг. selectinload-тай хувилбар энэ тохиргоотой асуудалгүй ажиллана. Энэ нь хатуу дадал бөгөөд том төслүүдэд түгээмэл; туршсаны дараа тохиргоог устгаж болно.
Туршилтын санг дараагийн хичээлд дахин ашиглана — устгалгүй үлдээ.
Хэрэв сонирхож байвал Жишээ 3-ын хослол (selectinload + joinedload)-ыг ажиллуулж, query-ний тоо болон хугацааг бусад гурван аргатай нэг хүснэгтэд бичээрэй. Мөн n1_seed.sql-ийн гишүүдийн тоог 1000-аас 5000 болгож (бусад тоог мөн тохируулж) санг дахин үүсгээд, гэнэн хувилбар ба selectinload-ийн query-ний тоо тус бүр хэрхэн өөрчлөгдөхийг ажиллуулахаасаа өмнө таамаглаарай. Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээлүүд тэдгээрийг хийсэн гэж тооцохгүй.
Туршилтын жижиг өгөгдөл дээр л хэмжих. Таван мөр дээр N+1 нь 1201 биш, 7 query — хэн ч анзаарахгүй. Гүйцэтгэлийг бодит хэмжээтэй ойролцоо өгөгдөл дээр шалга; энэ хичээлийн generate_series арга түүнийг хэдхэн мөрөөр үүсгэдэг.
Collection-д joinedload хэрэглээд unique() мартах. Жишээ 2-ын The unique() method must be invoked алдаа. Шийдэл нь .unique() нэмэх, эсвэл жагсаалт талд selectinload ашиглах.
Урьдчилан ачааллыг зөвхөн нэг давхарт хийх. selectinload(Member.loans) гэж бичээд .selectinload(Loan.book)-ийг мартвал зээлүүд урьдчилан уншигдах ч ном бүр залхуугаар уншигдана — 1 + 2 + 200 = 203 query. N+1 багассан ч алга болоогүй. Давталт дотор хүрч байгаа relationship бүрийг options-д бичих ёстой; тоологч энэ дутуу давхрыг шууд харуулна.
Хэрэггүй зүйлийг урьдчилан ачаалах. Эсрэг алдаа: давталт loan.book-д огт хүрдэггүй байхад .selectinload(Loan.book) нэмэх нь хэрэггүй query болон санах ой зарцуулна. Урьдчилан ачааллыг кодын бодит хэрэгцээгээр тодорхойл.
Session хаагдсаны дараа хандахыг eager loading-ээр "засах" гэж ойлгох. Өмнөх хичээлийн DetachedInstanceError: урьдчилан ачаалсан relationship Session-ий гадна ч уншигдана, учир нь өгөгдөл аль хэдийн санах ойд байна. Энэ нь тустай, гэвч урьдчилан ачаалаагүй relationship-д алдаа хэвээр гарна.
Энэ хичээлд та N+1 асуудлыг бодит хэмжээгээр харж, хэмжиж, засварлалаа. Python-д цэвэрхэн харагдах хоёр давхар давталт мянган гишүүний өгөгдөл дээр 1201 query илгээсэн: жагсаалтад 1, гишүүн бүрийн зээлд 1000, номонд identity map-ийн ачаар 200. Query бүрийн тогтмол "замын зардал" тул хугацаа өгөгдлийн сангийн алслалттай хамт өснө, жижиг туршилтын өгөгдөл асуудлыг нуудаг. before_cursor_execute event-ээр query тоологч бичсэн. selectinload нь холбоотой мөрүүдийг WHERE ... IN (...) query-ээр, 500-аар хэсэглэн урьдчилан уншиж 1201-ийг 4 болгосон; joinedload нь LEFT JOIN-оор нэг query болгосон ч collection дээр мөр үржүүлж .unique() шаардсан. Дүрэм: жагсаалт талд selectinload, нэг объект талд joinedload, сонголтыг хэмжилтээр шийдэх. Зөвхөн харуулах тайланд шууд багана сонгосон JOIN хамгийн хурдан байсан. N+1-ийг echo-ийн давтагдах хэлбэр, тоологч, lazy="raise" гурваар эрт илрүүлнэ.
Дараагийн хичээл энэ бүлгийн, мөн сургалтын үзэл баримтлалын хамгийн чухал асуултад хариулна: ORM-оо хэзээ ашиглах, хэзээ орхиж цэвэр SQL бичих вэ? Жишээ 4-ийн объектгүй query-г цааш нь хөгжүүлж, Бүлэг 9-ийн тайлангийн query-нүүдийг SQLModel-оос text()-ээр, параметртэйгээр ажиллуулна — мөн ORM-ийн тав тух, цэвэр SQL-ийн хяналт хоёрыг нэг програмд зөв хослуулах дүрмүүдийг гаргана.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.