[{"data":1,"prerenderedAt":308},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-jawaban-dua-perbaikan-di-dua-tempat-berbeda-id":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_id":8,"slug":9,"excerpt":10,"excerpt_id":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"When You Pick a Framework Just to Look Different","Ketika Pilih Framework Cuma Biar Kelihatan Beda","ketika-pilih-framework-cuma-biar-kelihatan-beda","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.","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.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_id":26,"slug":27},9,"Articles","Artikel","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_id":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_id":36,"slug":37},"Artificial Intelligence","Kecerdasan Artifisial","kecerdasan-artifisial",{"title":39,"title_id":40,"slug":41},"Programming Fundamentals","Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":43,"title_id":44,"slug":45},"Algorithms and Data Structures","Algoritma dan Struktur Data","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_id":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_id":53,"slug":54},"Learning Python","Belajar Python","belajar-python",{"title":56,"title_id":57,"slug":58},"Learning Go","Belajar Golang","belajar-golang",{"title":60,"title_id":61,"slug":62},"Learning Java","Belajar Java","belajar-java",{"title":64,"title_id":65,"slug":66},"Learning Pascal","Belajar Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_id":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":184,"renderedTakeaway":185,"latestArticles":186,"toc":285},{"id":74,"title":75,"title_id":75,"slug":76,"excerpt":77,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":14,"hidden_from_listing":12,"tags":79,"thumbnail_image":21,"og_image":84,"series":85,"authors":175,"body_locale":178,"available_locales":179,"body":181,"takeway":182,"prerequisite":183},105,"Jawabannya: Dua Perbaikan di Dua Tempat Berbeda","jawaban-dua-perbaikan-di-dua-tempat-berbeda","Query-nya sudah 2, kodenya sudah benar, dan latency-nya masih 1 detik. Jawabannya ada di dua tempat berbeda, dan cuma satu yang kelihatan di dalam kode.","2026-10-04T10:00:50.000Z",[80,81,82,83],"database","performance","backend","sqlalchemy","\u002Fog\u002Fjawaban-dua-perbaikan-di-dua-tempat-berbeda.png",{"id":86,"title":69,"title_id":69,"slug":70,"description":87,"description_id":88,"articles":89,"children_series":174,"parent_series":21},29,"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.","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.",[90,108,126,142,159],{"id":91,"title":92,"title_id":93,"slug":94,"excerpt":95,"excerpt_id":96,"has_translation":12,"published_at":97,"reading_time":14,"hidden_from_listing":15,"tags":98,"thumbnail_image":21,"og_image":99,"series":100,"authors":101},104,"N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","query-tinggal-2-tapi-response-masih-1-detik","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.","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",[80,81,82],"\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":86,"title":69,"title_id":69,"slug":70},[102],{"authors_id":103},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},1,"Daffa Izzuddin","izzudd","Writer",{"id":109,"title":110,"title_id":111,"slug":112,"excerpt":113,"excerpt_id":114,"has_translation":12,"published_at":115,"reading_time":116,"hidden_from_listing":15,"tags":117,"thumbnail_image":21,"og_image":121,"series":122,"authors":123},114,"One Payment, Four Ledger Rows: An Idempotency Lab","Saldo Naik Empat Kali untuk Satu Pembayaran","saldo-naik-empat-kali-untuk-satu-pembayaran","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.","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.","2026-10-11T13:45:00.000Z",4,[118,119,120,80,70],"idempotency","webhook","postgres","\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":86,"title":69,"title_id":69,"slug":70},[124],{"authors_id":125},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":127,"title":128,"title_id":129,"slug":130,"excerpt":131,"excerpt_id":132,"has_translation":12,"published_at":133,"reading_time":134,"hidden_from_listing":15,"tags":135,"thumbnail_image":21,"og_image":137,"series":138,"authors":139},116,"The Webhook Arrived Late. The Reconciliation Job Had Already Run.","Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","webhook-telat-job-rekonsiliasi-keburu-jalan","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.","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.","2026-10-11T13:44:00.000Z",3,[118,136,119,120,70],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":86,"title":69,"title_id":69,"slug":70},[140],{"authors_id":141},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":143,"title":144,"title_id":145,"slug":146,"excerpt":147,"excerpt_id":148,"has_translation":12,"published_at":149,"reading_time":116,"hidden_from_listing":15,"tags":150,"thumbnail_image":21,"og_image":154,"series":155,"authors":156},120,"Every Receipt in This Café Lists the Same Order","Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","tiap-nota-isinya-pesanan-yang-sama","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.","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","2026-10-11T23:00:15.000Z",[151,152,153,70],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":86,"title":69,"title_id":69,"slug":70},[157],{"authors_id":158},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":160,"title":161,"title_id":162,"slug":163,"excerpt":164,"excerpt_id":165,"has_translation":12,"published_at":149,"reading_time":116,"hidden_from_listing":15,"tags":166,"thumbnail_image":21,"og_image":169,"series":170,"authors":171},122,"A Script That Cannot Say Where It Went Wrong","Script yang Nggak Bisa Bilang di Mana Salahnya","script-yang-tidak-bilang-di-mana-salahnya","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.",[151,167,168,70],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":86,"title":69,"title_id":69,"slug":70},[172],{"authors_id":173},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},[],[176],{"authors_id":177},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},"id",[180,178],"en","\n**Ini artikel jawabannya.** Kalau kamu belum mengerjakan lab-nya, mendingan tutup dulu halaman\nini dan mulai dari [Query-nya Tinggal 2, Tapi Response-nya Masih 1\nDetik](\u002Fp\u002Fquery-tinggal-2-tapi-response-masih-1-detik). Lab-nya ada di\n[inva-lab](https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0001.n-plus-one) — `make up`, `make check`,\ndan targetnya `\u003C= 3` query dengan p95 `\u003C= 30 ms`.\n\nSisanya di bawah ini adalah jawabannya.\n\n## Angka lengkapnya\n\nEnam varian dari endpoint yang sama, di dataset yang sama (400 tulisan, 750.000 komentar,\n20 tulisan per halaman). Semua diukur di mesin yang sama, dan **setiap varian mengembalikan\nbody response yang identik** — yang berubah cuma performanya.\n\n| versi | query | p50 | p95 |\n|---|---|---|---|\n| apa adanya | 41 | 950 ms | 1.152 ms |\n| setelah penulis di-*eager load* | 21 | 915 ms | 981 ms |\n| jebakan: komentar di-*eager load*, dihitung `len()` | 2 | 983 ms | 1.026 ms |\n| jebakan + index | 2 | 949 ms | 1.002 ms |\n| komentar dihitung sebagai satu agregat | 2 | 60 ms | 62 ms |\n| agregat + index | 2 | **7 ms** | **8 ms** |\n\nTiga baris pertama sudah kamu lihat di artikel sebelumnya. Tiga baris terakhir itu jawabannya,\ndan perhatikan baris keempat — itu bagian yang paling gampang dilewatkan.\n\n## Perbaikan pertama: hitung, jangan pindahkan\n\nKesalahannya bukan memakai *eager loading*. Silakan pakai *eager loading* sampai kapan pun\nuntuk data yang memang mau kamu tampilkan.\n\nKesalahannya adalah memakai `len()` untuk **menghitung**. Supaya bisa memanggil `len()`, semua\nbarisnya harus ada dulu di memori aplikasi. Padahal kamu nggak butuh barisnya sama sekali —\nkamu butuh satu angka per tulisan.\n\nJadi jangan ambil barisnya. Minta angkanya:\n\n```python\npost_ids = [post.id for post in posts]\n\ncounts = dict(\n    session.execute(\n        select(Comment.post_id, func.count())\n        .where(Comment.post_id.in_(post_ids))\n        .group_by(Comment.post_id)\n    ).all()\n)\n\nfor post in posts:\n    post.comment_count = counts.get(post.id, 0)\n```\n\nQuery-nya tetap 2. Latency-nya turun dari 1.026 ms ke **62 ms**.\n\nPerhatikan juga: `latestArticles` di bawah, `readNextPosts` di halaman artikel, daftar di\nhalaman kategori — semua pola yang sama. Kalau kamu pernah menampilkan \"5 komentar terakhir\"\ndengan cara memuat semua komentar lalu memotongnya di kode, itu kesalahan yang sama.\n\n## Kenapa masih 62 ms\n\nDi titik ini query-nya sudah benar dan hasilnya sudah benar. Yang belum benar adalah\nskemanya, dan itu nggak kelihatan dari membaca kode sama sekali.\n\nIni yang sebenarnya terjadi. Rata-rata tiap tulisan punya ~1.875 komentar, dan satu halaman\nmenampilkan 20 tulisan. Jadi query agregat di atas harus memeriksa 37.500 baris. Jumlah itu\nmasuk akal untuk database.\n\nYang nggak masuk akal adalah cara dia mencarinya. Ini `EXPLAIN ANALYZE` dari query yang sama,\ndijalankan langsung di database, di luar aplikasi:\n\n```sql\nEXPLAIN ANALYZE\nSELECT post_id, count(*)\nFROM comments\nWHERE post_id IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)\nGROUP BY post_id;\n```\n\n```\n Finalize GroupAggregate  (actual time=64.299..65.905 rows=20 loops=1)\n   ->  Gather Merge  (actual time=64.292..65.886 rows=60 loops=1)\n         Workers Launched: 2\n         ->  Partial HashAggregate  (actual time=57.802..57.807 rows=20 loops=3)\n               ->  Parallel Seq Scan on comments  (actual time=0.046..54.035 rows=12500 loops=3)\n                     Filter: (post_id = ANY ('{1,2,3,...}'::integer[]))\n                     Rows Removed by Filter: 237500\n Execution Time: 66.049 ms\n```\n\nHasilnya 20 angka. Baris yang dibaca lalu dibuang: **237.500 × 3 worker = 712.500.**\n\nHampir seluruh isi tabel dipindai, dan 95% hasilnya langsung dibuang, cuma untuk tahu ada\nberapa komentar di 20 tulisan. Kalau tabelnya tumbuh 10× lipat, angka itu ikut tumbuh 10×\nlipat — dan `make check` di laptopmu nggak akan kelihatan bedanya, karena dataset-nya tetap.\n\nSatu hal lagi yang perlu kamu lihat: bentuk yang sama persis terjadi di versi satu-query-per-\ntulisan (50,9 ms, 748.125 baris dibuang) — dan juga di versi awal yang 41 query itu. Semua\nvarian lambat, dari satu sebab yang sama. Jumlah query-nya beda-beda; sebabnya satu.\n\n## Perbaikan kedua: index, dan ini di luar kode\n\n```sql\nCREATE INDEX CONCURRENTLY idx_comments_post_id ON comments (post_id);\n```\n\nSetelah itu, plan-nya berubah jadi ini:\n\n```\n HashAggregate  (actual time=12.297..12.302 rows=20 loops=1)\n   ->  Bitmap Heap Scan on comments  (actual time=1.685..7.374 rows=37500 loops=1)\n         ->  Bitmap Index Scan on idx_comments_post_id  (rows=37500 loops=1)\n               Index Cond: (post_id = ANY ('{1,2,3,...}'::integer[]))\n Execution Time: 12.427 ms\n```\n\nBaris yang dibaca sekarang **37.500** — persis yang dibutuhkan, nggak ada yang dibuang.\n`Seq Scan` hilang, diganti `Bitmap Index Scan`. Di level endpoint, hasilnya 8 ms.\n\nKenapa `CONCURRENTLY`? Di lab ini tabelnya milikmu sendiri dan nggak ada yang menulis, jadi\n`CREATE INDEX` biasa aman. Di produksi nggak: `comments` akan terus menerima tulisan, dan\n`CREATE INDEX` biasa mengambil kunci yang memblokir tulisan selama prosesnya. `CONCURRENTLY`\nnggak memblokir — harganya, dia mengerjakan dua kali pemindaian tabel dan butuh waktu lebih\nlama. Kalau pembuatannya gagal, yang tertinggal adalah index berstatus *invalid* yang harus\nkamu buang manual sebelum mencoba lagi.\n\n## Dua tempat berbeda, dan hanya satu yang kelihatan di kode\n\nIni bagian yang pantas kamu ingat.\n\nKamu bisa membaca `repositories.py` sampai hafal dan nggak akan pernah menemukan perbaikan\nkeduanya, karena yang salah bukan cara kamu mengambil data — yang salah adalah datanya nggak\npunya jalan pintas untuk pertanyaan seperti itu.\n\nDan perhatikan baris keempat tabel di atas sekali lagi: **jebakan di artikel sebelumnya tetap\n1.002 ms walaupun sudah dipasangi index.** Index mempercepat cara database *mencari*. Kalau\nmasalahnya adalah jumlah data yang harus *menyeberang*, index bukan jawabannya.\n\nMenghitung bukan butuh datanya. Butuh angkanya.\n\n## Kalau kamu berhenti sebelum index\n\nKalau kamu berhasil menyelesaikan perbaikan pertama lalu berhenti karena sudah 94% lebih\ncepat, kamu belum salah — kamu cuma belum selesai. Di dunia nyata, keputusan seperti itu\njustru sering benar: 62 ms mungkin sudah cukup, dan index punya harga (tulisan jadi lebih\nlambat, dan memakan tempat).\n\nYang bikin keputusan itu jadi keputusan adalah kalau kamu tahu ada lapisan kedua dan\nmemutuskan dengan sadar untuk nggak mengerjakannya. Bukan kalau kamu nggak tahu.\n\n## Lab-nya, kalau kamu mau buktikan sendiri\n\n👉 **[inva-lab](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\nmake check   # target: \u003C= 3 query, p95 \u003C= 30 ms\n```\n\nBranch `solusi` berisi dua perbaikan di atas, plus catatan pengukurannya. Kalau kamu mau\nmembuktikan bahwa biaya ini tumbuh mengikuti jumlah baris, kalikan isi `db\u002Finit\u002F02_seed.sql`\njadi 3× lipat, jalankan `make reset`, lalu `make bench` lagi.\n\n**Pertanyaan untuk lab berikutnya:** kalau tabel `comments` sudah punya 200 juta baris dan\n`count(*)` per tulisan sudah jadi bagian dari halaman yang dilihat jutaan orang, index saja\nmulai nggak cukup. Apa yang kamu ubah? (Ada tiga jawaban yang sering dipakai di produksi, dan\nsemuanya punya kekurangan masing-masing.)\n","- Query yang sedikit belum tentu cepat: 2 query bisa lebih lambat daripada 41 query.\n- Menghitung dengan len() berarti memindahkan semua barisnya ke memori aplikasi. Minta angkanya lewat agregat.\n- Setelah kodenya benar, lapisan kedua ada di skema: foreign key tanpa index bikin database memindai seluruh tabel lalu membuang hampir semua hasilnya.\n- Index mempercepat cara database mencari, bukan mengurangi jumlah data yang harus menyeberang.",{"id":91,"title":92,"slug":94},"\u003Cp>\u003Cstrong>Ini artikel jawabannya.\u003C\u002Fstrong> Kalau kamu belum mengerjakan lab-nya, mendingan tutup dulu halaman\nini dan mulai dari \u003Ca href=\"\u002Fp\u002Fquery-tinggal-2-tapi-response-masih-1-detik\">Query-nya Tinggal 2, Tapi Response-nya Masih 1\nDetik\u003C\u002Fa>. Lab-nya ada di\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0001.n-plus-one\">inva-lab\u003C\u002Fa> — \u003Ccode>make up\u003C\u002Fcode>, \u003Ccode>make check\u003C\u002Fcode>,\ndan targetnya \u003Ccode>&lt;= 3\u003C\u002Fcode> query dengan p95 \u003Ccode>&lt;= 30 ms\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Sisanya di bawah ini adalah jawabannya.\u003C\u002Fp>\n\u003Ch2 id=\"angka-lengkapnya\">Angka lengkapnya\u003C\u002Fh2>\n\u003Cp>Enam varian dari endpoint yang sama, di dataset yang sama (400 tulisan, 750.000 komentar,\n20 tulisan per halaman). Semua diukur di mesin yang sama, dan \u003Cstrong>setiap varian mengembalikan\nbody response yang identik\u003C\u002Fstrong> — yang berubah cuma performanya.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C\u002Fth>\n\u003Cth>query\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>jebakan: komentar di-\u003Cem>eager load\u003C\u002Fem>, dihitung \u003Ccode>len()\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>983 ms\u003C\u002Ftd>\n\u003Ctd>1.026 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>jebakan + index\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>949 ms\u003C\u002Ftd>\n\u003Ctd>1.002 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>komentar dihitung sebagai satu agregat\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>60 ms\u003C\u002Ftd>\n\u003Ctd>62 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>agregat + index\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>7 ms\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>8 ms\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Tiga baris pertama sudah kamu lihat di artikel sebelumnya. Tiga baris terakhir itu jawabannya,\ndan perhatikan baris keempat — itu bagian yang paling gampang dilewatkan.\u003C\u002Fp>\n\u003Ch2 id=\"perbaikan-pertama-hitung-jangan-pindahkan\">Perbaikan pertama: hitung, jangan pindahkan\u003C\u002Fh2>\n\u003Cp>Kesalahannya bukan memakai \u003Cem>eager loading\u003C\u002Fem>. Silakan pakai \u003Cem>eager loading\u003C\u002Fem> sampai kapan pun\nuntuk data yang memang mau kamu tampilkan.\u003C\u002Fp>\n\u003Cp>Kesalahannya adalah memakai \u003Ccode>len()\u003C\u002Fcode> untuk \u003Cstrong>menghitung\u003C\u002Fstrong>. Supaya bisa memanggil \u003Ccode>len()\u003C\u002Fcode>, semua\nbarisnya harus ada dulu di memori aplikasi. Padahal kamu nggak butuh barisnya sama sekali —\nkamu butuh satu angka per tulisan.\u003C\u002Fp>\n\u003Cp>Jadi jangan ambil barisnya. Minta angkanya:\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=\"post_ids%20%3D%20%5Bpost.id%20for%20post%20in%20posts%5D%0A%0Acounts%20%3D%20dict(%0A%20%20%20%20session.execute(%0A%20%20%20%20%20%20%20%20select(Comment.post_id%2C%20func.count())%0A%20%20%20%20%20%20%20%20.where(Comment.post_id.in_(post_ids))%0A%20%20%20%20%20%20%20%20.group_by(Comment.post_id)%0A%20%20%20%20).all()%0A)%0A%0Afor%20post%20in%20posts%3A%0A%20%20%20%20post.comment_count%20%3D%20counts.get(post.id%2C%200)\" 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\">post_ids \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [post.id \u003C\u002Fspan>\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\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">counts \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> dict\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    session.execute(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        select(Comment.post_id, func.count())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .where(Comment.post_id.in_(post_ids))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .group_by(Comment.post_id)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).all()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\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:#E1E4E8\"> counts.get(post.id, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Query-nya tetap 2. Latency-nya turun dari 1.026 ms ke \u003Cstrong>62 ms\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cp>Perhatikan juga: \u003Ccode>latestArticles\u003C\u002Fcode> di bawah, \u003Ccode>readNextPosts\u003C\u002Fcode> di halaman artikel, daftar di\nhalaman kategori — semua pola yang sama. Kalau kamu pernah menampilkan &quot;5 komentar terakhir&quot;\ndengan cara memuat semua komentar lalu memotongnya di kode, itu kesalahan yang sama.\u003C\u002Fp>\n\u003Ch2 id=\"kenapa-masih-62-ms\">Kenapa masih 62 ms\u003C\u002Fh2>\n\u003Cp>Di titik ini query-nya sudah benar dan hasilnya sudah benar. Yang belum benar adalah\nskemanya, dan itu nggak kelihatan dari membaca kode sama sekali.\u003C\u002Fp>\n\u003Cp>Ini yang sebenarnya terjadi. Rata-rata tiap tulisan punya ~1.875 komentar, dan satu halaman\nmenampilkan 20 tulisan. Jadi query agregat di atas harus memeriksa 37.500 baris. Jumlah itu\nmasuk akal untuk database.\u003C\u002Fp>\n\u003Cp>Yang nggak masuk akal adalah cara dia mencarinya. Ini \u003Ccode>EXPLAIN ANALYZE\u003C\u002Fcode> dari query yang sama,\ndijalankan langsung di database, di luar aplikasi:\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\">SQL\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=\"EXPLAIN%20ANALYZE%0ASELECT%20post_id%2C%20count(*)%0AFROM%20comments%0AWHERE%20post_id%20IN%20(1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20)%0AGROUP%20BY%20post_id%3B\" 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\">EXPLAIN ANALYZE\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">SELECT\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post_id, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">count\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">FROM\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> comments\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">WHERE\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">IN\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">4\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">5\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">6\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">7\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">8\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">9\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">10\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">11\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">12\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">13\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">14\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">15\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">16\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">17\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">18\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">19\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">,\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">20\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">GROUP BY\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> post_id;\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\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\">TEXT\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=\"%20Finalize%20GroupAggregate%20%20(actual%20time%3D64.299..65.905%20rows%3D20%20loops%3D1)%0A%20%20%20-%3E%20%20Gather%20Merge%20%20(actual%20time%3D64.292..65.886%20rows%3D60%20loops%3D1)%0A%20%20%20%20%20%20%20%20%20Workers%20Launched%3A%202%0A%20%20%20%20%20%20%20%20%20-%3E%20%20Partial%20HashAggregate%20%20(actual%20time%3D57.802..57.807%20rows%3D20%20loops%3D3)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20%20Parallel%20Seq%20Scan%20on%20comments%20%20(actual%20time%3D0.046..54.035%20rows%3D12500%20loops%3D3)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Filter%3A%20(post_id%20%3D%20ANY%20('%7B1%2C2%2C3%2C...%7D'%3A%3Ainteger%5B%5D))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Rows%20Removed%20by%20Filter%3A%20237500%0A%20Execution%20Time%3A%2066.049%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\">\u003Ccode> Finalize GroupAggregate  (actual time=64.299..65.905 rows=20 loops=1)\n   -&gt;  Gather Merge  (actual time=64.292..65.886 rows=60 loops=1)\n         Workers Launched: 2\n         -&gt;  Partial HashAggregate  (actual time=57.802..57.807 rows=20 loops=3)\n               -&gt;  Parallel Seq Scan on comments  (actual time=0.046..54.035 rows=12500 loops=3)\n                     Filter: (post_id = ANY ('{1,2,3,...}'::integer[]))\n                     Rows Removed by Filter: 237500\n Execution Time: 66.049 ms\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Hasilnya 20 angka. Baris yang dibaca lalu dibuang: \u003Cstrong>237.500 × 3 worker = 712.500.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Hampir seluruh isi tabel dipindai, dan 95% hasilnya langsung dibuang, cuma untuk tahu ada\nberapa komentar di 20 tulisan. Kalau tabelnya tumbuh 10× lipat, angka itu ikut tumbuh 10×\nlipat — dan \u003Ccode>make check\u003C\u002Fcode> di laptopmu nggak akan kelihatan bedanya, karena dataset-nya tetap.\u003C\u002Fp>\n\u003Cp>Satu hal lagi yang perlu kamu lihat: bentuk yang sama persis terjadi di versi satu-query-per-\ntulisan (50,9 ms, 748.125 baris dibuang) — dan juga di versi awal yang 41 query itu. Semua\nvarian lambat, dari satu sebab yang sama. Jumlah query-nya beda-beda; sebabnya satu.\u003C\u002Fp>\n\u003Ch2 id=\"perbaikan-kedua-index-dan-ini-di-luar-kode\">Perbaikan kedua: index, dan ini di luar kode\u003C\u002Fh2>\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\">SQL\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=\"CREATE%20INDEX%20CONCURRENTLY%20idx_comments_post_id%20ON%20comments%20(post_id)%3B\" 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\">CREATE\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> INDEX\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> CONCURRENTLY\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> idx_comments_post_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">ON\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> comments (post_id);\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Setelah itu, plan-nya berubah jadi 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\">TEXT\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=\"%20HashAggregate%20%20(actual%20time%3D12.297..12.302%20rows%3D20%20loops%3D1)%0A%20%20%20-%3E%20%20Bitmap%20Heap%20Scan%20on%20comments%20%20(actual%20time%3D1.685..7.374%20rows%3D37500%20loops%3D1)%0A%20%20%20%20%20%20%20%20%20-%3E%20%20Bitmap%20Index%20Scan%20on%20idx_comments_post_id%20%20(rows%3D37500%20loops%3D1)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Index%20Cond%3A%20(post_id%20%3D%20ANY%20('%7B1%2C2%2C3%2C...%7D'%3A%3Ainteger%5B%5D))%0A%20Execution%20Time%3A%2012.427%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\">\u003Ccode> HashAggregate  (actual time=12.297..12.302 rows=20 loops=1)\n   -&gt;  Bitmap Heap Scan on comments  (actual time=1.685..7.374 rows=37500 loops=1)\n         -&gt;  Bitmap Index Scan on idx_comments_post_id  (rows=37500 loops=1)\n               Index Cond: (post_id = ANY ('{1,2,3,...}'::integer[]))\n Execution Time: 12.427 ms\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Baris yang dibaca sekarang \u003Cstrong>37.500\u003C\u002Fstrong> — persis yang dibutuhkan, nggak ada yang dibuang.\n\u003Ccode>Seq Scan\u003C\u002Fcode> hilang, diganti \u003Ccode>Bitmap Index Scan\u003C\u002Fcode>. Di level endpoint, hasilnya 8 ms.\u003C\u002Fp>\n\u003Cp>Kenapa \u003Ccode>CONCURRENTLY\u003C\u002Fcode>? Di lab ini tabelnya milikmu sendiri dan nggak ada yang menulis, jadi\n\u003Ccode>CREATE INDEX\u003C\u002Fcode> biasa aman. Di produksi nggak: \u003Ccode>comments\u003C\u002Fcode> akan terus menerima tulisan, dan\n\u003Ccode>CREATE INDEX\u003C\u002Fcode> biasa mengambil kunci yang memblokir tulisan selama prosesnya. \u003Ccode>CONCURRENTLY\u003C\u002Fcode>\nnggak memblokir — harganya, dia mengerjakan dua kali pemindaian tabel dan butuh waktu lebih\nlama. Kalau pembuatannya gagal, yang tertinggal adalah index berstatus \u003Cem>invalid\u003C\u002Fem> yang harus\nkamu buang manual sebelum mencoba lagi.\u003C\u002Fp>\n\u003Ch2 id=\"dua-tempat-berbeda-dan-hanya-satu-yang-kelihatan-di-kode\">Dua tempat berbeda, dan hanya satu yang kelihatan di kode\u003C\u002Fh2>\n\u003Cp>Ini bagian yang pantas kamu ingat.\u003C\u002Fp>\n\u003Cp>Kamu bisa membaca \u003Ccode>repositories.py\u003C\u002Fcode> sampai hafal dan nggak akan pernah menemukan perbaikan\nkeduanya, karena yang salah bukan cara kamu mengambil data — yang salah adalah datanya nggak\npunya jalan pintas untuk pertanyaan seperti itu.\u003C\u002Fp>\n\u003Cp>Dan perhatikan baris keempat tabel di atas sekali lagi: \u003Cstrong>jebakan di artikel sebelumnya tetap\n1.002 ms walaupun sudah dipasangi index.\u003C\u002Fstrong> Index mempercepat cara database \u003Cem>mencari\u003C\u002Fem>. Kalau\nmasalahnya adalah jumlah data yang harus \u003Cem>menyeberang\u003C\u002Fem>, index bukan jawabannya.\u003C\u002Fp>\n\u003Cp>Menghitung bukan butuh datanya. Butuh angkanya.\u003C\u002Fp>\n\u003Ch2 id=\"kalau-kamu-berhenti-sebelum-index\">Kalau kamu berhenti sebelum index\u003C\u002Fh2>\n\u003Cp>Kalau kamu berhasil menyelesaikan perbaikan pertama lalu berhenti karena sudah 94% lebih\ncepat, kamu belum salah — kamu cuma belum selesai. Di dunia nyata, keputusan seperti itu\njustru sering benar: 62 ms mungkin sudah cukup, dan index punya harga (tulisan jadi lebih\nlambat, dan memakan tempat).\u003C\u002Fp>\n\u003Cp>Yang bikin keputusan itu jadi keputusan adalah kalau kamu tahu ada lapisan kedua dan\nmemutuskan dengan sadar untuk nggak mengerjakannya. Bukan kalau kamu nggak tahu.\u003C\u002Fp>\n\u003Ch2 id=\"lab-nya-kalau-kamu-mau-buktikan-sendiri\">Lab-nya, kalau kamu mau buktikan sendiri\u003C\u002Fh2>\n\u003Cp>👉 \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0001.n-plus-one\">inva-lab\u003C\u002Fa>\u003C\u002Fstrong>\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%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>\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>Branch \u003Ccode>solusi\u003C\u002Fcode> berisi dua perbaikan di atas, plus catatan pengukurannya. Kalau kamu mau\nmembuktikan bahwa biaya ini tumbuh mengikuti jumlah baris, kalikan isi \u003Ccode>db\u002Finit\u002F02_seed.sql\u003C\u002Fcode>\njadi 3× lipat, jalankan \u003Ccode>make reset\u003C\u002Fcode>, lalu \u003Ccode>make bench\u003C\u002Fcode> lagi.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Pertanyaan untuk lab berikutnya:\u003C\u002Fstrong> kalau tabel \u003Ccode>comments\u003C\u002Fcode> sudah punya 200 juta baris dan\n\u003Ccode>count(*)\u003C\u002Fcode> per tulisan sudah jadi bagian dari halaman yang dilihat jutaan orang, index saja\nmulai nggak cukup. Apa yang kamu ubah? (Ada tiga jawaban yang sering dipakai di produksi, dan\nsemuanya punya kekurangan masing-masing.)\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>Query yang sedikit belum tentu cepat: 2 query bisa lebih lambat daripada 41 query.\u003C\u002Fli>\n\u003Cli>Menghitung dengan len() berarti memindahkan semua barisnya ke memori aplikasi. Minta angkanya lewat agregat.\u003C\u002Fli>\n\u003Cli>Setelah kodenya benar, lapisan kedua ada di skema: foreign key tanpa index bikin database memindai seluruh tabel lalu membuang hampir semua hasilnya.\u003C\u002Fli>\n\u003Cli>Index mempercepat cara database mencari, bukan mengurangi jumlah data yang harus menyeberang.\u003C\u002Fli>\n\u003C\u002Ful>\n",[187,205,218,231,244,265],{"id":188,"title":189,"title_id":21,"slug":190,"excerpt":191,"excerpt_id":21,"has_translation":15,"published_at":192,"reading_time":193,"hidden_from_listing":15,"tags":194,"thumbnail_image":21,"og_image":196,"series":197,"authors":202},133,"Python Strings: the Badge That Only Worked for One Name","python-strings","The badge script printed the right name for the person who wrote it and the wrong name for everybody else. Slicing counts characters, and a name is words.","2026-10-28T13:00:00.000Z",6,[151,195],"string","\u002Fog\u002Fpython-strings.png",{"id":198,"title":199,"title_id":200,"slug":201},30,"Collections and Strings","Koleksi dan String","python-collections",[203],{"authors_id":204},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":206,"title":207,"title_id":21,"slug":208,"excerpt":209,"excerpt_id":21,"has_translation":15,"published_at":210,"reading_time":193,"hidden_from_listing":15,"tags":211,"thumbnail_image":21,"og_image":213,"series":214,"authors":215},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",[151,212],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":198,"title":199,"title_id":200,"slug":201},[216],{"authors_id":217},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":219,"title":220,"title_id":21,"slug":221,"excerpt":222,"excerpt_id":21,"has_translation":15,"published_at":223,"reading_time":193,"hidden_from_listing":15,"tags":224,"thumbnail_image":21,"og_image":226,"series":227,"authors":228},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",[151,225],"set","\u002Fog\u002Fpython-sets.png",{"id":198,"title":199,"title_id":200,"slug":201},[229],{"authors_id":230},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":232,"title":233,"title_id":21,"slug":234,"excerpt":235,"excerpt_id":21,"has_translation":15,"published_at":236,"reading_time":193,"hidden_from_listing":15,"tags":237,"thumbnail_image":21,"og_image":239,"series":240,"authors":241},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",[151,238],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":198,"title":199,"title_id":200,"slug":201},[242],{"authors_id":243},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":245,"title":246,"title_id":247,"slug":248,"excerpt":249,"excerpt_id":250,"has_translation":12,"published_at":251,"reading_time":14,"hidden_from_listing":15,"tags":252,"thumbnail_image":21,"og_image":259,"series":260,"authors":262},129,"So Your RAG Stops Hitting 504: Split the Query Path From Ingestion","Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","One RAG service that both parses uploads and answers queries will fall over the moment someone uploads a 200-page PDF. The cure is two paths and one queue.","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",[253,254,255,256,257,258],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":261,"title":35,"title_id":36,"slug":37},8,[263],{"authors_id":264},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},{"id":266,"title":267,"title_id":268,"slug":269,"excerpt":270,"excerpt_id":271,"has_translation":12,"published_at":272,"reading_time":193,"hidden_from_listing":15,"tags":273,"thumbnail_image":21,"og_image":280,"series":281,"authors":282},128,"Linear or Graph: How to Run Many Agents Without an Eternal Loop","Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","One agent with twenty tools gets messy fast. Once several agents revise each other, you need a graph that allows cycles and a number that makes it stop.","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",[274,275,276,277,278,279],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":261,"title":35,"title_id":36,"slug":37},[283],{"authors_id":284},{"id":104,"name":105,"slug":106,"role":107,"profile_picture":21},[286,290,293,296,299,302,305],{"text":287,"depth":288,"id":289},"Angka lengkapnya",2,"angka-lengkapnya",{"text":291,"depth":288,"id":292},"Perbaikan pertama: hitung, jangan pindahkan","perbaikan-pertama-hitung-jangan-pindahkan",{"text":294,"depth":288,"id":295},"Kenapa masih 62 ms","kenapa-masih-62-ms",{"text":297,"depth":288,"id":298},"Perbaikan kedua: index, dan ini di luar kode","perbaikan-kedua-index-dan-ini-di-luar-kode",{"text":300,"depth":288,"id":301},"Dua tempat berbeda, dan hanya satu yang kelihatan di kode","dua-tempat-berbeda-dan-hanya-satu-yang-kelihatan-di-kode",{"text":303,"depth":288,"id":304},"Kalau kamu berhenti sebelum index","kalau-kamu-berhenti-sebelum-index",{"text":306,"depth":288,"id":307},"Lab-nya, kalau kamu mau buktikan sendiri","lab-nya-kalau-kamu-mau-buktikan-sendiri",1793336438354]