Blog

tempatku sharing informasi pengalaman

N+1 query problem: udah tahu bermasalah tapi masih terus terulang

· Database · Query

Query sering tampak baik-baik saja saat data masih sedikit. Masalahnya baru terlihat setelah jumlah record bertambah. Salah satu pola yang perlu dicari adalah N+1 query problem: aplikasi mengambil daftar dengan satu query, lalu menjalankan query lain untuk setiap record di daftar tersebut.

Dari mana angka N+1 berasal?

Misalnya halaman menampilkan 50 pesanan dan nama produknya. Satu query mengambil daftar pesanan. Jika aplikasi lalu mengambil item untuk tiap pesanan secara terpisah, jumlahnya menjadi 1 + 50, atau 51 query. Huruf N mewakili jumlah record yang diproses.

-- 1 query untuk mengambil daftar pesanan
SELECT id, customer_name FROM orders LIMIT 50;

-- lalu query ini berjalan berulang untuk tiap pesanan
SELECT product_name FROM order_items WHERE order_id = ?;

Dengan beberapa pesanan, selisihnya mungkin tidak terasa. Saat daftar makin panjang, query berulang itu ikut bertambah. Hasil halaman bisa tetap benar, tetapi database menerima lebih banyak pekerjaan dan request berpotensi menunggu lebih lama. Jadi, jangan hanya periksa datanya; lihat juga jumlah query yang dipakai untuk menampilkannya.

Contoh TypeScript

Contoh ini memakai fungsi database sederhana agar pola query-nya terlihat. Fungsi getItems dipanggil sekali untuk setiap pesanan. Tipe datanya disederhanakan:

type Order = { id: number; customerName: string };
type OrderItem = { id: number; orderId: number; productName: string };

declare function getOrders(): Promise<Order[]>;
declare function getItems(orderId: number): Promise<OrderItem[]>;

async function loadOrdersWithItems() {
  const orders = await getOrders();

  return Promise.all(
    orders.map(async (order) => ({
      ...order,
      items: await getItems(order.id),
    })),
  );
}

Jika getOrders() mengambil 50 pesanan dan setiap getItems() menjalankan satu query, totalnya 51 query. Promise.all membuat query item berjalan bersamaan, tetapi jumlah query-nya tetap sama.

Contoh SQL

Dengan tiga pesanan, pola N+1 menjalankan satu query untuk pesanan lalu satu query item per pesanan. Berikut empat query yang terjadi:

SELECT id, customer_name FROM orders WHERE id IN (101, 102, 103);
SELECT id, order_id, product_name FROM order_items WHERE order_id = 101;
SELECT id, order_id, product_name FROM order_items WHERE order_id = 102;
SELECT id, order_id, product_name FROM order_items WHERE order_id = 103;

Batch mengambil semua item dengan satu query tambahan, jadi totalnya dua query:

SELECT id, customer_name FROM orders WHERE id IN (101, 102, 103);
SELECT id, order_id, product_name
FROM order_items
WHERE order_id IN (101, 102, 103);

Contoh Prisma ORM 8

Contoh ini memakai API Prisma ORM 8 untuk PostgreSQL. Anggap client db sudah dibuat dan model Order punya relasi items ke OrderItem. Query di dalam map membuat satu query item untuk setiap pesanan:

const orders = await db.orm.public.Order.all();

const ordersWithItems = await Promise.all(
  orders.map(async (order) => ({
    ...order,
    items: await db.orm.public.OrderItem
      .where({ orderId: order.id })
      .all(),
  })),
);

Ambil relasi melalui include, bukan dengan query item di dalam loop:

const orders = await db.orm.public.Order
  .select("id", "customerName")
  .include("items", (item) =>
    item.select("id", "productName"),
  )
  .all();

Untuk PostgreSQL, Prisma ORM 8 membaca relasi dengan satu statement saat memakai include. Contoh ini mengambil relasi pesanan dalam satu query, bukan satu query per pesanan. Baca juga dokumentasi resmi tentang relasi dan query Prisma. Kalau proyekmu masih memakai Prisma ORM 7, nama dan bentuk API client-nya berbeda. Lihat panduan perbandingan query Prisma 7 dan 8.

Cara menemukannya

  • Buka log query atau profiler saat halaman dijalankan.
  • Cari query dengan bentuk sama, tetapi nilai foreign key berbeda.
  • Coba data sedikit dan banyak, lalu bandingkan jumlah query-nya.
  • Ukur lagi setelah perbaikan untuk memastikan hasilnya berubah.

Contohnya, jalankan halaman dengan satu pesanan, lalu ulangi dengan puluhan pesanan. Jika query bertambah hampir satu untuk setiap pesanan, cari akses relasi di dalam loop.

Pastikan hasilnya tetap benar

Setelah mengubah cara pemuatan, cek kembali filter dan paginasi. Pastikan juga relasi yang dimuat memang dipakai. Tambahkan pengujian jumlah query atau pantau log-nya agar perubahan berikutnya tidak mengembalikan pola yang sama.