Endpoint daftar buku di aplikasi kamu dipanggil 8 ribu kali sehari dan tiap panggilan makan 1,8 detik. Server belum penuh, cache juga sudah terpasang. Waktu saya buka kode ORM-nya, polanya selalu sama: sebuah for yang menjalankan query di dalamnya. Itulah N+1 problem, dan biasanya perbaikannya cuma satu baris.

Akar Masalahnya: N+1 Query

N+1 artinya Django menjalankan satu query untuk mengambil data utama, lalu satu query tambahan untuk tiap baris hasilnya. 200 baris berarti 201 query. Yang harusnya selesai dengan satu JOIN malah dipecah jadi ratusan round-trip ke database.

# myapp/views.py — pola yang bikin lambat
authors = Author.objects.all()          # 1 query

for author in authors:
    print(author.publisher.name)        # 1 query per author

Tiga baris itu kelihatan polos. Padahal kalau tabel author isinya 500 baris, Django menembakkan 501 query ke database. Saya pernah lihat satu halaman admin diam-diam menjalankan 1.200 query hanya untuk merender sebuah tabel.

Siapkan Model Contoh

Supaya contohnya konkret, pakai model sederhana seperti ini. Kalau kamu belum pernah bikin project Django sama sekali, mulai dari tutorial Django pertama dulu.

python
# myapp/models.py
from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=100)

class Author(models.Model):
    name = models.CharField(max_length=100)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

class Genre(models.Model):
    name = models.CharField(max_length=50)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    genres = models.ManyToManyField(Genre)

select_related menyuruh Django membuat JOIN di level SQL, jadi data Publisher ikut terambil dalam satu query yang sama dengan Author. Tidak ada query susulan di dalam loop.

python
authors = Author.objects.select_related("publisher")

for author in authors:
    print(author.publisher.name)   # tidak ada query tambahan

SQL yang dihasilkan kira-kira begini:

sql
SELECT author.id, author.name, author.publisher_id,
       publisher.id, publisher.name
FROM author
INNER JOIN publisher ON publisher.id = author.publisher_id;

select_related hanya berlaku untuk ForeignKey dan OneToOneField (relasi maju). Untuk reverse ForeignKey atau ManyToMany, pakai prefetch_related.

Kalau kamu butuh data dari sisi sebaliknya — misal semua buku milik seorang author — select_related tidak bisa dipakai. Relasi one-to-many tidak bisa di-JOIN tanpa menggandakan baris. Di sinilah prefetch_related masuk.

python
authors = Author.objects.prefetch_related("book_set")

for author in authors:
    print([book.title for book in author.book_set.all()])

prefetch_related menjalankan dua query: satu untuk Author, satu lagi untuk semua Book sekaligus. Django lalu mencocokkan hasilnya di Python dan menaruhnya di cache per objek. Loop di atas tetap dua query, mau author-nya 5 atau 5.000.

Bisa juga dibuat bertingkat, atau dibatasi isinya lewat objek Prefetch:

python
from django.db.models import Prefetch

django_books = Book.objects.filter(title__icontains="django")

authors = Author.objects.prefetch_related(
    Prefetch("book_set", queryset=django_books, to_attr="django_books"),
    "book_set__genres",
)

for author in authors:
    print(author.django_books)   # hanya buku berjudul "django"

Ambil Kolom Secukupnya: only() dan defer()

Kadang tabelnya lebar: kolom bio berisi teks panjang yang tidak kamu pakai di halaman list. Membawa kolom itu ke memori setiap request cuma buang-buang. only() membatasi kolom yang di-SELECT, sementara defer() kebalikannya — mengecualikan kolom tertentu.

python
# hanya ambil kolom yang dipakai
Author.objects.only("name")             # SELECT id, name

# ambil semua kecuali kolom yang berat
Author.objects.defer("bio")             # semua kolom minus bio

Hati-hati: kolom yang tidak di-only() tetap bisa diakses, tapi Django akan menjalankan query tambahan diam-diam untuk mengambilnya. Jangan pakai only() kalau kamu tidak yakin kolom apa saja yang dibutuhkan template.

values() dan values_list() untuk Data Mentah

Kalau yang kamu butuh cuma nama-nama author untuk dropdown, tidak perlu membuat objek model penuh. values_list() mengembalikan tuple atau list datar, jauh lebih ringan.

python
names = Author.objects.values_list("name", flat=True)
# ['Rina', 'Budi', 'Sari', ...]

pairs = Author.objects.values("name", "publisher__name")
# [{'name': 'Rina', 'publisher__name': 'Gramedia'}, ...]

count() dan exists() Alih-alih len()

len(queryset) terlihat wajar, tapi ia memaksa Django mengevaluasi seluruh queryset dan memuat semua baris ke memori hanya untuk menghitung jumlahnya. count() menerjemahkannya jadi SELECT COUNT(*) di database. Mirip dengan itu, exists() cukup untuk sekadar tahu ada atau tidak.

python
# lambat — memuat semua baris ke memori
if len(Book.objects.filter(author=author)) > 0:
    ...

# cepat — cuma SELECT COUNT(*)
if Book.objects.filter(author=author).exists():
    ...

total = Book.objects.count()   # bukan len(Book.objects.all())

Jangan optimasi sebelum kamu ukur. Kalau halaman cuma dipanggil tiga kali sehari, query 1,2 detik mungkin tidak masalah. Pasang alat ukurnya dulu, baru putuskan mana yang layak diperbaiki.

Deteksi Query Boros: debug-toolbar dan explain()

Dua alat ini yang saya pakai untuk menemukan query boros. django-debug-toolbar menampilkan daftar semua query beserta durasinya di tiap halaman. Untuk satu queryset yang mencurigakan, explain() menampilkan rencana eksekusi database — kamu bisa lihat apakah JOIN dan index benar-benar dipakai.

python
# lihat rencana eksekusi
print(Author.objects.select_related("publisher").explain())

# hitung query yang berjalan dalam satu blok
from django.db import connection, reset_queries
reset_queries()
authors = list(Author.objects.select_related("publisher"))
print(len(connection.queries))   # 1

Mulai dari endpoint yang paling sering dipanggil, bukan yang paling jelek. Buka debug-toolbar, cari query yang jumlahnya mencurigakan, lalu perbaiki dengan select_related, prefetch_related, atau count(). Kalau database-nya PostgreSQL, cek juga index di tutorial PostgreSQL. Sebelum mengubah apa pun, pastikan kamu sudah bisa membaca angka performa lewat profiling Python — jangan menebak.