我在CitizenApp生產環境里,被一個性能問題耗了整整一周。白天查監控,晚上讀慢查詢日志,直到某個凌晨三點,終端里跳出一行SQL執行計劃,我才突然意識到——錯的不是我的查詢策略,而是我在所有關系加載上都用了同一套寫法。之前我盲目地在每個端點里塞joinedload,腦子里就一個念頭:“預加載總比懶加載快”。結果現實狠狠給了我一課。
這里有個扎心的真相:在SQLAlchemy里根本不存在一種“通吃”的關系加載策略。“最佳實踐”完全取決于你的并發模型、租戶數量,以及當前是在服務單個用戶還是50個并發請求。這篇文章就把我用debug時間換來的教訓拆開了揉碎了講給你聽,如果你恰好也在用FastAPI搭多租戶服務,大概率能幫你少踩一個深坑。
![]()
先說一個所有人都知道、但依舊會翻車的經典問題——N+1查詢。假設你的數據模型長成這樣,典型的多租戶結構:一個租戶下有多家機構,一家機構下又有多個用戶。
class Tenant(Base):__tablename__ = "tenants"id = Column(Integer, primary_key=True)name = Column(String)class Organization(Base):__tablename__ = "organizations"id = Column(Integer, primary_key=True)tenant_id = Column(Integer, ForeignKey("tenants.id"))name = Column(String)tenant = relationship("Tenant")class User(Base):__tablename__ = "users_table"id = Column(Integer, primary_key=True)organization_id = Column(Integer, ForeignKey("organizations.id"))name = Column(String)organization = relationship("Organization")
然后你寫了一個看起來人畜無害的接口:
@app.get("/all-users")async def list_users(session: Session):user_records = session.query(User).all() # 這里只跑1條SQLreturn ["id": u.id,"name": u.name,"org_name": u.organization.name # 每次循環觸發1條新查詢for u in user_records]
是的,這就是教科書級的N+1。第一條查詢撈出了所有用戶記錄,緊接著在序列化每個用戶時,為了拿organization.name,又在循環里單獨發出一條sql。假設你有100個用戶,這個接口總共會產生1+100=101條查詢。這一點大部分人都有所耳聞,甚至還會在心里默默記上一筆:“要用joinedload啊”。但真正讓我栽跟頭的,正是這個“標準答案”。
我的第一反應確實就是加上joinedload,順手得就像條件反射:
from sqlalchemy.orm import joinedload@app.get("/all-users")async def list_users(session: Session):user_records = session.query(User).options(joinedload(User.organization)).all()return [...]
在只有5個用戶、單請求壓測的時候,這個修改堪稱完美。查詢數從101暴跌到1,響應時間賞心悅目。
可一旦把它推到50個并發請求、每個請求還各自面對100個用戶的真實負載下,畫風就完全變了。這時你的數據庫里執行的已經不是幾條干凈利落的小查詢,而是一個瘋狂膨脹的左連接怪物。那條簡單的joinedload會一次性把User和Organization的所有字段拉出來,笛卡爾積之下臨時表瞬間膨脹。10個組織各擁100個用戶,結果集就是1000行,每行都拖著完整的組織信息。如果再加一重joinedload,數據量會指數級炸開。
更要命的是,在多租戶隔離場景里,joinedload會直接把租戶字段也預加載進來,如果你不小心把Tenant也急加載,一個請求就能把幾百張表的數據攪在一起。CPU飆升、內存打滿、查詢響應從200ms飆到幾秒,完全不是嚇唬人。
后來我找到的解法其實并不復雜:對單條記錄、確定性路徑,selectinload是更好的朋友;對列表接口,如果沒有分頁,堅決不用joinedload;更細致一點,在特定端點里甚至要回歸懶加載,配合緩存和對租戶的過濾。關鍵就一句話——在加載策略上,不存在一勞永逸。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.