Masalah utama dalam judul ini terpenuhi dengan cara langsung: tim backend Go Fiber harus segera menemukan query PostgreSQL yang melambat, lalu memasang indeks yang relevan agar filter dan join tetap scalable. Artikel ini mengurai langkah observabilitas, pola penulisan handler serta middleware Fiber untuk pelacakan, pendekatan indexing, dan opsi pagination yang tahan terhadap pertumbuhan data.

1. Observabilitas : Log Query dan EXPLAIN ANALYZE

Mulai dengan log waktu eksekusi query. Dalam aplikasi Go Fiber, bentuk log bisa ditangkap lewat middleware sebelum ke handler dan setelah response. Catat nama handler, parameter query, dan waktu respon database sehingga bisa memetakan apakah penundaan bersifat database-specific.

1.1. Middleware pengukuran waktu

func LogQueryDuration() fiber.Handler {
    return func(c *fiber.Ctx) error {
        start := time.Now()
        err := c.Next()
        duration := time.Since(start)
        log.Printf("%s %s took %s", c.Method(), c.Path(), duration)
        return err
    }
}

Middleware ini tidak hanya mencatat waktu handler keseluruhan, tetapi juga bisa dikombinasikan dengan lapisan database wrappers untuk mengetahui bagian mana yang paling lama. Gabungkan dengan context tracing agar log bisa dihubungkan ke query actual.

1.2. Gunakan EXPLAIN ANALYZE untuk query bermasalah

Setelah log menunjukkan endpoint lambat, jalankan EXPLAIN ANALYZE pada query yang sama secara manual. Cari sequential scan, big joins tanpa indeks, nested loop di data besar, atau sort yang memakai disk. Hasil EXPLAIN harus disimpan bersama catatan parameter sehingga Anda tahu apakah hasilnya konsisten dengan pola traffic sebenarnya. Untuk query dinamis, siapkan template EXPLAIN yang boleh Anda jalankan melalui psql dengan parameter yang cocok.

2. Struktur Handler Fiber yang Terukur

Handler harus jelas memisahkan parsing request, pemanggilan repository, dan penanganan respon. Ini mempermudah identifikasi bagian mana yang memerlukan optimasi.

func ListUsers(c *fiber.Ctx) error {
    filter := parseFilter(c)
    ctx := c.Context()

    users, err := repository.ListUsers(ctx, filter)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "gagal mengambil data pengguna")
    }

    return c.JSON(users)
}

Repository sebaiknya menerima context lalu meneruskannya ke database driver agar observabilitas (misalnya tracing, timeout) tetap konsisten. Di repository inilah Anda menambahkan logging query atau menggunakan library seperti sqlx untuk named query agar parameter jelas saat debugging.

3. Index Cerdas untuk Filter dan Join

Performa PostgreSQL bergantung banyak pada indeks yang sesuai pola akses. Prioritaskan indexing berdasarkan query yang terbukti lambat dari observabilitas. Berikut pendekatan umum:

  • Filter tunggal: Jika query menggunakan WHERE terhadap satu kolom, indeks B-tree pada kolom tersebut akan mempercepat pencarian. Perhatikan cardinality untuk menghindari indeks yang tidak digunakan.
  • Filter gabungan/pagination: Gunakan indeks komposit (multi-column) jika query mengurutkan lalu memfilter. Urutan kolom dalam indeks harus mencerminkan urutan filter dan sort.
  • Join: Pastikan kolom join memiliki indeks. Untuk join antar tabel besar, indexed foreign key menghindari nested loop penuh.

Selalu gunakan EXPLAIN untuk membuktikan indeks mana yang digunakan. Jika query tetap melakukan sequential scan padahal indeks ada, periksa apakah tipe data dan operator cocok (misalnya menggunakan <> pada kolom yang memiliki indeks). Hindari indeks pada kolom yang sering di-update karena memperlambat write.

3.1. Trade-off indexing

Menambahkan indeks mempercepat read tetapi memperlambat write dan menambah overhead storage. Untuk data yang banyak operasi INSERT/UPDATE, fokus ke indeks yang benar-benar dipakai query kritikal. Gunakan pg_stat_user_indexes untuk memonitor <>.

4. Pagination: Cursor vs Keyset

Ketika datanya tumbuh, pagination offset menjadi tidak stabil karena nilai offset menghasilkan scan lebih besar dan data yang berubah menyebabkan hasil hilang atau duplikat.

  • Cursor pagination (keyset): Gunakan index pada kolom yang menjadi basis sorting (misal created_at, id terurut). Query contohnya: WHERE (created_at, id) < (last_created_at, last_id). Ini memanfaatkan indeks komposit dan menghasilkan response stabil, terutama dengan data insert terus menerus.
  • Offset pagination: Masih berguna untuk laporan statis atau ketika pengguna perlu lompat langsung ke halaman tertentu. Namun, batasi offset maksimum dan kombinasikan dengan caching hasil page tertentu.

Dalam implementasi Fiber, kirim cursor token dalam respon sehingga klien tahu nilai terakhir. Token ini bisa berupa string JSON terenkripsi atau hanya dua kolom yang relevan.

5. Debugging dan Tips Tambahan

  • Catat parameter nyata: Pastikan log menyimpan parameter aktual. Sering kali query lambat hanya pada subset tertentu.
  • Gunakan connection pool observability: Monitoring pool membantu tahu apakah lambat karena menunggu koneksi, bukan query.
  • Perhatikan vacuum/analyze: PostgreSQL perlu statistik up-to-date. Jalankan VACUUM ANALYZE secara berkala untuk membuat planner menggunakan indeks terbaru.
  • Index-only scan: Jika query hanya membaca kolom tertentu, pertimbangkan covering index (indeks yang mencakup kolom-kolom dalam SELECT) agar tidak perlu load row data.

Setelah perubahan, ulangi EXPLAIN ANALYZE dan pantau middleware log untuk memastikan latency menurun. Jika tidak, evaluasi apakah bottleneck di database atau di aplikasi Go Fiber (misalnya marshalling JSON yang mahal).

Kesimpulan

Untuk tim backend Go Fiber, langkah praktisnya adalah menggabungkan observabilitas level handler, diagnosis EXPLAIN ANALYZE, dan indeks yang sesuai pola query. Gunakan pagination keyset dimana memungkinkan dan catat trade-off setiap perubahan indeks. Dengan pendekatan ini data yang tumbuh tidak akan membuat respons API melambat secara drastis.