Back to blog

Gara-Gara Serializer Nested, API Gw Jadi 6 Detik

Gara-Gara Serializer Nested, API Gw Jadi 6 Detik

djangoperformancebackendapidebugging

Gara-Gara Serializer Nested, API Gw Jadi 6 Detik

Awalnya gw kira:

“ah paling query database biasa”

Ternyata bukan 😭

API homepage yang tadinya:

200–300ms

pelan-pelan naik jadi:

2 detik

Terus:

4 detik

Dan akhirnya:

6 DETIK 💀

Padahal:

  • CPU normal
  • server gak down
  • PostgreSQL gak merah
  • Redis aman

Jadi problemnya di mana?

Endpoint Monster

Endpoint-nya kira-kira begini:

class HomepageAPIView(APIView): def get(self, request): campaigns = Campaign.objects.all()[:20]

    serializer = CampaignSerializer(
        campaigns,
        many=True
    )

    return Response(serializer.data)

Kelihatannya innocent.

Masalahnya ada di:

CampaignSerializer

😭

Nested Serializer Dari Neraka

Isi serializer-nya kira-kira:

class CampaignSerializer(serializers.ModelSerializer): user = UserSerializer() category = CategorySerializer() donations = DonationSerializer(many=True)

class Meta:
    model = Campaign
    fields = "\_\_all\_\_"

Kelihatannya clean.

Kelihatannya elegant.

Kelihatannya reusable.

Tapi ternyata…

ini pembuka gerbang penderitaan 😭

Yang Sebenarnya Terjadi

Misalnya:

  • 20 campaign
  • tiap campaign punya:
  • user
  • category
  • donations

Tanpa sadar yang terjadi:

1 query campaign 20 query user 20 query category 20 query donation

Belum kalau:

  • donation serializer nested lagi
  • user serializer nested profile lagi
  • profile nested address lagi

😭😭😭

Akhirnya total query bisa:

200+

buat SATU endpoint.

Dan Parahnya…

Waktu local development:

gak kerasa

Karena:

  • data masih sedikit
  • database kosong
  • internet cepat
  • laptop ngangkat

Pas production:

  • data ratusan ribu
  • relation makin banyak
  • serializer makin nested

baru mulai meledak 🔥

Ketahuan Gara-Gara Django Silk

Akhirnya gw coba profiling pakai:

Django Silk

Dan hasilnya:

214 SQL queries

💀

Pas buka detail request:

  • query sama diulang terus
  • SELECT user WHERE id=...
  • SELECT category WHERE id=...
  • SELECT donation WHERE ...

Classic:

N+1 Query Apocalypse

Penyebab Utamanya

Masalah utama nested serializer biasanya:

  • lazy loading relation
  • looping relation tanpa prefetch
  • serializer manggil query tambahan
  • SerializerMethodField query DB lagi
  • nested terlalu dalam

Dan yang paling bahaya:

SerializerMethodField

😭

Karena sering gak sadar bikin query baru tiap object.

Contoh Dosa Yang Sering Terjadi

Misalnya:

class CampaignSerializer(serializers.ModelSerializer): total_donation = serializers.SerializerMethodField()

def get\_total\_donation(self, obj):
    return Donation.objects.filter(
        campaign=obj
    ).count()

Kelihatannya normal.

Padahal kalau ada:

100 campaign

Maka:

100 query tambahan 😭

Solusi Pertama: select_related

Untuk ForeignKey / OneToOne:

Campaign.objects.select_related( "user", "category" )

Ini bakal JOIN query jadi satu.

Solusi Kedua: prefetch_related

Untuk reverse relation / many-to-many:

Campaign.objects.prefetch_related( "donations" )

Ini bikin Django ambil relation sekaligus.

Bukan query satu-satu.

Setelah Dioptimasi

Query turun dari:

214 query

jadi:

8 query 🔥

Latency:

6 detik

jadi:

400ms

Dan server langsung adem 😭

Pelajaran Yang Gw Dapet

1. Serializer Itu Bisa Mahal

Serializer DRF kelihatannya “clean”.

Tapi abstraction tinggi sering nyembunyiin query mahal.

2. Nested Serializer Harus Hati-Hati

Bukan berarti gak boleh.

Tapi:

  • relation harus diprefetch
  • jangan nested terlalu dalam
  • jangan recursive tanpa limit

3. SerializerMethodField Itu Tersangka Utama

Kadang innocent.

Kadang monster 😭

Selalu cek:

  • apakah dia query DB?
  • dipanggil berapa kali?
  • bisa dianotasi gak?

Tips Yang Sekarang Selalu Gw Pakai

Selalu Profiling

Pakai:

  • Django Silk
  • Django Debug Toolbar
  • query logging

Jangan nebak-nebak.

Selalu Cek Jumlah Query

Kadang endpoint:

300ms

Tapi query:

400 query

Artinya tinggal tunggu waktu buat meledak 😭

Jangan Takut Flat Serializer

Kadang lebih bagus:

CampaignListSerializer CampaignDetailSerializer CampaignSimpleSerializer

Daripada:

UniversalSerializerDariNerakaSerializer

😭

Realita Backend Engineering

Mayoritas performance issue backend itu bukan:

  • Python lambat
  • Django lambat
  • PostgreSQL jelek

Tapi:

query yang gak sadar kebanyakan

Dan nested serializer sering jadi:

penyebab utamanya 😭

Penutup

DRF serializer itu powerful banget.

Tapi kalau gak hati-hati:

  • query meledak
  • latency naik
  • CPU naik
  • database nangis

Dan yang paling serem:

semuanya masih “jalan normal”

Sampai traffic naik 💀

Jadi sekarang tiap bikin serializer, gw selalu mikir:

"ini bakal query berapa kali ya?"

Karena kadang…

1 baris nested serializer bisa berubah jadi:

6 detik penderitaan 😭