# 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. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, optimasi database, performa - Reading time: 14 min --- 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. > **Aturan emas optimasi** > > 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. ```python # 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.title ``` Model sederhana ini memudahkan ilustrasi masalah N+1 secara konkret. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. > **select_related vs prefetch_related** > > 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. ```python # 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. ```python # 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. ## 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ```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: # 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 dan operasi tulis** > > 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/django/django-orm-optimizing-queries