Django ORM: optimasi query untuk performa maksimal
Panduan lengkap untuk mengoptimalkan query Django ORM. select_related, prefetch_related, indeks, analisis masalah N+1, dan teknik lanjutan untuk aplikasi berperforma tinggi.

Django ORM menyediakan abstraksi yang elegan untuk berinteraksi dengan basis data, tetapi kemudahan tersebut dapat menutupi masalah performa yang kritis. Aplikasi Django yang tidak dioptimalkan dapat menghasilkan ratusan query padahal satu query saja sudah cukup. Panduan ini membahas teknik-teknik penting untuk mengidentifikasi dan menyelesaikan masalah tersebut.
Sebelum mengoptimalkan, lakukan pengukuran terlebih dahulu. Penggunaan django-debug-toolbar di lingkungan pengembangan memungkinkan setiap query SQL yang dihasilkan terlihat dan titik bottleneck cepat ditemukan.
Memahami masalah N+1
Masalah N+1 merupakan jebakan paling umum saat bekerja dengan ORM. Ia muncul ketika query awal mengambil N objek, lalu N query tambahan dijalankan untuk mengakses relasi pada setiap objek. Perbanyakan query semacam ini menurunkan performa secara signifikan.
# models.py
from django.db import models
class Author(models.Model):
"""Model yang merepresentasikan penulis sebuah buku."""
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):
"""Model yang merepresentasikan buku beserta penulisnya."""
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.titleModel sederhana ini memudahkan ilustrasi masalah N+1 secara konkret.
# views.py - Contoh bermasalah
def list_books_bad(request):
"""❌ View ini menghasilkan N+1 query."""
books = Book.objects.all() # 1 query untuk buku
for book in books:
# Setiap akses ke book.author memicu query SQL
print(f"{book.title} oleh {book.author.name}")
# Dengan 100 buku = 101 query SQL!
return render(request, 'books/list.html', {'books': books})Kode tampak sederhana namun menghasilkan satu query per buku untuk mengambil penulisnya.
Mengatasi N+1 dengan select_related
Metode select_related melakukan SQL JOIN dan mengambil data terkait dalam satu query. Metode ini bekerja pada relasi ForeignKey dan OneToOneField.
# views.py - Solusi optimal dengan select_related
def list_books_optimized(request):
"""✅ View ini menghasilkan satu query dengan JOIN."""
# select_related menjalankan SQL JOIN
books = Book.objects.select_related('author').all()
for book in books:
# Tidak ada query tambahan: author sudah dimuat
print(f"{book.title} oleh {book.author.name}")
# Total: hanya 1 query SQL terlepas dari jumlah buku
return render(request, 'books/list.html', {'books': books})Query SQL yang dihasilkan menggunakan LEFT OUTER JOIN untuk mengambil penulis bersama buku.
# Merangkai select_related untuk relasi bersarang
# 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):
"""Mengambil buku, penulis, dan penerbit dalam satu query."""
books = Book.objects.select_related(
'author', # ForeignKey ke Author
'publisher' # ForeignKey ke Publisher
).all()
return render(request, 'books/list.html', {'books': books})Beberapa relasi dapat dioptimalkan sekaligus dengan menyebutkannya di dalam select_related.
Mengoptimalkan relasi ManyToMany dengan prefetch_related
Untuk relasi ManyToMany atau relasi terbalik (ForeignKey dilihat dari sisi sebaliknya), prefetch_related menjalankan query terpisah namun teroptimasi sehingga JOIN besar dapat dihindari.
# models.py
class Tag(models.Model):
"""Tag untuk mengategorikan buku."""
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')Relasi ManyToMany antara Book dan Tag membutuhkan prefetch_related agar optimasi efektif.
# views.py - Optimasi ManyToMany
def list_books_with_tags(request):
"""✅ Mengambil buku beserta tag-nya secara efisien."""
books = Book.objects.prefetch_related('tags').all()
for book in books:
# Tag sudah dimuat sebelumnya, tanpa query tambahan
tag_names = [tag.name for tag in book.tags.all()]
print(f"{book.title}: {', '.join(tag_names)}")
# Total: 2 query (buku + tag) terlepas dari jumlahnya
return render(request, 'books/list.html', {'books': books})prefetch_related menjalankan query terpisah untuk tag lalu menggabungkannya di sisi Python.
Gunakan select_related untuk ForeignKey dan OneToOne (SQL JOIN). Gunakan prefetch_related untuk ManyToMany dan relasi terbalik (query terpisah). Keduanya dapat dipadukan pada QuerySet yang sama.
Menyesuaikan pemuatan awal dengan objek Prefetch
Objek Prefetch memberikan kontrol detail atas data yang dimuat sebelumnya: filter, pengurutan, hingga membatasi jumlah hasil.
# views.py
from django.db.models import Prefetch
def list_authors_with_recent_books(request):
"""Mengambil penulis hanya dengan buku-buku terbarunya."""
# Prefetch khusus: hanya buku sejak 2025
recent_books_prefetch = Prefetch(
'books',
queryset=Book.objects.filter(
published_date__year__gte=2025
).order_by('-published_date'),
to_attr='recent_books' # Disimpan dalam atribut khusus
)
authors = Author.objects.prefetch_related(
recent_books_prefetch
).all()
for author in authors:
# Akses melalui atribut khusus
for book in author.recent_books:
print(f"{author.name}: {book.title}")
return render(request, 'authors/list.html', {'authors': authors})Atribut to_attr menyimpan hasil pada list Python alih-alih manager bawaan.
# Kombinasi lanjutan: select_related + Prefetch
def list_authors_complete(request):
"""Contoh lengkap optimasi multi-level."""
authors = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=Book.objects.select_related(
'publisher' # Mengoptimalkan penerbit setiap buku
).prefetch_related(
'tags' # Sekaligus tag setiap buku
).filter(published_date__year=2026)
)
).all()
return render(request, 'authors/complete.html', {'authors': authors})Pendekatan ini sangat memangkas jumlah query untuk struktur data yang kompleks.
Siap menguasai wawancara Django Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Membatasi kolom dengan only() dan defer()
Secara bawaan, Django mengambil seluruh kolom tabel. Pada model dengan banyak field atau field berukuran besar, membatasi kolom akan meningkatkan performa.
# views.py
def list_books_minimal(request):
"""Mengambil hanya kolom yang dibutuhkan."""
# only() menentukan kolom yang dimuat
books = Book.objects.only(
'id',
'title',
'published_date'
).select_related('author')
# Perhatian: akses ke field yang tidak disertakan akan memicu query
for book in books:
print(book.title) # OK, disertakan
# print(book.isbn) # Akan menimbulkan query tambahan
return render(request, 'books/list.html', {'books': books})Metode only() menghasilkan objek "deferred" yang hanya memuat kolom tertentu.
# defer() untuk mengecualikan kolom tertentu
def list_authors_without_bio(request):
"""Mengecualikan field besar yang jarang digunakan."""
# defer() mengecualikan kolom yang dipilih
authors = Author.objects.defer(
'bio' # TextField tidak dimuat
).all()
for author in authors:
print(author.name) # OK
print(author.email) # OK
# author.bio akan memuat field saat diakses
return render(request, 'authors/list.html', {'authors': authors})defer() adalah kebalikan dari only(): kolom yang disebut tidak dimuat di awal.
Mengoptimalkan dengan values() dan values_list()
Ketika hanya nilai tertentu yang dibutuhkan tanpa objek model lengkap, values() dan values_list() mengembalikan dictionary atau tuple yang lebih ringan.
# views.py
def get_book_titles(request):
"""Mengambil hanya judul sebagai daftar."""
# values_list mengembalikan tuple
titles = Book.objects.values_list('title', flat=True)
# Hasil: ['Buku 1', 'Buku 2', ...]
# values mengembalikan dictionary
book_data = Book.objects.values('title', 'published_date')
# Hasil: [{'title': 'Buku 1', 'published_date': ...}, ...]
return render(request, 'books/titles.html', {'titles': titles})Metode-metode ini menghindari pembuatan objek model dan mengurangi penggunaan memori.
# Kombinasi dengan agregasi
from django.db.models import Count, Avg
def get_author_statistics(request):
"""Statistik per penulis tanpa memuat objek."""
stats = Author.objects.values('name').annotate(
book_count=Count('books'),
avg_year=Avg('books__published_date__year')
).order_by('-book_count')
# Hasil: [{'name': 'Penulis', 'book_count': 5, 'avg_year': 2024}, ...]
return render(request, 'authors/stats.html', {'stats': stats})Anotasi memungkinkan perhitungan dilakukan langsung di basis data.
Membuat indeks untuk mempercepat query
Indeks basis data sangat mempercepat pencarian. Django mengizinkan pendefinisian indeks langsung di model.
# 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:
# Indeks komposit untuk query yang sering dipakai
indexes = [
# Indeks pada tanggal terbit (sering diurutkan)
models.Index(
fields=['published_date'],
name='book_pub_date_idx'
),
# Indeks komposit untuk filter penulis + status
models.Index(
fields=['author', 'status'],
name='book_author_status_idx'
),
# Indeks parsial: hanya buku yang tersedia
models.Index(
fields=['published_date'],
name='book_available_idx',
condition=models.Q(status='available')
),
]
# Pengurutan default yang memanfaatkan indeks
ordering = ['-published_date']Indeks-indeks ini meningkatkan performa query yang memfilter berdasarkan kolom tersebut.
Indeks mempercepat pembacaan tetapi sedikit memperlambat penulisan (INSERT, UPDATE). Buatlah indeks hanya pada kolom yang sering muncul di klausa WHERE, ORDER BY, atau JOIN.
Memakai query SQL mentah saat diperlukan
Untuk query kompleks atau optimasi spesifik basis data, query SQL mentah memberikan kontrol penuh.
# views.py
from django.db import connection
def get_books_with_raw_sql(request):
"""Query mentah untuk kasus khusus."""
# Cara 1: raw() untuk memperoleh objek model
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):
"""Eksekusi langsung untuk query non-SELECT."""
with connection.cursor() as cursor:
# Query dengan agregasi kompleks
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})Query mentah melewati ORM tetapi mengorbankan portabilitas antar basis data.
Menganalisis query dengan django-debug-toolbar
Alat django-debug-toolbar memungkinkan tampilan semua query SQL yang dihasilkan oleh sebuah view Django.
# settings.py - Konfigurasi debug toolbar
INSTALLED_APPS = [
# ... aplikasi lainnya
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... middleware lainnya
]
# Tampilkan toolbar untuk request lokal
INTERNAL_IPS = ['127.0.0.1']
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.sql.SQLPanel', # Query SQL
'debug_toolbar.panels.timer.TimerPanel', # Waktu eksekusi
'debug_toolbar.panels.cache.CachePanel', # Cache
]Konfigurasi ini mengaktifkan panel-panel paling berguna untuk optimasi.
# Logging query SQL di pengembangan
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}Logging ini menampilkan setiap query SQL di konsol, bermanfaat untuk mengenali masalah N+1.
Melakukan caching pada QuerySet
Untuk data yang sering diakses dan jarang diubah, caching menghindari query yang berulang.
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
def list_featured_books(request):
"""Mengambil buku unggulan dengan cache."""
cache_key = 'featured_books_list'
# Coba ambil dari cache
books = cache.get(cache_key)
if books is None:
# Cache miss: eksekusi query
books = list(
Book.objects.select_related('author')
.filter(featured=True)
.order_by('-published_date')[:10]
)
# Simpan dalam cache selama 5 menit
cache.set(cache_key, books, timeout=300)
return render(request, 'books/featured.html', {'books': books})
# Decorator untuk cache seluruh view
@cache_page(60 * 15) # Cache 15 menit
def list_all_tags(request):
"""Menampilkan seluruh tag (data jarang berubah)."""
tags = Tag.objects.annotate(
book_count=Count('books')
).order_by('-book_count')
return render(request, 'tags/list.html', {'tags': tags})Cache mengurangi beban basis data untuk data statis.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Kesimpulan
Optimasi query Django ORM bertumpu pada beberapa prinsip dasar:
✅ Ukur sebelum optimasi: gunakan django-debug-toolbar untuk menemukan masalah nyata
✅ Eliminasi masalah N+1: select_related untuk ForeignKey, prefetch_related untuk ManyToMany
✅ Batasi data: only(), defer(), values() memuat hanya yang dibutuhkan
✅ Indeks dengan bijak: buat indeks pada kolom yang sering difilter atau diurutkan
✅ Cache strategis: data yang jarang berubah memperoleh manfaat dari cache Django
✅ Query mentah: sebagai pilihan terakhir untuk optimasi spesifik basis data
Dijalankan secara metodis, teknik-teknik ini mengubah aplikasi Django yang lambat menjadi sistem yang efisien dan mampu menangani volume data besar. Django ORM tetap menjadi alat yang ampuh ketika seluk-beluknya dikuasai.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Django 5.2: Custom Middleware dan Signal Handling untuk Wawancara Teknis
Panduan lengkap Django 5.2 custom middleware dan signal handling untuk persiapan wawancara teknis. Mencakup request pipeline, async middleware, post_save, pre_save, custom signals, dan praktik terbaik produksi.

Pertanyaan Wawancara Django dan Python: 25 Teratas di 2026
25 pertanyaan wawancara Django dan Python yang paling sering ditanyakan. ORM, views, middleware, DRF, signals, dan optimasi dengan jawaban lengkap beserta contoh kode.

Django 5: Membangun REST API dengan Django REST Framework
Panduan lengkap membangun REST API profesional dengan Django 5 dan DRF. Serializers, ViewSets, autentikasi JWT, dan praktik terbaik dijelaskan secara mendetail.