[{"data":1,"prerenderedAt":294},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-query-tinggal-2-tapi-response-masih-1-detik-id":69},{},{"featuredPost":5,"navigations":26},{"id":6,"title":7,"title_en":8,"slug":9,"excerpt":10,"excerpt_en":8,"has_translation":11,"published_at":12,"reading_time":13,"hidden_from_listing":11,"tags":14,"thumbnail_image":8,"og_image":19,"series":20,"authors":25},110,"Ketika Pilih Framework Cuma Biar Kelihatan Beda",null,"ketika-pilih-framework-cuma-biar-kelihatan-beda","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",false,"2026-10-06T15:25:21.000Z",5,[15,16,17,18],"karier","framework","opini","pengalaman","\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":21,"title":22,"title_en":23,"slug":24},9,"Artikel","Articles","all",[],[27,43,64],{"collection":28,"item":29},"series",{"__typename":28,"title":22,"title_en":23,"slug":24,"children_series":30},[31,35,39],{"title":32,"title_en":33,"slug":34},"Kecerdasan Artifisial","Artificial Intelligence","kecerdasan-artifisial",{"title":36,"title_en":37,"slug":38},"Konsep Dasar Pemrograman","Programming Fundamentals","konsep-dasar-pemrograman",{"title":40,"title_en":41,"slug":42},"Algoritma dan Struktur Data","Algorithms and Data Structures","algoritma-struktur-data",{"collection":28,"item":44},{"__typename":28,"title":45,"title_en":45,"slug":46,"children_series":47},"Tutorial","tutorial",[48,52,56,60],{"title":49,"title_en":50,"slug":51},"Belajar Python","Learning Python","belajar-python",{"title":53,"title_en":54,"slug":55},"Belajar Golang","Learning Go","belajar-golang",{"title":57,"title_en":58,"slug":59},"Belajar Java","Learning Java","belajar-java",{"title":61,"title_en":62,"slug":63},"Belajar Pascal","Learning Pascal","belajar-pascal",{"collection":28,"item":65},{"__typename":28,"title":66,"title_en":66,"slug":67,"children_series":68},"Lab","lab",[],{"article":70,"renderedBody":173,"renderedTakeaway":174,"latestArticles":175,"toc":274},{"id":71,"title":72,"title_en":73,"slug":74,"excerpt":75,"excerpt_en":76,"has_translation":77,"published_at":78,"reading_time":13,"hidden_from_listing":11,"tags":79,"thumbnail_image":8,"og_image":83,"series":84,"authors":165,"body_locale":168,"available_locales":169,"body":171,"takeway":172,"prerequisite":8},104,"Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","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.","A feed endpoint that shows 20 posts takes a full second and hits the database with 41 queries. Apply the standard N+1 advice, get it down to 2 queries, and the latency does not move. This is that problem as it happened, plus the lab.",true,"2026-10-04T09:59:21.000Z",[80,81,82],"database","performance","backend","\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":85,"title":66,"title_en":66,"slug":67,"description":86,"description_en":87,"articles":88,"children_series":164,"parent_series":8},29,"Lab praktik: kerjakan masalahnya dulu, baru lihat jawabannya. Tiap lab punya repo yang bisa di-clone, data seukuran produksi, dan target yang harus kamu capai sendiri. Artikel jawabannya ada di seri ini juga, dan cuma bisa kamu buka dari artikel masalahnya.","Practice labs: work the problem first, then read the answer. Every lab ships with a repo you can clone, production-sized data, and a target you have to hit yourself. The write-up lives in this series too, and it only opens from the problem article.",[89,98,116,132,149],{"id":71,"title":72,"title_en":73,"slug":74,"excerpt":75,"excerpt_en":76,"has_translation":77,"published_at":78,"reading_time":13,"hidden_from_listing":11,"tags":79,"thumbnail_image":8,"og_image":83,"series":90,"authors":91},{"id":85,"title":66,"title_en":66,"slug":67},[92],{"authors_id":93},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},1,"Daffa Izzuddin","izzudd","Writer",{"id":99,"title":100,"title_en":101,"slug":102,"excerpt":103,"excerpt_en":104,"has_translation":77,"published_at":105,"reading_time":106,"hidden_from_listing":11,"tags":107,"thumbnail_image":8,"og_image":111,"series":112,"authors":113},114,"Saldo Naik Empat Kali untuk Satu Pembayaran","One Payment, Four Ledger Rows: An Idempotency Lab","saldo-naik-empat-kali-untuk-satu-pembayaran","Kodenya nggak salah, dan gateway-nya juga nggak salah. Tapi satu pembayaran muncul empat kali di ledger. Coba tebak kenapa — lalu bikin `make check` hijau di lab-nya.","The code is not wrong and the gateway is not wrong, yet one payment shows up four times in the ledger. Guess why, then make `make check` green in the lab.","2026-10-11T13:45:00.000Z",4,[108,109,110,80,67],"idempotency","webhook","postgres","\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":85,"title":66,"title_en":66,"slug":67},[114],{"authors_id":115},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":117,"title":118,"title_en":119,"slug":120,"excerpt":121,"excerpt_en":122,"has_translation":77,"published_at":123,"reading_time":124,"hidden_from_listing":11,"tags":125,"thumbnail_image":8,"og_image":127,"series":128,"authors":129},116,"Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","The Webhook Arrived Late. The Reconciliation Job Had Already Run.","webhook-telat-job-rekonsiliasi-keburu-jalan","Jalur webhook sudah idempotent. Lalu ada job rekonsiliasi pagi, dan satu pembayaran tercatat dua kali dari dua jalur yang dua-duanya benar. Coba tebak kuncinya salah di mana.","The webhook path is already idempotent. Then a morning reconciliation job appears, and one payment gets recorded twice from two paths that are both right. Guess where the key is wrong.","2026-10-11T13:44:00.000Z",3,[108,126,109,110,67],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":85,"title":66,"title_en":66,"slug":67},[130],{"authors_id":131},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":133,"title":134,"title_en":135,"slug":136,"excerpt":137,"excerpt_en":138,"has_translation":77,"published_at":139,"reading_time":106,"hidden_from_listing":11,"tags":140,"thumbnail_image":8,"og_image":144,"series":145,"authors":146},120,"Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","Every Receipt in This Café Lists the Same Order","tiap-nota-isinya-pesanan-yang-sama","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","Every table's receipt holds the same items, and the total is Rp0 — even though each table ordered exactly one thing. Guess why, then make `make check` green. A Python scope and mutation lab.","2026-10-11T23:00:15.000Z",[141,142,143,67],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":85,"title":66,"title_en":66,"slug":67},[147],{"authors_id":148},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":150,"title":151,"title_en":152,"slug":153,"excerpt":154,"excerpt_en":155,"has_translation":77,"published_at":139,"reading_time":106,"hidden_from_listing":11,"tags":156,"thumbnail_image":8,"og_image":159,"series":160,"authors":161},122,"Script yang Nggak Bisa Bilang di Mana Salahnya","A Script That Cannot Say Where It Went Wrong","script-yang-tidak-bilang-di-mana-salahnya","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.",[141,157,158,67],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":85,"title":66,"title_en":66,"slug":67},[162],{"authors_id":163},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},[],[166],{"authors_id":167},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},"id",[170,168],"en","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:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab)** — folder [`0001.n-plus-one\u002F`](https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0001.n-plus-one)\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab.git\ncd inva-lab\u002F0001.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](\u002Fp\u002Fjawaban-dua-perbaikan-di-dua-tempat-berbeda)\n","- 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.","\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\u002Fp>\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\u002Fstrong> hanya untuk melayani satu request itu.\u003C\u002Fp>\n\u003Ch2 id=\"n-1-dan-kenapa-namanya-begitu\">N+1, dan kenapa namanya begitu\u003C\u002Fh2>\n\u003Cp>Kalau kamu sudah tahu bagian ini, lewati saja satu paragraf.\u003C\u002Fp>\n\u003Cp>Namanya N+1 karena jumlahnya: \u003Cstrong>1 query\u003C\u002Fstrong> untuk mengambil daftar utamanya, lalu \u003Cstrong>N query\u003C\u002Fstrong>\ntambahan untuk melengkapi tiap baris di daftar itu. Untuk 20 tulisan, hasilnya 21 query.\u003C\u002Fp>\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\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\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\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\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\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session.execute(select(Post).limit(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">20\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)).scalars().all()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(post.author.name)   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># tiap akses memicu query baru ke database\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Penyebabnya \u003Cem>lazy loading\u003C\u002Fem>, 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\u002Fp>\n\u003Ch2 id=\"nasihat-yang-semua-orang-sudah-tahu\">Nasihat yang semua orang sudah tahu\u003C\u002Fh2>\n\u003Cp>Solusinya \u003Cem>eager loading\u003C\u002Fem>: beri tahu ORM supaya relasinya sekalian diambil, bukan satu-satu.\u003C\u002Fp>\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\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\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\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\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\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    select(Post).options(joinedload(Post.author)).limit(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">20\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">).scalars().all()\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\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\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C\u002Fth>\n\u003Cth>query per request\u003C\u002Fth>\n\u003Cth>p50\u003C\u002Fth>\n\u003Cth>p95\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>apa adanya\u003C\u002Ftd>\n\u003Ctd>41\u003C\u002Ftd>\n\u003Ctd>950 ms\u003C\u002Ftd>\n\u003Ctd>1.152 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>setelah penulis di-\u003Cem>eager load\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>21\u003C\u002Ftd>\n\u003Ctd>915 ms\u003C\u002Ftd>\n\u003Ctd>981 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Dua puluh query berhasil kamu hapus, dan p50-nya turun \u003Cstrong>35 ms\u003C\u002Fstrong>. 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\u002Fp>\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\u002Fstrong>, dan dia menyumbang hampir semuanya.\u003C\u002Fp>\n\u003Ch2 id=\"n-1-yang-kedua\">N+1 yang kedua\u003C\u002Fh2>\n\u003Cp>20 query sisanya itu menghitung jumlah komentar untuk tiap tulisan. Ditulis seperti ini:\u003C\u002Fp>\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\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\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\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\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\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    post.comment_count \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        select(func.count())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .select_from(Comment)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .where(Comment.post_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post.id)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).scalar_one()\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Perhatikan bahwa kode ini ditulis dengan niat baik. Dia memakai \u003Ccode>COUNT(*)\u003C\u002Fcode> supaya nggak perlu\nmemuat ribuan baris komentar hanya untuk menghitung jumlahnya — itu memang lebih hemat\ndaripada \u003Ccode>len(post.comments)\u003C\u002Fcode>. Yang tertinggal cuma satu hal: dia berjalan di dalam\nloop, jadi tetap satu query per tulisan.\u003C\u002Fp>\n\u003Cp>Dan begitu masalahnya kelihatan, perbaikannya jadi kelihatan sepele juga — ambil semuanya\nsekaligus, lalu hitung di memori:\u003C\u002Fp>\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\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\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\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\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\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> session.execute(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    select(Post)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .options(joinedload(Post.author), selectinload(Post.comments))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .limit(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">20\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">).scalars().all()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    post.comment_count \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(post.comments)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Query-nya sekarang \u003Cstrong>2\u003C\u002Fstrong>. Satu untuk daftar tulisan beserta penulisnya, satu untuk seluruh\nkomentar di halaman itu. Turun dari 41, dan itu penurunan yang pantas kamu banggakan.\u003C\u002Fp>\n\u003Cp>Latency-nya \u003Cstrong>1.026 ms\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Ch2 id=\"dua-query-satu-detik\">Dua query, satu detik\u003C\u002Fh2>\n\u003Cp>Itu bukan salah tulis. Di titik ini:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C\u002Fth>\n\u003Cth>query per request\u003C\u002Fth>\n\u003Cth>p50\u003C\u002Fth>\n\u003Cth>p95\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>apa adanya\u003C\u002Ftd>\n\u003Ctd>41\u003C\u002Ftd>\n\u003Ctd>950 ms\u003C\u002Ftd>\n\u003Ctd>1.152 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>setelah penulis di-\u003Cem>eager load\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>21\u003C\u002Ftd>\n\u003Ctd>915 ms\u003C\u002Ftd>\n\u003Ctd>981 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>setelah komentar di-\u003Cem>eager load\u003C\u002Fem>, dihitung dengan \u003Ccode>len()\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>2\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>983 ms\u003C\u002Ftd>\n\u003Ctd>1.026 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Jumlah query turun 95%, dan latency-nya \u003Cstrong>naik\u003C\u002Fstrong>.\u003C\u002Fp>\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\u002Fem>, bukan \u003Cem>banyaknya data yang harus\nmenyeberang\u003C\u002Fem>. 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\u002Fp>\n\u003Cp>Dan yang paling nggak nyaman: kode barusan terasa seperti kode yang benar. Dia memakai \u003Cem>eager\nloading\u003C\u002Fem>, 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\u002Fp>\n\u003Ch2 id=\"yang-belum-dijawab\">Yang belum dijawab\u003C\u002Fh2>\n\u003Cp>Kalau query-nya sudah 2 dan waktunya masih 1 detik, pertanyaannya harus diganti. Berhenti\nmenghitung query, lalu tanya ini:\u003C\u002Fp>\n\u003Cp>\u003Cstrong>berapa banyak baris yang harus dibaca database untuk melayani satu request ini?\u003C\u002Fstrong>\u003C\u002Fp>\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\u002Fp>\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\u002Fp>\n\u003Ch2 id=\"lab-nya-kamu-yang-kerjakan\">Lab-nya: kamu yang kerjakan\u003C\u002Fh2>\n\u003Cp>👉 \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\">inva-lab\u003C\u002Fa>\u003C\u002Fstrong> — folder \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0001.n-plus-one\">\u003Ccode>0001.n-plus-one\u002F\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Fp>\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\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">BASH\u003C\u002Fspan>\n    \u003C\u002Fdiv>\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%2F0001.n-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\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\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\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> clone\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab.git\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">cd\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> inva-lab\u002F0001.n-plus-one\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> up\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">      # 400 post, 750.000 komentar\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> bench\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">   # lihat angkanya dulu, sebelum mengubah apa pun\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> check\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">   # target: &#x3C;= 3 query, p95 &#x3C;= 30 ms\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\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\u002Fp>\n\u003Cp>Satu hal yang perlu kamu tahu dari awal: menghilangkan N+1-nya saja nggak akan cukup untuk\nbikin \u003Ccode>make check\u003C\u002Fcode> hijau. Ada lapisan kedua, dan lapisan itu nggak ada di dalam kode.\u003C\u002Fp>\n\u003Cp>Kalau nyangkut lebih dari 45 menit, ada \u003Ccode>HINTS.md\u003C\u002Fcode> dengan tiga tingkat petunjuk. Pakai urut,\njangan langsung ke yang ketiga.\u003C\u002Fp>\n\u003Chr>\n\u003Cp>\u003Cstrong>Sudah mentok dan mau lihat jawabannya?\u003C\u002Fstrong>\n\u003Ca href=\"\u002Fp\u002Fjawaban-dua-perbaikan-di-dua-tempat-berbeda\">Jawabannya: Dua Perbaikan di Dua Tempat Berbeda\u003C\u002Fa>\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>N+1 itu 1 query untuk mengambil daftar, lalu N query tambahan untuk melengkapi tiap barisnya.\u003C\u002Fli>\n\u003Cli>Nasihat &quot;pakai eager loading&quot; nggak salah, tapi dia memperbaiki cara mengambil data - bukan banyaknya data yang harus menyeberang.\u003C\u002Fli>\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\u002Fli>\n\u003Cli>Kasus lengkapnya bisa kamu kerjakan sendiri di lab: make up, lalu make check.\u003C\u002Fli>\n\u003C\u002Ful>\n",[176,194,207,220,239,257],{"id":177,"title":178,"title_en":8,"slug":179,"excerpt":180,"excerpt_en":8,"has_translation":11,"published_at":181,"reading_time":182,"hidden_from_listing":11,"tags":183,"thumbnail_image":8,"og_image":185,"series":186,"authors":191},132,"Python Tuples: Why All Three Points Are the Same Point","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","2026-10-28T12:00:00.000Z",6,[141,184],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":187,"title":188,"title_en":189,"slug":190},30,"Koleksi dan String","Collections and Strings","python-collections",[192],{"authors_id":193},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":195,"title":196,"title_en":8,"slug":197,"excerpt":198,"excerpt_en":8,"has_translation":11,"published_at":199,"reading_time":182,"hidden_from_listing":11,"tags":200,"thumbnail_image":8,"og_image":202,"series":203,"authors":204},131,"Python Sets: the Duplicate That Keeps Coming Back","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","2026-10-28T11:00:00.000Z",[141,201],"set","\u002Fog\u002Fpython-sets.png",{"id":187,"title":188,"title_en":189,"slug":190},[205],{"authors_id":206},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":208,"title":209,"title_en":8,"slug":210,"excerpt":211,"excerpt_en":8,"has_translation":11,"published_at":212,"reading_time":182,"hidden_from_listing":11,"tags":213,"thumbnail_image":8,"og_image":215,"series":216,"authors":217},130,"Python Dictionaries: the Key That Isn't There","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","2026-10-28T10:00:00.000Z",[141,214],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":187,"title":188,"title_en":189,"slug":190},[218],{"authors_id":219},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":221,"title":222,"title_en":8,"slug":223,"excerpt":224,"excerpt_en":8,"has_translation":11,"published_at":225,"reading_time":13,"hidden_from_listing":11,"tags":226,"thumbnail_image":8,"og_image":233,"series":234,"authors":236},129,"Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","2026-10-22T01:30:00.000Z",[227,228,229,230,231,232],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":235,"title":32,"title_en":33,"slug":34},8,[237],{"authors_id":238},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":240,"title":241,"title_en":8,"slug":242,"excerpt":243,"excerpt_en":8,"has_translation":11,"published_at":244,"reading_time":182,"hidden_from_listing":11,"tags":245,"thumbnail_image":8,"og_image":252,"series":253,"authors":254},128,"Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","2026-10-21T16:05:00.000Z",[246,247,248,249,250,251],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":235,"title":32,"title_en":33,"slug":34},[255],{"authors_id":256},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},{"id":258,"title":259,"title_en":8,"slug":260,"excerpt":261,"excerpt_en":8,"has_translation":11,"published_at":262,"reading_time":182,"hidden_from_listing":11,"tags":263,"thumbnail_image":8,"og_image":269,"series":270,"authors":271},127,"Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",[227,264,265,266,267,268],"vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":235,"title":32,"title_en":33,"slug":34},[272],{"authors_id":273},{"id":94,"name":95,"slug":96,"role":97,"profile_picture":8},[275,279,282,285,288,291],{"text":276,"depth":277,"id":278},"N+1, dan kenapa namanya begitu",2,"n-1-dan-kenapa-namanya-begitu",{"text":280,"depth":277,"id":281},"Nasihat yang semua orang sudah tahu","nasihat-yang-semua-orang-sudah-tahu",{"text":283,"depth":277,"id":284},"N+1 yang kedua","n-1-yang-kedua",{"text":286,"depth":277,"id":287},"Dua query, satu detik","dua-query-satu-detik",{"text":289,"depth":277,"id":290},"Yang belum dijawab","yang-belum-dijawab",{"text":292,"depth":277,"id":293},"Lab-nya: kamu yang kerjakan","lab-nya-kamu-yang-kerjakan",1793246467047]