[{"data":1,"prerenderedAt":136},["ShallowReactive",2],{"site-header":3,"site-footer-license":37,"article-query-tinggal-2-tapi-response-masih-1-detik-with-latest":41},{"navigations":4},[5,20],{"collection":6,"item":7},"series",{"__typename":6,"title":8,"slug":9,"children_series":10},"Artikel","all",[11,14,17],{"title":12,"slug":13},"Algoritma dan Struktur Data","algoritma-struktur-data",{"title":15,"slug":16},"Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":18,"slug":19},"Kecerdasan Artifisial","kecerdasan-artifisial",{"collection":6,"item":21},{"__typename":6,"title":22,"slug":23,"children_series":24},"Tutorial","tutorial",[25,28,31,34],{"title":26,"slug":27},"Belajar Java","belajar-java",{"title":29,"slug":30},"Belajar Pascal","belajar-pascal",{"title":32,"slug":33},"Belajar Python","belajar-python",{"title":35,"slug":36},"Belajar Golang","belajar-golang",{"name":38,"url":39,"show":40},"CC BY-SA","https://creativecommons.org/licenses/by-sa/4.0/",false,{"article":42,"renderedBody":72,"renderedTakeaway":73,"latestArticles":74,"toc":116},{"prerequisite":43,"id":44,"title":45,"slug":46,"excerpt":47,"published_at":48,"thumbnail_image":49,"tags":51,"reading_time":55,"takeway":56,"body":57,"authors":58,"series":64},null,"104","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","query-tinggal-2-tapi-response-masih-1-detik","Endpoint feed yang cuma menampilkan 20 tulisan butuh satu detik, dan server database dihujani 41 query. Kamu kerjakan nasihat N+1 yang standar, query-nya turun jadi 2, dan latency-nya nggak bergerak. Ini versi \"apa adanya\" dari masalah itu, plus lab-nya.","2026-10-04T09:59:21.000Z",{"id":50},"3214df59-f4c6-4677-9983-2e0a55b18f97",[52,53,54],"database","performance","backend",5,"- N+1 itu 1 query untuk mengambil daftar, lalu N query tambahan untuk melengkapi tiap barisnya.\n- Nasihat \"pakai eager loading\" nggak salah, tapi dia memperbaiki cara mengambil data - bukan banyaknya data yang harus menyeberang.\n- Jumlah query itu metrik yang gampang dilihat, dan itu sebabnya dia jadi satu-satunya yang dilihat. Dua kelompok query bisa berbeda biaya ratusan kali lipat.\n- Kasus lengkapnya bisa kamu kerjakan sendiri di lab: make up, lalu make check.","Satu halaman feed butuh satu detik untuk tampil. Datanya cuma 400 tulisan dan 750.000\nkomentar, dan yang dikerjakan endpoint-nya sederhana: ambil 20 tulisan terbaru, lalu tampilkan\nnama penulisnya dan jumlah komentarnya masing-masing.\n\nTidak ada error. Tidak ada log kuning. Tidak ada satu baris pun yang kelihatan salah kalau\nkamu membacanya. Sampai ada yang mengeluh, kamu mengukur, dan ternyata database\ndihujani **41 query** hanya untuk melayani satu request itu.\n\n## N+1, dan kenapa namanya begitu\n\nKalau kamu sudah tahu bagian ini, lewati saja satu paragraf.\n\nNamanya N+1 karena jumlahnya: **1 query** untuk mengambil daftar utamanya, lalu **N query**\ntambahan untuk melengkapi tiap baris di daftar itu. Untuk 20 tulisan, hasilnya 21 query.\n\n```python\nposts = session.execute(select(Post).limit(20)).scalars().all()\n\nfor post in posts:\n    print(post.author.name)   # tiap akses memicu query baru ke database\n```\n\nPenyebabnya *lazy loading*, perilaku bawaan hampir semua ORM: data relasi nggak diambil sampai\nkamu benar-benar mengaksesnya. Sekilas itu masuk akal — hemat memori, dan kamu cuma mengambil\nyang kamu butuh. Masalahnya \"kamu butuh\" ternyata terjadi di dalam loop, dan hasilnya\nbukan penghematan, tapi 20 kali bolak-balik ke database untuk sesuatu yang bisa diselesaikan\nsekali jalan.\n\n## Nasihat yang semua orang sudah tahu\n\nSolusinya *eager loading*: beri tahu ORM supaya relasinya sekalian diambil, bukan satu-satu.\n\n```python\nposts = session.execute(\n    select(Post).options(joinedload(Post.author)).limit(20)\n).scalars().all()\n```\n\nSampai di sini kamu sudah mengerjakan nasihat standarnya dengan benar. Nama penulis sekarang\nnggak lagi memicu query tambahan. Tapi cuma itu yang berubah:\n\n| versi | query per request | p50 | p95 |\n|---|---|---|---|\n| apa adanya | 41 | 950 ms | 1.152 ms |\n| setelah penulis di-*eager load* | 21 | 915 ms | 981 ms |\n\nDua puluh query berhasil kamu hapus, dan p50-nya turun **35 ms**. Sekitar 4%. Kenapa begitu\nkecil? Karena 20 query yang baru kamu hapus itu mencari berdasarkan primary key — pencarian\npaling murah yang bisa dilakukan database. Bolak-baliknya memang boros, tapi yang bolak-balik\nnggak pernah jadi biaya utamanya.\n\nDi sini kebanyakan orang berhenti dan menyimpulkan masalahnya ada di tempat lain. Padahal\nmasalahnya masih di depan mata: **ada N+1 yang kedua**, dan dia menyumbang hampir semuanya.\n\n## N+1 yang kedua\n\n20 query sisanya itu menghitung jumlah komentar untuk tiap tulisan. Ditulis seperti ini:\n\n```python\nfor post in posts:\n    post.comment_count = session.execute(\n        select(func.count())\n        .select_from(Comment)\n        .where(Comment.post_id == post.id)\n    ).scalar_one()\n```\n\nPerhatikan bahwa kode ini ditulis dengan niat baik. Dia memakai `COUNT(*)` supaya nggak perlu\nmemuat ribuan baris komentar hanya untuk menghitung jumlahnya — itu memang lebih hemat\ndaripada `len(post.comments)`. Yang tertinggal cuma satu hal: dia berjalan di dalam\nloop, jadi tetap satu query per tulisan.\n\nDan begitu masalahnya kelihatan, perbaikannya jadi kelihatan sepele juga — ambil semuanya\nsekaligus, lalu hitung di memori:\n\n```python\nposts = session.execute(\n    select(Post)\n    .options(joinedload(Post.author), selectinload(Post.comments))\n    .limit(20)\n).scalars().all()\n\nfor post in posts:\n    post.comment_count = len(post.comments)\n```\n\nQuery-nya sekarang **2**. Satu untuk daftar tulisan beserta penulisnya, satu untuk seluruh\nkomentar di halaman itu. Turun dari 41, dan itu penurunan yang pantas kamu banggakan.\n\nLatency-nya **1.026 ms**.\n\n## Dua query, satu detik\n\nItu bukan salah tulis. Di titik ini:\n\n| versi | query per request | p50 | p95 |\n|---|---|---|---|\n| apa adanya | 41 | 950 ms | 1.152 ms |\n| setelah penulis di-*eager load* | 21 | 915 ms | 981 ms |\n| setelah komentar di-*eager load*, dihitung dengan `len()` | **2** | 983 ms | 1.026 ms |\n\nJumlah query turun 95%, dan latency-nya **naik**.\n\nIni bagian yang bikin cerita N+1 hampir selalu nggak lengkap. Nasihat \"pakai eager loading\"\nitu nggak salah — tapi dia memperbaiki *cara mengambil data*, bukan *banyaknya data yang harus\nmenyeberang*. Empat puluh satu query yang murah bisa jadi masalah yang jauh lebih kecil\ndaripada dua query yang masing-masing memindahkan puluhan ribu baris dari database ke memori\naplikasi.\n\nDan yang paling nggak nyaman: kode barusan terasa seperti kode yang benar. Dia memakai *eager\nloading*, persis yang diajarkan semua artikel N+1 — termasuk artikel ini, tiga bagian di atas.\nKamu bisa membacanya tiga kali dan nggak akan menemukan kesalahannya, karena kesalahannya\nbukan di cara kamu menulisnya.\n\n## Yang belum dijawab\n\nKalau query-nya sudah 2 dan waktunya masih 1 detik, pertanyaannya harus diganti. Berhenti\nmenghitung query, lalu tanya ini:\n\n**berapa banyak baris yang harus dibaca database untuk melayani satu request ini?**\n\nJawabannya bukan 40-an baris. Jawabannya ratusan ribu, dan itu terjadi 20 kali. Sementara di\nsisi aplikasi, angka yang menyeberang ke memori juga bukan 20 — jauh lebih besar.\n\nDari mana angka-angka itu datang dan kenapa semahal itu, nggak bisa dijawab di sini. Bukan\nkarena jawabannya panjang, tapi karena kamu nggak akan memercayainya sebelum melihatnya\nsendiri di query plan.\n\n## Lab-nya: kamu yang kerjakan\n\n👉 **[inva-lab](https://github.com/izzudd/inva-lab)** — folder [`n-plus-one/`](https://github.com/izzudd/inva-lab/tree/main/n-plus-one)\n\n```bash\ngit clone https://github.com/izzudd/inva-lab.git\ncd inva-lab/n-plus-one\nmake up      # 400 post, 750.000 komentar\nmake bench   # lihat angkanya dulu, sebelum mengubah apa pun\nmake check   # target: \u003C= 3 query, p95 \u003C= 30 ms\n```\n\nYang ada di dalamnya adalah versi \"apa adanya\" dari tabel di atas — 41 query, satu detik, dan\nkode yang sengaja nggak punya kesalahan yang kelihatan. Empat puluh satu query itu bisa kamu\nhitung sendiri di setiap header response, jadi kamu bisa menebak-nebak sepuasnya sebelum\nmengubah satu baris pun.\n\nSatu hal yang perlu kamu tahu dari awal: menghilangkan N+1-nya saja nggak akan cukup untuk\nbikin `make check` hijau. Ada lapisan kedua, dan lapisan itu nggak ada di dalam kode.\n\nKalau nyangkut lebih dari 45 menit, ada `HINTS.md` dengan tiga tingkat petunjuk. Pakai urut,\njangan langsung ke yang ketiga.\n\n---\n\n**Sudah mentok dan mau lihat jawabannya?**\n[Jawabannya: Dua Perbaikan di Dua Tempat Berbeda](/p/jawaban-dua-perbaikan-di-dua-tempat-berbeda)\n",[59],{"authors_id":60},{"name":61,"slug":62,"role":63,"profile_picture":43},"Daffa Izzuddin","izzudd","Writer",{"id":65,"title":66,"slug":67,"articles":68,"parent_series":43},"29","Lab","lab",[69],{"id":44,"title":45,"slug":46,"excerpt":47,"thumbnail_image":70,"tags":71},{"id":50},[52,53,54],"\u003Cp>Satu halaman feed butuh satu detik untuk tampil. Datanya cuma 400 tulisan dan 750.000\nkomentar, dan yang dikerjakan endpoint-nya sederhana: ambil 20 tulisan terbaru, lalu tampilkan\nnama penulisnya dan jumlah komentarnya masing-masing.\u003C/p>\n\u003Cp>Tidak ada error. Tidak ada log kuning. Tidak ada satu baris pun yang kelihatan salah kalau\nkamu membacanya. Sampai ada yang mengeluh, kamu mengukur, dan ternyata database\ndihujani \u003Cstrong>41 query\u003C/strong> hanya untuk melayani satu request itu.\u003C/p>\n\u003Ch2 id=\"n-1-dan-kenapa-namanya-begitu\">N+1, dan kenapa namanya begitu\u003C/h2>\n\u003Cp>Kalau kamu sudah tahu bagian ini, lewati saja satu paragraf.\u003C/p>\n\u003Cp>Namanya N+1 karena jumlahnya: \u003Cstrong>1 query\u003C/strong> untuk mengambil daftar utamanya, lalu \u003Cstrong>N query\u003C/strong>\ntambahan untuk melengkapi tiap baris di daftar itu. Untuk 20 tulisan, hasilnya 21 query.\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"posts%20%3D%20session.execute(select(Post).limit(20)).scalars().all()%0A%0Afor%20post%20in%20posts%3A%0A%20%20%20%20print(post.author.name)%20%20%20%23%20tiap%20akses%20memicu%20query%20baru%20ke%20database\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">posts \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> session.execute(select(Post).limit(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)).scalars().all()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(post.author.name)   \u003C/span>\u003Cspan style=\"color:#6A737D\"># tiap akses memicu query baru ke database\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Penyebabnya \u003Cem>lazy loading\u003C/em>, perilaku bawaan hampir semua ORM: data relasi nggak diambil sampai\nkamu benar-benar mengaksesnya. Sekilas itu masuk akal — hemat memori, dan kamu cuma mengambil\nyang kamu butuh. Masalahnya &quot;kamu butuh&quot; ternyata terjadi di dalam loop, dan hasilnya\nbukan penghematan, tapi 20 kali bolak-balik ke database untuk sesuatu yang bisa diselesaikan\nsekali jalan.\u003C/p>\n\u003Ch2 id=\"nasihat-yang-semua-orang-sudah-tahu\">Nasihat yang semua orang sudah tahu\u003C/h2>\n\u003Cp>Solusinya \u003Cem>eager loading\u003C/em>: beri tahu ORM supaya relasinya sekalian diambil, bukan satu-satu.\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"posts%20%3D%20session.execute(%0A%20%20%20%20select(Post).options(joinedload(Post.author)).limit(20)%0A).scalars().all()\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">posts \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    select(Post).options(joinedload(Post.author)).limit(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">).scalars().all()\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Sampai di sini kamu sudah mengerjakan nasihat standarnya dengan benar. Nama penulis sekarang\nnggak lagi memicu query tambahan. Tapi cuma itu yang berubah:\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C/th>\n\u003Cth>query per request\u003C/th>\n\u003Cth>p50\u003C/th>\n\u003Cth>p95\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>apa adanya\u003C/td>\n\u003Ctd>41\u003C/td>\n\u003Ctd>950 ms\u003C/td>\n\u003Ctd>1.152 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>setelah penulis di-\u003Cem>eager load\u003C/em>\u003C/td>\n\u003Ctd>21\u003C/td>\n\u003Ctd>915 ms\u003C/td>\n\u003Ctd>981 ms\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Cp>Dua puluh query berhasil kamu hapus, dan p50-nya turun \u003Cstrong>35 ms\u003C/strong>. Sekitar 4%. Kenapa begitu\nkecil? Karena 20 query yang baru kamu hapus itu mencari berdasarkan primary key — pencarian\npaling murah yang bisa dilakukan database. Bolak-baliknya memang boros, tapi yang bolak-balik\nnggak pernah jadi biaya utamanya.\u003C/p>\n\u003Cp>Di sini kebanyakan orang berhenti dan menyimpulkan masalahnya ada di tempat lain. Padahal\nmasalahnya masih di depan mata: \u003Cstrong>ada N+1 yang kedua\u003C/strong>, dan dia menyumbang hampir semuanya.\u003C/p>\n\u003Ch2 id=\"n-1-yang-kedua\">N+1 yang kedua\u003C/h2>\n\u003Cp>20 query sisanya itu menghitung jumlah komentar untuk tiap tulisan. Ditulis seperti ini:\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"for%20post%20in%20posts%3A%0A%20%20%20%20post.comment_count%20%3D%20session.execute(%0A%20%20%20%20%20%20%20%20select(func.count())%0A%20%20%20%20%20%20%20%20.select_from(Comment)%0A%20%20%20%20%20%20%20%20.where(Comment.post_id%20%3D%3D%20post.id)%0A%20%20%20%20).scalar_one()\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    post.comment_count \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        select(func.count())\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .select_from(Comment)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .where(Comment.post_id \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post.id)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).scalar_one()\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Perhatikan bahwa kode ini ditulis dengan niat baik. Dia memakai \u003Ccode>COUNT(*)\u003C/code> supaya nggak perlu\nmemuat ribuan baris komentar hanya untuk menghitung jumlahnya — itu memang lebih hemat\ndaripada \u003Ccode>len(post.comments)\u003C/code>. Yang tertinggal cuma satu hal: dia berjalan di dalam\nloop, jadi tetap satu query per tulisan.\u003C/p>\n\u003Cp>Dan begitu masalahnya kelihatan, perbaikannya jadi kelihatan sepele juga — ambil semuanya\nsekaligus, lalu hitung di memori:\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"posts%20%3D%20session.execute(%0A%20%20%20%20select(Post)%0A%20%20%20%20.options(joinedload(Post.author)%2C%20selectinload(Post.comments))%0A%20%20%20%20.limit(20)%0A).scalars().all()%0A%0Afor%20post%20in%20posts%3A%0A%20%20%20%20post.comment_count%20%3D%20len(post.comments)\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">posts \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    select(Post)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .options(joinedload(Post.author), selectinload(Post.comments))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .limit(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">).scalars().all()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    post.comment_count \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(post.comments)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Query-nya sekarang \u003Cstrong>2\u003C/strong>. Satu untuk daftar tulisan beserta penulisnya, satu untuk seluruh\nkomentar di halaman itu. Turun dari 41, dan itu penurunan yang pantas kamu banggakan.\u003C/p>\n\u003Cp>Latency-nya \u003Cstrong>1.026 ms\u003C/strong>.\u003C/p>\n\u003Ch2 id=\"dua-query-satu-detik\">Dua query, satu detik\u003C/h2>\n\u003Cp>Itu bukan salah tulis. Di titik ini:\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C/th>\n\u003Cth>query per request\u003C/th>\n\u003Cth>p50\u003C/th>\n\u003Cth>p95\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>apa adanya\u003C/td>\n\u003Ctd>41\u003C/td>\n\u003Ctd>950 ms\u003C/td>\n\u003Ctd>1.152 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>setelah penulis di-\u003Cem>eager load\u003C/em>\u003C/td>\n\u003Ctd>21\u003C/td>\n\u003Ctd>915 ms\u003C/td>\n\u003Ctd>981 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>setelah komentar di-\u003Cem>eager load\u003C/em>, dihitung dengan \u003Ccode>len()\u003C/code>\u003C/td>\n\u003Ctd>\u003Cstrong>2\u003C/strong>\u003C/td>\n\u003Ctd>983 ms\u003C/td>\n\u003Ctd>1.026 ms\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Cp>Jumlah query turun 95%, dan latency-nya \u003Cstrong>naik\u003C/strong>.\u003C/p>\n\u003Cp>Ini bagian yang bikin cerita N+1 hampir selalu nggak lengkap. Nasihat &quot;pakai eager loading&quot;\nitu nggak salah — tapi dia memperbaiki \u003Cem>cara mengambil data\u003C/em>, bukan \u003Cem>banyaknya data yang harus\nmenyeberang\u003C/em>. Empat puluh satu query yang murah bisa jadi masalah yang jauh lebih kecil\ndaripada dua query yang masing-masing memindahkan puluhan ribu baris dari database ke memori\naplikasi.\u003C/p>\n\u003Cp>Dan yang paling nggak nyaman: kode barusan terasa seperti kode yang benar. Dia memakai \u003Cem>eager\nloading\u003C/em>, persis yang diajarkan semua artikel N+1 — termasuk artikel ini, tiga bagian di atas.\nKamu bisa membacanya tiga kali dan nggak akan menemukan kesalahannya, karena kesalahannya\nbukan di cara kamu menulisnya.\u003C/p>\n\u003Ch2 id=\"yang-belum-dijawab\">Yang belum dijawab\u003C/h2>\n\u003Cp>Kalau query-nya sudah 2 dan waktunya masih 1 detik, pertanyaannya harus diganti. Berhenti\nmenghitung query, lalu tanya ini:\u003C/p>\n\u003Cp>\u003Cstrong>berapa banyak baris yang harus dibaca database untuk melayani satu request ini?\u003C/strong>\u003C/p>\n\u003Cp>Jawabannya bukan 40-an baris. Jawabannya ratusan ribu, dan itu terjadi 20 kali. Sementara di\nsisi aplikasi, angka yang menyeberang ke memori juga bukan 20 — jauh lebih besar.\u003C/p>\n\u003Cp>Dari mana angka-angka itu datang dan kenapa semahal itu, nggak bisa dijawab di sini. Bukan\nkarena jawabannya panjang, tapi karena kamu nggak akan memercayainya sebelum melihatnya\nsendiri di query plan.\u003C/p>\n\u003Ch2 id=\"lab-nya-kamu-yang-kerjakan\">Lab-nya: kamu yang kerjakan\u003C/h2>\n\u003Cp>👉 \u003Cstrong>\u003Ca href=\"https://github.com/izzudd/inva-lab\">inva-lab\u003C/a>\u003C/strong> — folder \u003Ca href=\"https://github.com/izzudd/inva-lab/tree/main/n-plus-one\">\u003Ccode>n-plus-one/\u003C/code>\u003C/a>\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">BASH\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"git%20clone%20https%3A%2F%2Fgithub.com%2Fizzudd%2Finva-lab.git%0Acd%20inva-lab%2Fn-plus-one%0Amake%20up%20%20%20%20%20%20%23%20400%20post%2C%20750.000%20komentar%0Amake%20bench%20%20%20%23%20lihat%20angkanya%20dulu%2C%20sebelum%20mengubah%20apa%20pun%0Amake%20check%20%20%20%23%20target%3A%20%3C%3D%203%20query%2C%20p95%20%3C%3D%2030%20ms\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">git\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> clone\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://github.com/izzudd/inva-lab.git\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">cd\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> inva-lab/n-plus-one\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> up\u003C/span>\u003Cspan style=\"color:#6A737D\">      # 400 post, 750.000 komentar\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> bench\u003C/span>\u003Cspan style=\"color:#6A737D\">   # lihat angkanya dulu, sebelum mengubah apa pun\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> check\u003C/span>\u003Cspan style=\"color:#6A737D\">   # target: &#x3C;= 3 query, p95 &#x3C;= 30 ms\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Yang ada di dalamnya adalah versi &quot;apa adanya&quot; dari tabel di atas — 41 query, satu detik, dan\nkode yang sengaja nggak punya kesalahan yang kelihatan. Empat puluh satu query itu bisa kamu\nhitung sendiri di setiap header response, jadi kamu bisa menebak-nebak sepuasnya sebelum\nmengubah satu baris pun.\u003C/p>\n\u003Cp>Satu hal yang perlu kamu tahu dari awal: menghilangkan N+1-nya saja nggak akan cukup untuk\nbikin \u003Ccode>make check\u003C/code> hijau. Ada lapisan kedua, dan lapisan itu nggak ada di dalam kode.\u003C/p>\n\u003Cp>Kalau nyangkut lebih dari 45 menit, ada \u003Ccode>HINTS.md\u003C/code> dengan tiga tingkat petunjuk. Pakai urut,\njangan langsung ke yang ketiga.\u003C/p>\n\u003Chr>\n\u003Cp>\u003Cstrong>Sudah mentok dan mau lihat jawabannya?\u003C/strong>\n\u003Ca href=\"/p/jawaban-dua-perbaikan-di-dua-tempat-berbeda\">Jawabannya: Dua Perbaikan di Dua Tempat Berbeda\u003C/a>\u003C/p>\n","\u003Cul>\n\u003Cli>N+1 itu 1 query untuk mengambil daftar, lalu N query tambahan untuk melengkapi tiap barisnya.\u003C/li>\n\u003Cli>Nasihat &quot;pakai eager loading&quot; nggak salah, tapi dia memperbaiki cara mengambil data - bukan banyaknya data yang harus menyeberang.\u003C/li>\n\u003Cli>Jumlah query itu metrik yang gampang dilihat, dan itu sebabnya dia jadi satu-satunya yang dilihat. Dua kelompok query bisa berbeda biaya ratusan kali lipat.\u003C/li>\n\u003Cli>Kasus lengkapnya bisa kamu kerjakan sendiri di lab: make up, lalu make check.\u003C/li>\n\u003C/ul>\n",[75,78,91,105],{"id":44,"title":45,"slug":46,"excerpt":47,"thumbnail_image":76,"tags":77},{"id":50},[52,53,54],{"id":79,"title":80,"slug":81,"excerpt":82,"thumbnail_image":83,"tags":85},"46","Mengapa C Mulai Ditinggalkan: Duel Filosofi Keamanan Rust vs Kesederhanaan Zig","mengapa-c-mulai-ditinggalkan-duel-filosofi-rust-vs-zig","Pernah denger kalau sekitar 70% celah keamanan kritis software modern itu asalnya dari bahasa C? Iya, bahasa yang udah jadi fondasi internet ini 😅. Yuk, kita bedah duel filosofi Rust vs Zig dan cari tahu mana yang pas buat proyek kalian!",{"id":84},"1942a5c9-c1f6-476d-9913-768f78d9d8eb",[86,87,88,89,90],"system-programming","rust","zig","c","memory-safety",{"id":92,"title":93,"slug":94,"excerpt":95,"thumbnail_image":96,"tags":98},"45","Rate Limiting Algorithms: Token Bucket vs Leaky Bucket vs Sliding Window","rate-limiting-algorithms-token-bucket-leaky-bucket-sliding-window","Udah tahu kenapa *rate limiting* itu wajib, tapi masih bingung beda Token Bucket, Leaky Bucket, Fixed Window, dan Sliding Window? Yuk, kita adu empat algoritma ini bareng-bareng dan cari tahu mana yang paling pas buat API kalian!",{"id":97},"0e0ed26d-20c6-443c-bde1-4a6200f6f801",[54,99,100,101,102,103,104],"system-design","rate-limiting","redis","api-gateway","architecture","traffict-management",{"id":106,"title":107,"slug":108,"excerpt":109,"thumbnail_image":110,"tags":112},"44","Transactional Outbox Pattern: Solusi Konsistensi Database dan Message Broker Tanpa 2PC","transactional-outbox-pattern-konsistensi-database-message-broker","Pernah nggak kalian lihat pesanan udah masuk database, tapi email konfirmasinya nggak pernah kekirim? Itu gejala *dual-write problem*. Yuk, kita bedah Transactional Outbox Pattern biar data dan *event* kalian nggak lagi berselisih paham!",{"id":111},"e3c617fd-1486-4b82-b937-94fbec846c77",[54,113,103,52,114,99,115],"microservices","message-broker","postgresql",[117,121,124,127,130,133],{"text":118,"depth":119,"id":120},"N+1, dan kenapa namanya begitu",2,"n-1-dan-kenapa-namanya-begitu",{"text":122,"depth":119,"id":123},"Nasihat yang semua orang sudah tahu","nasihat-yang-semua-orang-sudah-tahu",{"text":125,"depth":119,"id":126},"N+1 yang kedua","n-1-yang-kedua",{"text":128,"depth":119,"id":129},"Dua query, satu detik","dua-query-satu-detik",{"text":131,"depth":119,"id":132},"Yang belum dijawab","yang-belum-dijawab",{"text":134,"depth":119,"id":135},"Lab-nya: kamu yang kerjakan","lab-nya-kamu-yang-kerjakan",1791110589229]