Django ORM 1 - Tối ưu hóa truy vấn bằng select_related và prefetch_related


Written on April 1, 2023

Trong quá trình phát triển ứng dụng web sử dụng Django ORM, tối ưu hóa truy vấn là một khía cạnh quan trọng để đảm bảo hiệu suất và tốc độ của ứng dụng, đồng thời tránh gặp phải vấn đề n+1 query. Trong Django ORM hỗ trợ hai công cụ để làm việc này là select_relatedprefetch_related

Chúng ta sẽ sử dụng những models này để làm ví dụ trong toàn bộ bài này

from datetime import date

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

    def __str__(self):
        return self.headline

Bạn muốn query thông tin của Entry ID = 5 và lấy Blog tương ứng. Phải cần tới 2 câu query để lấy đầy đủ thông tin.

# Hits the database.
e = Entry.objects.get(id=5)

# Hits the database again to get the related Blog object.
b = e.blog

Sau đây là cách sử dụng select_related chỉ cần dùng 1 câu query duy nhất để lấy thông tin của entry và blog tương ứng.

# Hits the database.
e = Entry.objects.select_related("blog").get(id=5)

# Doesn't hit the database, because e.blog has been prepopulated
# in the previous query.
b = e.blog

Một số ví dụ phức tạp hơn, để tránh n+1 query

from django.utils import timezone

# Find all the blogs with entries scheduled to be published in the future.
blogs = set()

for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog"):
    # Without select_related(), this would make a database query for each
    # loop iteration in order to fetch the related blog for each entry.
    blogs.add(e.blog)

Có thể lần theo khoá ngoại để tiếp tục load thông tin. Ví dụ

from django.db import models

class City(models.Model):
    # ...
    pass

class Person(models.Model):
    # ...
    hometown = models.ForeignKey(
        City,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

class Book(models.Model):
    # ...
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

Từ Book entity, có thể query tới Person, rồi tới City, bằng cách sử dụng ký tự __ . Trong ví dụ này, ta muốn thông tin về author và hometown của một Book và chỉ cần một câu query duy nhất.

# Hits the database with joins to the author and hometown tables.
b = Book.objects.select_related("author__hometown").get(id=4)
p = b.author  # Doesn't hit the database.
c = p.hometown  # Doesn't hit the database.

# Without select_related()...
b = Book.objects.get(id=4)  # Hits the database.
p = b.author  # Hits the database.
c = p.hometown  # Hits the database.

select_related có thể không có tham số, select_related()  bằng cách nay ta sẽ load tất cả các khoá ngoại không null của object. Cách này không khuyến nghị vì nó sẽ trả về nhiều data hơn cần thiết.

Để lấy nhiều khoá ngoại, có 2 cách: theo chuỗi hoặc thêm tham số khi gọi select_related. 2 cách sau là tương đương

select_related('foo', 'bar') 
select_related('foo').select_related('bar').

select_related hoạt động bằng cách tạo ra câu JOIN SQL và update SELECT fields trong cùng một câu query. Dùng trong trường hợp mapping 1-1 hoặc khóa ngoại.

prefetch_related thì ngược lại so với select_related, phù hợp cho môi quan hệ many-to-many và many-to-one và một số GenericRelation không thể sử dụng select_related

Để minh họa cho cách prefetch_related hoạt động, giả sử chúng ta có models sau:

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )

Pizza và Topping có mối quan hệ nhiều-nhiều, giả sự bây giờ chúng ta liệt kê tât cả Pizza bằng câu lệnh: Pizza.objects.all()

Cho mỗi item trong queryset Pizza.objects.all(), __str__ function sẽ query trong table topping để lấy thông tin. ==> n+1 query problem.

Chúng ta có thể giảm thiểu xuống còn 2 câu query bằng cách. Pizza.objects.prefetch_related('toppings')

Một câu query để lấy thông tin về Pizza và một câu để load thông tin về toppings liên quan.

Bây giờ, mỗi khi function self.toppings.all() được gọi, nói sẽ sử dụng data trong Queryset cache. Nghĩa là, tất cả các toppings liên quan sẽ được tìm nạp trong một truy vấn duy nhất và được sử dụng để tạo các Queryset có bộ nhớ đệm chứa sẵn các kết quả có liên quan; các Queryset này sau đó được sử dụng trong các lệnh gọi self.toppings.all().