Django dan PostgreSQL pada 2026: Pengindeksan, Pencarian Teks Lengkap, dan Pertanyaan Wawancara

Panduan praktis optimasi Django PostgreSQL: indeks B-tree, parsial, dan penutup, pencarian teks lengkap dengan SearchVector dan GIN, plus pertanyaan wawancara 2026.

Optimasi Django PostgreSQL dengan pengindeksan dan pencarian teks lengkap

Optimasi Django PostgreSQL menentukan perbedaan antara aplikasi yang mampu berkembang melampaui seribu pengguna pertamanya dan aplikasi yang tersendat pada setiap tampilan daftar. PostgreSQL hadir dengan mesin pengindeksan dan subsistem pencarian teks lengkap yang dibiarkan tidak tersentuh oleh sebagian besar proyek Django, sehingga membuang performa kueri yang sebenarnya bisa diperoleh kembali tanpa biaya. Panduan ini membahas pola pengindeksan, perkakas pencarian django.contrib.postgres, serta pertanyaan wawancara basis data yang membedakan insinyur Django tingkat menengah dari tingkat senior pada 2026.

Ukur Sebelum Mengindeks

Setiap indeks mempercepat operasi baca, tetapi memperlambat operasi tulis dan memakan ruang disk. Jalankan EXPLAIN (ANALYZE, BUFFERS) pada kueri yang sebenarnya, tambahkan indeks, lalu bandingkan rencana eksekusinya. Indeks yang tidak pernah dipilih oleh perencana kueri hanyalah beban tambahan pada setiap INSERT dan UPDATE.

Dasar Pengindeksan PostgreSQL di Django

Secara bawaan, PostgreSQL hanya membuat indeks B-tree pada kunci primer dan kolom dengan unique=True. Setiap kolom lain yang difilter atau diurutkan akan menjalankan pemindaian sekuensial hingga sebuah indeks tersedia. Django menyediakan indeks melalui Meta.indexes, yang lebih dianjurkan dibandingkan db_index=True yang lebih lama karena mendukung indeks komposit, parsial, dan penutup (covering) dalam satu tempat.

Urutan kolom di dalam indeks komposit bukan sekadar formalitas. Sebuah B-tree hanya dapat memanfaatkan indeks untuk sebuah kueri ketika filter menggunakan awalan (prefix) terdepan dari kolom-kolom yang diindeks, sebuah aturan yang dijelaskan secara mendalam di Use The Index, Luke. Indeks pada (status, created_at) melayani WHERE status = ? ORDER BY created_at, tetapi indeks yang sama tidak dapat mempercepat kueri yang hanya memfilter berdasarkan created_at.

python
# models.py
from django.db import models

class Order(models.Model):
    customer_email = models.EmailField()
    status = models.CharField(max_length=20)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # B-tree kolom tunggal untuk pencarian eksak dan rentang pada status
            models.Index(fields=["status"]),
            # Indeks komposit: melayani WHERE status = ? ORDER BY created_at DESC
            # Kolom terdepan (status) harus muncul di filter agar indeks terpakai
            models.Index(fields=["status", "-created_at"]),
        ]

Indeks komposit mengubah kueri dasbor seperti "pesanan tertunda terbaru" menjadi satu kali pemindaian indeks, bukan proses filter-lalu-urut atas seluruh tabel.

Indeks Parsial dan Penutup untuk Kueri Tertarget

Indeks parsial hanya mencakup baris yang memenuhi suatu kondisi, sehingga ukurannya tetap kecil dan tetap berada di memori. Ketika sebagian besar baris berbagi nilai yang sama dan kueri hanya menyentuh kelompok minoritas, indeks parsial jauh lebih murah dibandingkan indeks penuh. Indeks penutup melangkah lebih jauh: dengan menambahkan kolom non-kunci melalui include, PostgreSQL menjawab kueri langsung dari indeks tanpa menyentuh heap tabel, sebuah pola akses yang disebut index-only scan.

python
# models.py
from django.db import models
from django.db.models import Q

class Order(models.Model):
    # field seperti didefinisikan di atas

    class Meta:
        indexes = [
            # Indeks parsial: hanya mengindeks baris pending, mengabaikan riwayat completed
            models.Index(
                fields=["created_at"],
                name="pending_orders_idx",
                condition=Q(status="pending"),
            ),
            # Indeks penutup: total dibaca langsung dari indeks (index-only scan)
            models.Index(
                fields=["status"],
                include=["total"],
                name="status_total_covering_idx",
            ),
        ]

Pada tabel pesanan yang hanya 2% barisnya berstatus pending, indeks parsial bisa lima puluh kali lebih kecil dibandingkan indeks penuh pada kolom yang sama, dan perencana kueri menjaganya tetap panas di dalam buffer cache. Klausa include didokumentasikan pada index-only scan PostgreSQL dan merupakan keuntungan yang paling sering terlewatkan pada aplikasi Django yang mengagregasi kolom numerik di balik filter status.

Pencarian Teks Lengkap Django dengan PostgreSQL

Menggunakan title__icontains=query terasa praktis, tetapi cara ini memaksa pemindaian sekuensial dan tidak dapat memeringkat hasil berdasarkan relevansi. Pencarian teks lengkap Django membungkus tipe asli PostgreSQL tsvector dan tsquery melalui SearchVector, SearchQuery, dan SearchRank. Sebuah vektor pencarian menormalkan teks menjadi leksem, membuang kata henti (stop word) dan mereduksi kata ke bentuk dasarnya, sehingga pencarian untuk "running" cocok dengan "run" dan "ran".

python
# views.py
from django.contrib.postgres.search import (
    SearchVector, SearchQuery, SearchRank,
)
from .models import Article

def search_articles(query_text):
    query = SearchQuery(query_text, config="english")
    # Bobot A memeringkat kecocokan judul di atas kecocokan isi (bobot B)
    vector = (
        SearchVector("title", weight="A")
        + SearchVector("body", weight="B")
    )
    return (
        Article.objects.annotate(rank=SearchRank(vector, query))
        .filter(rank__gte=0.1)
        .order_by("-rank")
    )

Parameter weight memberikan kelompok prioritas dari A (tertinggi) hingga D. Kecocokan pada judul memiliki peringkat lebih tinggi daripada kecocokan pada isi meskipun keduanya mengandung istilah yang dicari, dan hal ini sesuai dengan cara pengguna mengharapkan pencarian bekerja. API lengkap dan model pembobotan empat tingkat dibahas dalam referensi pencarian teks lengkap Django.

Indeks GIN dan SearchVectorField untuk Pencarian Cepat

Kueri di atas menghitung ulang vektor pencarian untuk setiap baris pada setiap permintaan, yang masih wajar untuk beberapa ribu baris tetapi tidak dapat diterima untuk sejuta baris. Pola produksi menyimpan vektor tersebut di dalam SearchVectorField dan mengindeksnya dengan indeks GIN, yaitu jenis indeks yang digunakan PostgreSQL untuk nilai komposit seperti dokumen teks lengkap dan JSONB.

python
# models.py
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    # Dokumen pencarian yang sudah dihitung sebelumnya dan disimpan
    search_vector = SearchVectorField(null=True)

    class Meta:
        indexes = [
            # GIN mengubah pencarian teks lengkap menjadi pemindaian indeks logaritmik
            GinIndex(fields=["search_vector"]),
        ]

Field yang disimpan perlu tetap sinkron dengan title dan body. Sinyal post_save yang menulis vektor menggunakan .update() menghindari pemicuan sinyal secara rekursif, karena QuerySet.update() tidak memancarkan sinyal save.

python
# signals.py
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article

@receiver(post_save, sender=Article)
def sync_search_vector(sender, instance, **kwargs):
    Article.objects.filter(pk=instance.pk).update(
        search_vector=SearchVector("title", weight="A")
        + SearchVector("body", weight="B"),
    )

Bagi tim yang menggunakan Django 5.0 atau lebih baru, GeneratedField yang dihitung di basis data atau sebuah trigger PostgreSQL menghilangkan UPDATE tambahan sepenuhnya dengan memelihara kolom tersebut di dalam basis data. Apa pun mekanisme yang menjaganya tetap terisi, kueri kemudian memfilter field yang telah diindeks secara langsung, dan indeks GIN mengubah apa yang tadinya pemindaian tabel penuh menjadi pencarian logaritmik.

Indeks GIN Mahal untuk Ditulis

Indeks GIN mempercepat operasi baca dengan biaya tulis yang nyata, karena setiap penyisipan menyentuh banyak entri indeks. Pada tabel dengan volume tulis tinggi, atur parameter penyimpanan fastupdate dan pantau ukuran pending-list, atau operasi tulis teks lengkap akan tertahan di belakang pemeliharaan indeks.

Memilih Jenis Indeks PostgreSQL yang Tepat

B-tree adalah pilihan bawaan yang tepat, tetapi PostgreSQL menyediakan beberapa jenis indeks melalui django.contrib.postgres.indexes, dan memilih yang salah hanya membuang ruang disk tanpa memperbaiki satu kueri pun. Pilihan ini mengikuti bentuk data dan operator yang digunakan kueri, sebagaimana dikatalogkan dalam dokumentasi jenis indeks PostgreSQL.

| Jenis indeks | Paling cocok untuk | Kelas Django | |------------|----------|--------------| | B-tree | Kesetaraan dan rentang pada kolom skalar | models.Index | | GIN | Pencarian teks lengkap, JSONB, kandungan array | GinIndex | | BRIN | Tabel besar hanya-tambah (append-only) yang terurut berdasarkan kolom | BrinIndex | | Hash | Pencarian hanya-kesetaraan pada kolom berukuran besar | HashIndex |

BRIN layak mendapat perhatian khusus untuk data deret waktu. Pada tabel berisi puluhan juta baris yang disisipkan dalam urutan stempel waktu, sebuah BrinIndex pada created_at hanya menempati beberapa kilobita di saat B-tree membutuhkan ratusan megabita, karena BRIN hanya menyimpan nilai minimum dan maksimum per rentang blok. Konsekuensinya, BRIN hanya membantu ketika urutan fisik baris mengikuti kolom yang diindeks, kondisi yang secara alami terpenuhi pada log hanya-tambah dan tabel peristiwa.

python
# models.py
from django.contrib.postgres.indexes import BrinIndex
from django.db import models

class Event(models.Model):
    payload = models.JSONField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # Indeks mungil untuk pemindaian rentang pada tabel hanya-tambah yang terurut waktu
            BrinIndex(fields=["created_at"]),
        ]

Menyesuaikan jenis indeks dengan pola akses adalah tema yang berulang dalam wawancara senior, karena hal itu membuktikan bahwa seorang kandidat memikirkan tata letak penyimpanan alih-alih menambahkan B-tree secara refleks di mana-mana.

Siap menguasai wawancara Django Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pertanyaan Wawancara Basis Data Django pada 2026

Pertanyaan basis data mendominasi wawancara Django senior karena kefasihan menggunakan ORM tidak serta-merta berarti memahami apa yang dihasilkan ORM. Pertanyaan-pertanyaan berikut mencerminkan hal yang benar-benar digali panel perekrut pada 2026, dan pertanyaan-pertanyaan itu berpadu secara alami dengan penyetelan pada tingkat kueri yang dibahas dalam panduan optimasi kueri ORM Django.

Kapan indeks komposit pada (a, b) gagal membantu sebuah kueri? Ketika kueri tidak memfilter berdasarkan a. Sebuah B-tree diurutkan berdasarkan kolom terdepannya terlebih dahulu, sehingga filter yang hanya menggunakan b tidak dapat memanfaatkan indeks. Inilah aturan awalan-terkiri (leftmost-prefix), dan kandidat yang mampu menjelaskannya menunjukkan bahwa mereka memahami struktur indeks, bukan sekadar menghafal resep.

Apa perbedaan antara select_related dan prefetch_related? select_related melakukan SQL JOIN dan bekerja untuk relasi foreign key dan satu-ke-satu dalam satu kueri. prefetch_related menjalankan kueri kedua lalu menggabungkan hasilnya di Python, yang diperlukan untuk relasi banyak-ke-banyak dan foreign key terbalik. Memilih yang keliru akan melewatkan optimasi atau justru memicu kueri kedua yang tidak perlu.

Bagaimana icontains berbeda dari pencarian teks lengkap pada tingkat basis data? icontains dikompilasi menjadi ILIKE '%term%', yang tidak dapat memanfaatkan indeks B-tree standar dan memindai setiap baris. Pencarian teks lengkap mencocokkan terhadap tsvector yang telah dihitung sebelumnya dan didukung indeks GIN, lalu memeringkat hasil berdasarkan relevansi. Perbedaan ini merupakan sinyal andal bahwa seorang kandidat telah melangkah melampaui kumpulan data mainan.

Mengapa count() bisa lambat pada tabel besar, dan apa alternatifnya? PostgreSQL tidak menyimpan jumlah baris tabel dalam cache, sehingga COUNT(*) memindai setiap baris yang terlihat di bawah MVCC. Untuk jumlah perkiraan, mengueri pg_class.reltuples mengembalikan estimasi secara instan, dan untuk penomoran halaman, keyset pagination pada kolom yang terindeks menghindari penghitungan sama sekali. Latihan terstruktur untuk pola-pola ini tersedia dalam modul wawancara caching Django, tempat penghitung berbasis cache menjadi tema yang berulang, dan di seluruh jalur pembelajaran Django yang lengkap.

Apa yang diungkap EXPLAIN ANALYZE yang tidak diberikan EXPLAIN saja? EXPLAIN menampilkan perkiraan biaya dari perencana; EXPLAIN ANALYZE benar-benar menjalankan kueri dan melaporkan waktu serta jumlah baris yang sebenarnya. Selisih besar antara jumlah baris perkiraan dan aktual menunjukkan statistik tabel yang usang, yang dapat diperbaiki dengan ANALYZE, dan merupakan penyebab umum perencana mengabaikan indeks yang semestinya jelas berlaku.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Kesimpulan

  • Definisikan indeks di Meta.indexes alih-alih db_index=True agar indeks komposit, parsial, dan penutup berada di satu tempat yang mudah diaudit.
  • Urutkan kolom indeks komposit berdasarkan aturan awalan-terkiri: kolom terdepan harus muncul dalam filter kueri agar indeks dapat diterapkan.
  • Gunakan indeks parsial ketika kueri hanya menyentuh sebagian kecil baris, seperti pesanan pending dalam tabel yang didominasi riwayat completed.
  • Tambahkan indeks penutup dengan include untuk memungkinkan index-only scan dan melewati akses heap pada kueri agregat yang padat operasi baca.
  • Ganti pencarian icontains dengan SearchVector, SearchQuery, dan SearchRank untuk memperoleh pemeringkatan relevansi dan stemming secara gratis.
  • Simpan dokumen pencarian dalam SearchVectorField yang didukung GinIndex, dijaga tetap sinkron oleh sebuah sinyal, GeneratedField, atau trigger basis data, sebelum pencarian teks lengkap mencapai skala produksi.
  • Selalu validasi keputusan pengindeksan dengan EXPLAIN (ANALYZE, BUFFERS); indeks yang tidak pernah dipilih perencana adalah pajak saat tulis tanpa manfaat saat baca.

Tag

#django
#postgresql
#database optimization
#full text search
#python

Bagikan

Artikel terkait