Django ORM: tối ưu truy vấn để đạt hiệu năng tối đa

Hướng dẫn đầy đủ về tối ưu truy vấn Django ORM. select_related, prefetch_related, chỉ mục, phân tích vấn đề N+1 và các kỹ thuật nâng cao cho ứng dụng hiệu năng cao.

Tối ưu truy vấn Django ORM cho hiệu năng tối đa

Django ORM cung cấp một lớp trừu tượng tinh tế cho việc tương tác với cơ sở dữ liệu, nhưng sự đơn giản đó có thể che giấu những vấn đề hiệu năng nghiêm trọng. Một ứng dụng Django thiếu tối ưu có thể tạo ra hàng trăm truy vấn trong khi chỉ một truy vấn là đủ. Bài viết này trình bày các kỹ thuật cần thiết để nhận diện và xử lý những vấn đề đó.

Quy tắc vàng khi tối ưu

Trước khi tối ưu, cần đo lường. Việc dùng django-debug-toolbar trong môi trường phát triển cho phép quan sát từng truy vấn SQL được tạo ra và nhanh chóng nhận diện nút thắt cổ chai.

Hiểu rõ vấn đề N+1

Vấn đề N+1 là cái bẫy phổ biến nhất khi làm việc với ORM. Nó xảy ra khi truy vấn đầu tiên lấy về N đối tượng, sau đó N truy vấn bổ sung được thực thi để truy cập quan hệ của từng đối tượng. Sự nhân lên của truy vấn này khiến hiệu năng giảm sút đáng kể.

python
# models.py
from django.db import models

class Author(models.Model):
    """Mô hình đại diện cho tác giả của một quyển sách."""
    name = models.CharField(max_length=200)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    """Mô hình đại diện cho quyển sách kèm tác giả."""
    title = models.CharField(max_length=300)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)

    def __str__(self):
        return self.title

Những mô hình đơn giản này giúp minh họa vấn đề N+1 một cách cụ thể.

python
# views.py - Ví dụ có vấn đề
def list_books_bad(request):
    """❌ View này tạo ra N+1 truy vấn."""
    books = Book.objects.all()  # 1 truy vấn cho danh sách sách

    for book in books:
        # Mỗi lần truy cập book.author kích hoạt một truy vấn SQL
        print(f"{book.title} bởi {book.author.name}")

    # Với 100 cuốn sách = 101 truy vấn SQL!
    return render(request, 'books/list.html', {'books': books})

Đoạn mã trông vô hại nhưng lại tạo ra một truy vấn cho mỗi cuốn sách để lấy tác giả.

Phương thức select_related thực hiện một SQL JOIN và lấy dữ liệu liên quan trong duy nhất một truy vấn. Nó hoạt động cho các quan hệ ForeignKey và OneToOneField.

python
# views.py - Giải pháp tối ưu với select_related
def list_books_optimized(request):
    """✅ View này tạo ra duy nhất một truy vấn có JOIN."""
    # select_related thực hiện một SQL JOIN
    books = Book.objects.select_related('author').all()

    for book in books:
        # Không có truy vấn bổ sung: tác giả đã được nạp sẵn
        print(f"{book.title} bởi {book.author.name}")

    # Tổng cộng: 1 truy vấn SQL duy nhất bất kể số lượng sách
    return render(request, 'books/list.html', {'books': books})

Truy vấn SQL được sinh ra dùng LEFT OUTER JOIN để lấy tác giả cùng lúc với sách.

python
# Liên kết select_related cho quan hệ lồng nhau
# models.py
class Publisher(models.Model):
    name = models.CharField(max_length=200)
    country = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

# views.py
def list_books_with_details(request):
    """Lấy sách, tác giả và nhà xuất bản chỉ với một truy vấn."""
    books = Book.objects.select_related(
        'author',      # ForeignKey tới Author
        'publisher'    # ForeignKey tới Publisher
    ).all()

    return render(request, 'books/list.html', {'books': books})

Nhiều quan hệ có thể được tối ưu cùng lúc bằng cách liệt kê chúng trong select_related.

Đối với các quan hệ ManyToMany hoặc quan hệ ngược (ForeignKey nhìn từ phía bên kia), prefetch_related thực hiện các truy vấn tách rời nhưng được tối ưu, tránh JOIN khổng lồ.

python
# models.py
class Tag(models.Model):
    """Thẻ dùng để phân loại sách."""
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, related_name='books')

Quan hệ ManyToMany giữa Book và Tag yêu cầu prefetch_related để tối ưu hiệu quả.

python
# views.py - Tối ưu ManyToMany
def list_books_with_tags(request):
    """✅ Lấy sách và thẻ của chúng một cách hiệu quả."""
    books = Book.objects.prefetch_related('tags').all()

    for book in books:
        # Thẻ đã được nạp sẵn, không có truy vấn bổ sung
        tag_names = [tag.name for tag in book.tags.all()]
        print(f"{book.title}: {', '.join(tag_names)}")

    # Tổng cộng: 2 truy vấn (sách + thẻ) bất kể khối lượng
    return render(request, 'books/list.html', {'books': books})

prefetch_related thực hiện một truy vấn riêng cho thẻ rồi ghép dữ liệu lại trong Python.

select_related vs prefetch_related

Nên dùng select_related cho ForeignKey và OneToOne (SQL JOIN). Sử dụng prefetch_related cho ManyToMany và quan hệ ngược (truy vấn riêng). Cả hai có thể kết hợp trên cùng một QuerySet.

Tùy biến tải sẵn dữ liệu với đối tượng Prefetch

Đối tượng Prefetch cho phép kiểm soát chi tiết dữ liệu được tải sẵn: lọc, sắp xếp và thậm chí giới hạn số kết quả.

python
# views.py
from django.db.models import Prefetch

def list_authors_with_recent_books(request):
    """Lấy tác giả chỉ kèm theo các cuốn sách gần đây."""
    # Prefetch tùy biến: chỉ sách từ năm 2025
    recent_books_prefetch = Prefetch(
        'books',
        queryset=Book.objects.filter(
            published_date__year__gte=2025
        ).order_by('-published_date'),
        to_attr='recent_books'  # Lưu vào thuộc tính tùy biến
    )

    authors = Author.objects.prefetch_related(
        recent_books_prefetch
    ).all()

    for author in authors:
        # Truy cập qua thuộc tính tùy biến
        for book in author.recent_books:
            print(f"{author.name}: {book.title}")

    return render(request, 'authors/list.html', {'authors': authors})

Thuộc tính to_attr lưu kết quả vào một list Python thay vì manager thông thường.

python
# Kết hợp nâng cao: select_related + Prefetch
def list_authors_complete(request):
    """Ví dụ trọn vẹn về tối ưu nhiều cấp."""
    authors = Author.objects.prefetch_related(
        Prefetch(
            'books',
            queryset=Book.objects.select_related(
                'publisher'  # Tối ưu cả nhà xuất bản của từng cuốn
            ).prefetch_related(
                'tags'       # Và thẻ của từng cuốn
            ).filter(published_date__year=2026)
        )
    ).all()

    return render(request, 'authors/complete.html', {'authors': authors})

Phương pháp này giảm mạnh số lượng truy vấn cho các cấu trúc dữ liệu phức tạp.

Sẵn sàng chinh phục phỏng vấn Django?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Giới hạn cột với only() và defer()

Mặc định Django lấy toàn bộ cột của bảng. Với mô hình có nhiều trường hoặc trường lớn, việc giới hạn cột giúp tăng hiệu năng.

python
# views.py
def list_books_minimal(request):
    """Chỉ lấy những cột cần thiết."""
    # only() chỉ định các cột được lấy
    books = Book.objects.only(
        'id',
        'title',
        'published_date'
    ).select_related('author')

    # Lưu ý: truy cập trường không nằm trong only sẽ kích hoạt truy vấn
    for book in books:
        print(book.title)  # OK, đã có
        # print(book.isbn)  # Sẽ tạo thêm một truy vấn

    return render(request, 'books/list.html', {'books': books})

Phương thức only() tạo ra đối tượng "deferred" chỉ nạp những cột được chỉ định.

python
# defer() để loại bỏ cột cụ thể
def list_authors_without_bio(request):
    """Loại bỏ trường lớn ít khi sử dụng."""
    # defer() loại bỏ những cột được liệt kê
    authors = Author.objects.defer(
        'bio'  # TextField không được nạp
    ).all()

    for author in authors:
        print(author.name)   # OK
        print(author.email)  # OK
        # author.bio sẽ được nạp khi cần

    return render(request, 'authors/list.html', {'authors': authors})

defer() là nghịch đảo của only(): các cột liệt kê không được nạp ngay từ đầu.

Tối ưu với values() và values_list()

Khi chỉ cần một số giá trị mà không cần đối tượng mô hình đầy đủ, values()values_list() trả về dictionary hoặc tuple nhẹ hơn.

python
# views.py
def get_book_titles(request):
    """Chỉ lấy tiêu đề dưới dạng danh sách."""
    # values_list trả về tuple
    titles = Book.objects.values_list('title', flat=True)
    # Kết quả: ['Sách 1', 'Sách 2', ...]

    # values trả về dictionary
    book_data = Book.objects.values('title', 'published_date')
    # Kết quả: [{'title': 'Sách 1', 'published_date': ...}, ...]

    return render(request, 'books/titles.html', {'titles': titles})

Các phương thức này tránh khởi tạo đối tượng mô hình, qua đó giảm tiêu thụ bộ nhớ.

python
# Kết hợp với phép tổng hợp
from django.db.models import Count, Avg

def get_author_statistics(request):
    """Thống kê theo tác giả mà không nạp đối tượng."""
    stats = Author.objects.values('name').annotate(
        book_count=Count('books'),
        avg_year=Avg('books__published_date__year')
    ).order_by('-book_count')

    # Kết quả: [{'name': 'Tác giả', 'book_count': 5, 'avg_year': 2024}, ...]
    return render(request, 'authors/stats.html', {'stats': stats})

Annotation cho phép thực hiện tính toán ngay trong cơ sở dữ liệu.

Tạo chỉ mục để tăng tốc truy vấn

Chỉ mục cơ sở dữ liệu giúp tìm kiếm nhanh hơn rất nhiều. Django cho phép định nghĩa chỉ mục trực tiếp trong mô hình.

python
# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=300, db_index=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)
    status = models.CharField(max_length=20, default='available')

    class Meta:
        # Chỉ mục ghép cho các truy vấn thường gặp
        indexes = [
            # Chỉ mục theo ngày xuất bản (sắp xếp thường xuyên)
            models.Index(
                fields=['published_date'],
                name='book_pub_date_idx'
            ),
            # Chỉ mục ghép cho lọc theo tác giả + trạng thái
            models.Index(
                fields=['author', 'status'],
                name='book_author_status_idx'
            ),
            # Chỉ mục một phần: chỉ những cuốn còn sẵn
            models.Index(
                fields=['published_date'],
                name='book_available_idx',
                condition=models.Q(status='available')
            ),
        ]
        # Sắp xếp mặc định tận dụng chỉ mục
        ordering = ['-published_date']

Những chỉ mục này nâng cao hiệu năng cho truy vấn lọc theo các cột tương ứng.

Chỉ mục và thao tác ghi

Chỉ mục giúp đọc nhanh hơn nhưng làm chậm chút ít các thao tác ghi (INSERT, UPDATE). Chỉ tạo chỉ mục trên các cột thường được dùng trong WHERE, ORDER BY hoặc JOIN.

Sử dụng truy vấn SQL thô khi cần

Đối với truy vấn phức tạp hoặc tối ưu đặc thù cho cơ sở dữ liệu, truy vấn SQL thô đem lại quyền kiểm soát hoàn toàn.

python
# views.py
from django.db import connection

def get_books_with_raw_sql(request):
    """Truy vấn thô cho trường hợp đặc biệt."""
    # Cách 1: raw() để lấy đối tượng mô hình
    books = Book.objects.raw('''
        SELECT b.*, a.name as author_name
        FROM library_book b
        INNER JOIN library_author a ON b.author_id = a.id
        WHERE b.published_date > %s
        ORDER BY b.published_date DESC
    ''', ['2025-01-01'])

    return render(request, 'books/list.html', {'books': books})

def execute_custom_query(request):
    """Thực thi trực tiếp cho truy vấn không phải SELECT."""
    with connection.cursor() as cursor:
        # Truy vấn với phép tổng hợp phức tạp
        cursor.execute('''
            SELECT
                a.name,
                COUNT(b.id) as book_count,
                AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
            FROM library_author a
            LEFT JOIN library_book b ON b.author_id = a.id
            GROUP BY a.id, a.name
            HAVING COUNT(b.id) > 2
            ORDER BY book_count DESC
        ''')
        results = cursor.fetchall()

    return render(request, 'stats.html', {'results': results})

Truy vấn thô bỏ qua ORM nhưng đánh đổi tính khả chuyển giữa các cơ sở dữ liệu.

Phân tích truy vấn với django-debug-toolbar

Công cụ django-debug-toolbar cho phép xem mọi truy vấn SQL được sinh ra bởi một view Django.

python
# settings.py - Cấu hình debug toolbar
INSTALLED_APPS = [
    # ... các ứng dụng khác
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ... các middleware khác
]

# Hiển thị toolbar cho yêu cầu cục bộ
INTERNAL_IPS = ['127.0.0.1']

DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.sql.SQLPanel',      # Truy vấn SQL
    'debug_toolbar.panels.timer.TimerPanel',  # Thời gian thực thi
    'debug_toolbar.panels.cache.CachePanel',  # Cache
]

Cấu hình này kích hoạt các panel hữu ích nhất cho việc tối ưu.

python
# Ghi log truy vấn SQL trong môi trường phát triển
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

Log này hiển thị từng truy vấn SQL trên console, hữu ích cho việc phát hiện vấn đề N+1.

Cache QuerySet

Với dữ liệu được truy cập thường xuyên nhưng ít thay đổi, cache giúp tránh lặp lại truy vấn.

python
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page

def list_featured_books(request):
    """Lấy sách nổi bật với cache."""
    cache_key = 'featured_books_list'

    # Thử lấy từ cache
    books = cache.get(cache_key)

    if books is None:
        # Cache miss: thực thi truy vấn
        books = list(
            Book.objects.select_related('author')
            .filter(featured=True)
            .order_by('-published_date')[:10]
        )
        # Lưu cache trong 5 phút
        cache.set(cache_key, books, timeout=300)

    return render(request, 'books/featured.html', {'books': books})

# Decorator cache cho toàn bộ view
@cache_page(60 * 15)  # Cache 15 phút
def list_all_tags(request):
    """Liệt kê toàn bộ thẻ (dữ liệu ít thay đổi)."""
    tags = Tag.objects.annotate(
        book_count=Count('books')
    ).order_by('-book_count')

    return render(request, 'tags/list.html', {'tags': tags})

Cache giảm tải cho cơ sở dữ liệu với những dữ liệu tĩnh.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Kết luận

Việc tối ưu truy vấn Django ORM dựa trên một số nguyên tắc cốt lõi:

Đo lường trước khi tối ưu: dùng django-debug-toolbar để phát hiện vấn đề thực sự

Loại bỏ vấn đề N+1: select_related cho ForeignKey, prefetch_related cho ManyToMany

Giới hạn dữ liệu: only(), defer(), values() chỉ nạp những gì cần

Đánh chỉ mục thông minh: tạo chỉ mục cho cột thường lọc hoặc sắp xếp

Cache có chiến lược: dữ liệu ít thay đổi sẽ được hưởng lợi từ cache của Django

Truy vấn thô: chỉ là phương án cuối cho tối ưu đặc thù theo cơ sở dữ liệu

Khi áp dụng có phương pháp, những kỹ thuật này biến một ứng dụng Django chậm thành hệ thống hiệu quả, có khả năng xử lý lượng dữ liệu lớn. Django ORM vẫn là công cụ mạnh mẽ một khi các tinh tế của nó được làm chủ.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#django
#django orm
#python
#tối ưu cơ sở dữ liệu
#hiệu năng

Chia sẻ

Bài viết liên quan