iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0

今天主要是想聊聊 N+1 problem,但因為和 ORM 有關係,雖然大家對 ORM 都很熟了,但我們還是可以來複習一下。

What is ORM

ORM(Object Relational Mapping) 中文WIKI 翻作物件關聯對映,但我覺得念起來很怪 XD。
講到 ORM ,後端的開發者們一定都不陌生,他帶給我們在開發上許多方便,它可以透過程式語言來執行 SQL 的指令,也可以透過程式語言裡面的物件來與資料庫交換資料 (CRUD),可以加快我們開發的速度

  • 優點
    • SQL 的指令通常較為複雜且不好維護,因此使用 ORM 可以增加程式碼的可讀性和可維護性。
    • 讓開發者可以更簡單快速的使用物件與 DB 互動。
    • 因為到了資料庫更上一層去做執行指令轉換的動作,因此就算今天需要從 Postgresql 轉換成其他種資料庫,ORM 語法也不用像 SQL 語法可能需要改變,不需要因為換了 DB ,語法就不能用了。
    • 提高安全性,避免 SQL Injection,但也不是說就完全沒問題了, ORM injection 也是存在的,只是這裡就不多詳述了。
  • 缺點
    • 因為多了一層轉換,速度肯定沒有原生 SQL 來的快,處理複雜的指令效能會降低。
    • 占用更多的記憶體和資源資源
    • 學習成本較高。每個程式語言使用的 ORM 語法差別會很大,如果換了一個就需要重新學習。

我們以 Python 的 SQLAlchemy 來舉例

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

# 定義 Model
class Order(Base):
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    product_name = Column(String)
    price = Column(Float)

# 建立 PostgreSQL 連接 URL 
DATABASE_URL = 'postgresql+psycopg2://username:password@localhost:5432/mydatabase'

# 建立 PostgreSQL 資料庫連接引擎
engine = create_engine(DATABASE_URL)

Base.metadata.create_all(engine)

# 建立 Session
Session = sessionmaker(bind=engine)
session = Session()

# 使用 ORM 新增一筆訂單
new_order = User(product_name="TV", price=10000)
session.add(new_order)
session.commit()

# 查詢全部訂單
orders = session.query(Order).all()

我們可以看到從建立連線到執行新增和查詢都可以用 python 的物件來完成。
它們替代的就是以下的 SQL 語法。

#新增一筆訂單
INSERT INTO orders (product_name, price) VALUES ('TV', 10000);

# 查詢訂單
SELECT * FROM orders;

**N + 1 problem **

在使用 ORM 時,可能常常會出現一個效能問題,也就是 **N + 1 problem **
這個問題通常來自於 ORM 的不當寫法所造成的。

  • e.g.
    • 以一個簡單的 SQLAlchemy 例子來說,我們有一個 Order 資料表,儲存了客戶的訂單資料,還有一個 Product 資料表,儲存產品的詳細資訊。我們希望查詢每個訂單中產品的發售日期,若我們以以下的方式寫 ORM:
orders = db.query(Order).all()
for order in orders:
    product = db.query(Product).filter(Product.name == user.product_name).all()

在上面的 ORM 中,我們會先執行

  • (1次查詢) 對 Order 資料表進行一次查詢,取得所有訂單資料。
  • (N次查詢) 取得訂單資料後,對每個 Order 的產品都進行一次查詢,這時候會對 Product 資料表進行 N 次查詢。

這樣每次執行都會是 N+1 次查詢,在次數多了之後,就會產生效能的影響。

  • 解決方法:
    1. 使用 join 來做即可避免多一次的查詢。
      products = db.query(Order).join(Product).all()
      
    2. pre-load
      預加載(pre-load)是另一種解決 N + 1 問題的方式。在 SQLAlchemy 中可以透過 options() 配合 joinedload()subqueryload() 等來預先載入相關的資料,減少額外的查詢次數。

舉個例子,對於 SQLAlchemy 來說,我們可以這樣寫:

orders = db.query(Order).options(joinedload(Order.product)).all()

這樣可以在查詢 Order 時自動加載相關的 Product 資料,避免在遍歷 Order 時進行額外的查詢。
N+1 problem 通常在了解發生原因後,很快就能理解怎麼解了,算是一個簡單但可能被疏忽的問題!

Reference


上一篇
Day-18 | Database ACID transaction(2) feat. 髒讀、不可重複讀、幻讀& Postgresql
下一篇
Day-20 | Message Queue - MQTT
系列文
埋藏在後端工程下的地雷與寶藏30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言