Belajar Django - Advanced ORM & Query Optimization
Episode 14 of 27

Belajar Django - Advanced ORM & Query Optimization

Meningkatkan kemampuan ORM Django ke level lanjutan: Q objects untuk query kompleks, F expressions untuk update atomik, subquery, window functions, dan strategi optimisasi query termasuk pemberantasan N+1 untuk aplikasi berkinerja tinggi.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Dasar-dasar ORM di episode 6 dan performa cache di episode 12 sudah membawa aplikasi jauh. Tapi ada kalanya query itu sendiri yang jadi bottleneck: data bertambah, logika filtering kompleks, dan N+1 kembali menghantui. Di episode ini kita menaikkan skill ORM ke level lanjutan — alat-alat yang membedakan "kode yang jalan" dari "kode yang cepat".

Mengapa topik ini penting? Karena di traffic tinggi, perbedaan satu query vs seratus query adalah perbedaan 200 OK dan timeout. Q objects, F expressions, subquery, dan window functions adalah empat senjata yang menyelesaikan 90% masalah performa query di dunia nyata.

Q Objects: Filter Logis Kompleks

filter(**kwargs) biasa hanya menggabungkan kondisi dengan AND. Untuk OR, NOT, dan kombinasi, gunakan Q objects:

Pythonblog/views.py - Q objects
from django.db.models import Q
 
# Post berstatus draft ATAU view_count di bawah 10
posts = Post.objects.filter(
    Q(status="draft") | Q(view_count__lt=10)
)
 
# Filter "search" lintas banyak field
query = request.GET.get("q", "")
posts = Post.objects.filter(
    Q(title__icontains=query) | Q(body__icontains=query)
).distinct()

Operator: | (OR), & (AND), ~ (NOT). Q(condition) | Q(...) bisa dikombinasikan dengan kwargs biasa (yang selalu AND). distinct() perlu saat JOIN menghasilkan duplikat baris — contoh klasik saat memfilter tags__name yang banyak.

F Expressions: Update Atomik

F expressions mereferensikan kolom database dalam query, bukan nilai Python. Ini krusial untuk dua alasan: update atomik (bebas race condition) dan menghindari memuat data ke memori.

PythonUpdate atomik dengan F
from django.db.models import F
 
# Anti-pattern: race condition saat traffic tinggi
post = Post.objects.get(pk=1)
post.view_count += 1
post.save()
 
# Benar: atomik di database
Post.objects.filter(pk=1).update(view_count=F("view_count") + 1)

Tanpa F, dua request bersamaan bisa membaca nilai sama (misal 10), masing-masing menulis 11 — hasil akhir 11, bukan 12. Dengan F, database mengeksekusi SET view_count = view_count + 1 secara atomik di bawah lock. Pola ini wajib untuk counter seperti view_count.

Tip

F() juga bisa dibandingkan antar kolom dalam satu query: filter(updated_at__gt=F("created_at")). Dan setelah update memakai F, ingat untuk memanggil .refresh_from_db() jika masih butuh nilai terbaru di objek yang sama — nilai di memori tidak otomatis berubah.

Subquery dan Exists

Untuk query yang membutuhkan nilai dari query lain, gunakan Subquery. Contoh klasik: "post yang punya komentar terbaru di 24 jam terakhir":

Pythonblog/views.py - Subquery
from django.db.models import OuterRef, Subquery
 
latest_comment_time = Comment.objects.filter(
    post=OuterRef("pk")
).order_by("-created_at").values("created_at")[:1]
 
posts = Post.objects.annotate(
    last_comment_at=Subquery(latest_comment_time)
).filter(last_comment_at__isnull=False)

OuterRef("pk") mereferensikan post di query luar. Untuk cek keberadaan yang hemat, Exists lebih murah daripada Subquery karena berhenti saat menemukan baris pertama:

PythonExists
from django.db.models import Exists, OuterRef
 
has_comments = Comment.objects.filter(post=OuterRef("pk"))
posts = Post.objects.annotate(
    has_comments=Exists(has_comments)
)

Window Functions

Window functions menghitung agregat per-baris tanpa menggabungkan baris — beda dengan annotate biasa yang mengelompokkan. Contoh: ranking post berdasarkan view_count:

PythonWindow functions
from django.db.models import F, Window
from django.db.models.functions import Rank
 
posts = Post.objects.annotate(
    rank=Window(
        expression=Rank(),
        order_by=F("view_count").desc(),
        partition_by=F("category"),
    )
)

Rank() memberi peringkat dalam grup per category. partition_by memecah window per kategori, order_by menentukan urutan peringkat. Window functions sangat berguna untuk "top N per kategori", moving average, dan cumulative sum — semua dihitung di database, bukan di Python.

Warning

Window functions adalah fitur database modern — didukung penuh PostgreSQL 9.4+, MySQL 8.0+, dan SQLite 3.25+. Kalau project masih memakai MySQL 5.7 atau SQLite lama, query ini akan error. Ini juga salah satu alasan rekomendasi Django menuju PostgreSQL untuk production.

Pemberantasan N+1: Audit Query

N+1 (episode 6) kembali muncul seiring query bertambah kompleks. Senjata pertamanya adalah melihat query sebenarnya:

PythonLihat query yang dihasilkan ORM
qs = Post.objects.select_related("author").prefetch_related("comments")
print(qs.query)  # SQL mentah
from django.db import connection
print(len(connection.queries))  # jumlah query terpakai

django-debug-toolbar (episode 15) menampilkan jumlah query per halaman di browser — deteksi N+1 jadi instan. Strategi optimalisasi mengikuti prinsip:

  1. select_related untuk semua FK yang pasti diakses (forward).
  2. prefetch_related untuk reverse dan M2M.
  3. only/defer untuk memuat subset kolom besar (TextField) hanya saat dibutuhkan.
  4. values/values_list untuk data yang tidak perlu di-instantiate menjadi objek model.
  5. Indexes di Meta untuk kolom yang difilter/diurutkan.

Contoh gabungan:

PythonQuery gabungan yang optimal
posts = (
    Post.objects.filter(status="published")
    .select_related("author", "category")
    .prefetch_related("tags")
    .only("id", "title", "slug", "author_id", "category_id")
    .order_by("-created_at")
)

only(...) membatasi kolom yang dimuat — body (TextField besar) tidak dibawa sampai benar-benar diakses, menghemat bandwidth DB untuk list view.

Praktik: Optimisasi Halaman Post List

Gabungkan semua teknik untuk halaman list:

Pythonblog/views.py - PostListView optimal
from django.db.models import Count
from django.views.generic import ListView
 
class PostListView(ListView):
    model = Post
    paginate_by = 10
 
    def get_queryset(self):
        return (
            super()
            .get_queryset()
            .filter(status="published")
            .select_related("author", "category")
            .annotate(comment_count=Count("comments"))
            .only("id", "title", "slug", "excerpt", "author__username",
                  "category__name", "created_at")
        )

Satu query utama + agregat comment_count — tanpa N+1. Bandingkan dengan versi naif yang memuat body + query komentar per post; untuk 50 post, selisihnya bisa 1 query vs 100+ query.

Penutup

Inti yang harus dibawa pulang:

  • Q objects (|, &, ~) untuk filter kompleks; distinct() saat JOIN memunculkan duplikat.
  • F expressions untuk update atomik — wajib untuk counter bebas race condition.
  • Subquery/OuterRef untuk query bertingkat; Exists lebih hemat untuk cek keberadaan.
  • Window functions (Window + Rank) untuk ranking/agregat per-baris tanpa menggabungkan baris.
  • Audit query dengan qs.query, connection.queries, dan debug-toolbar.
  • Kombinasi select_related + prefetch_related + only = 1 query list tanpa N+1.

Di episode 15 selanjutnya kita belajar melihat ke dalam aplikasi: Logging, Debugging & Testing — konfigurasi LOGGING, structured logging, Django Debug Toolbar, TestCase dengan client test, pytest-django, dan coverage. Sampai jumpa di episode 15!

Belajar Django - Advanced ORM & Query Optimization | Belajar Django