# 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. - Published: 2026-06-27 - Updated: 2026-07-06 - Author: SharpSkill - Tags: django, postgresql, database optimization, full text search, python - Reading time: 9 min --- 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](https://use-the-index-luke.com/sql/where-clause/the-equals-operator/concatenated-keys). 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](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) 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](https://docs.djangoproject.com/en/5.2/ref/contrib/postgres/search/). ## 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](https://www.postgresql.org/docs/current/indexes-types.html). | 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. ## 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](/blog/django/django-orm-optimizing-queries). **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](/technologies/django/interview-questions/django-caching), tempat penghitung berbasis cache menjadi tema yang berulang, dan di seluruh [jalur pembelajaran Django](/technologies/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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/django/django-postgresql-indexing-full-text-search-2026