[{"data":1,"prerenderedAt":146},["ShallowReactive",2],{"site-header":3,"site-footer-license":37,"article-jawaban-dua-perbaikan-di-dua-tempat-berbeda-with-latest":41},{"navigations":4},[5,20],{"collection":6,"item":7},"series",{"__typename":6,"title":8,"slug":9,"children_series":10},"Artikel","all",[11,14,17],{"title":12,"slug":13},"Algoritma dan Struktur Data","algoritma-struktur-data",{"title":15,"slug":16},"Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":18,"slug":19},"Kecerdasan Artifisial","kecerdasan-artifisial",{"collection":6,"item":21},{"__typename":6,"title":22,"slug":23,"children_series":24},"Tutorial","tutorial",[25,28,31,34],{"title":26,"slug":27},"Belajar Java","belajar-java",{"title":29,"slug":30},"Belajar Pascal","belajar-pascal",{"title":32,"slug":33},"Belajar Python","belajar-python",{"title":35,"slug":36},"Belajar Golang","belajar-golang",{"name":38,"url":39,"show":40},"CC BY-SA","https://creativecommons.org/licenses/by-sa/4.0/",false,{"article":42,"renderedBody":79,"renderedTakeaway":80,"latestArticles":81,"toc":123},{"prerequisite":43,"id":47,"title":48,"slug":49,"excerpt":50,"published_at":51,"thumbnail_image":52,"tags":54,"reading_time":59,"takeway":60,"body":61,"authors":62,"series":69},{"id":44,"title":45,"slug":46},"104","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","query-tinggal-2-tapi-response-masih-1-detik","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",{"id":53},"c310adb9-6931-4a19-99b6-c13588188e89",[55,56,57,58],"database","performance","backend","sqlalchemy",5,"- 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.","**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](/p/query-tinggal-2-tapi-response-masih-1-detik). Lab-nya ada di\n[inva-lab](https://github.com/izzudd/inva-lab/tree/main/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://github.com/izzudd/inva-lab/tree/main/n-plus-one)**\n\n```bash\ngit clone https://github.com/izzudd/inva-lab.git\ncd inva-lab/n-plus-one\nmake up\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/init/02_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.)",[63],{"authors_id":64},{"name":65,"slug":66,"role":67,"profile_picture":68},"Daffa Izzuddin","izzudd","Writer",null,{"id":70,"title":71,"slug":72,"articles":73,"parent_series":68},"29","Lab","lab",[74],{"id":44,"title":45,"slug":46,"excerpt":75,"thumbnail_image":76,"tags":78},"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.",{"id":77},"3214df59-f4c6-4677-9983-2e0a55b18f97",[55,56,57],"\u003Cp>\u003Cstrong>Ini artikel jawabannya.\u003C/strong> Kalau kamu belum mengerjakan lab-nya, mendingan tutup dulu halaman\nini dan mulai dari \u003Ca href=\"/p/query-tinggal-2-tapi-response-masih-1-detik\">Query-nya Tinggal 2, Tapi Response-nya Masih 1\nDetik\u003C/a>. Lab-nya ada di\n\u003Ca href=\"https://github.com/izzudd/inva-lab/tree/main/n-plus-one\">inva-lab\u003C/a> — \u003Ccode>make up\u003C/code>, \u003Ccode>make check\u003C/code>,\ndan targetnya \u003Ccode>&lt;= 3\u003C/code> query dengan p95 \u003Ccode>&lt;= 30 ms\u003C/code>.\u003C/p>\n\u003Cp>Sisanya di bawah ini adalah jawabannya.\u003C/p>\n\u003Ch2 id=\"angka-lengkapnya\">Angka lengkapnya\u003C/h2>\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/strong> — yang berubah cuma performanya.\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>versi\u003C/th>\n\u003Cth>query\u003C/th>\n\u003Cth>p50\u003C/th>\n\u003Cth>p95\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>apa adanya\u003C/td>\n\u003Ctd>41\u003C/td>\n\u003Ctd>950 ms\u003C/td>\n\u003Ctd>1.152 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>setelah penulis di-\u003Cem>eager load\u003C/em>\u003C/td>\n\u003Ctd>21\u003C/td>\n\u003Ctd>915 ms\u003C/td>\n\u003Ctd>981 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>jebakan: komentar di-\u003Cem>eager load\u003C/em>, dihitung \u003Ccode>len()\u003C/code>\u003C/td>\n\u003Ctd>2\u003C/td>\n\u003Ctd>983 ms\u003C/td>\n\u003Ctd>1.026 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>jebakan + index\u003C/td>\n\u003Ctd>2\u003C/td>\n\u003Ctd>949 ms\u003C/td>\n\u003Ctd>1.002 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>komentar dihitung sebagai satu agregat\u003C/td>\n\u003Ctd>2\u003C/td>\n\u003Ctd>60 ms\u003C/td>\n\u003Ctd>62 ms\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>agregat + index\u003C/td>\n\u003Ctd>2\u003C/td>\n\u003Ctd>\u003Cstrong>7 ms\u003C/strong>\u003C/td>\n\u003Ctd>\u003Cstrong>8 ms\u003C/strong>\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\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/p>\n\u003Ch2 id=\"perbaikan-pertama-hitung-jangan-pindahkan\">Perbaikan pertama: hitung, jangan pindahkan\u003C/h2>\n\u003Cp>Kesalahannya bukan memakai \u003Cem>eager loading\u003C/em>. Silakan pakai \u003Cem>eager loading\u003C/em> sampai kapan pun\nuntuk data yang memang mau kamu tampilkan.\u003C/p>\n\u003Cp>Kesalahannya adalah memakai \u003Ccode>len()\u003C/code> untuk \u003Cstrong>menghitung\u003C/strong>. Supaya bisa memanggil \u003Ccode>len()\u003C/code>, semua\nbarisnya harus ada dulu di memori aplikasi. Padahal kamu nggak butuh barisnya sama sekali —\nkamu butuh satu angka per tulisan.\u003C/p>\n\u003Cp>Jadi jangan ambil barisnya. Minta angkanya:\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">post_ids \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [post.id \u003C/span>\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> posts]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">counts \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> dict\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    session.execute(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        select(Comment.post_id, func.count())\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .where(Comment.post_id.in_(post_ids))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .group_by(Comment.post_id)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).all()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> posts:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    post.comment_count \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> counts.get(post.id, \u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Query-nya tetap 2. Latency-nya turun dari 1.026 ms ke \u003Cstrong>62 ms\u003C/strong>.\u003C/p>\n\u003Cp>Perhatikan juga: \u003Ccode>latestArticles\u003C/code> di bawah, \u003Ccode>readNextPosts\u003C/code> 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/p>\n\u003Ch2 id=\"kenapa-masih-62-ms\">Kenapa masih 62 ms\u003C/h2>\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/p>\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/p>\n\u003Cp>Yang nggak masuk akal adalah cara dia mencarinya. Ini \u003Ccode>EXPLAIN ANALYZE\u003C/code> dari query yang sama,\ndijalankan langsung di database, di luar aplikasi:\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">SQL\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">EXPLAIN ANALYZE\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">SELECT\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post_id, \u003C/span>\u003Cspan style=\"color:#79B8FF\">count\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">FROM\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> comments\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">WHERE\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post_id \u003C/span>\u003Cspan style=\"color:#F97583\">IN\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">4\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">5\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">6\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">7\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">8\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">9\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">10\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">11\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">12\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">13\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">14\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">15\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">16\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">17\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">18\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">19\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">GROUP BY\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> post_id;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\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/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Hasilnya 20 angka. Baris yang dibaca lalu dibuang: \u003Cstrong>237.500 × 3 worker = 712.500.\u003C/strong>\u003C/p>\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/code> di laptopmu nggak akan kelihatan bedanya, karena dataset-nya tetap.\u003C/p>\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/p>\n\u003Ch2 id=\"perbaikan-kedua-index-dan-ini-di-luar-kode\">Perbaikan kedua: index, dan ini di luar kode\u003C/h2>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">SQL\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">CREATE\u003C/span>\u003Cspan style=\"color:#F97583\"> INDEX\u003C/span>\u003Cspan style=\"color:#B392F0\"> CONCURRENTLY\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> idx_comments_post_id \u003C/span>\u003Cspan style=\"color:#F97583\">ON\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> comments (post_id);\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Setelah itu, plan-nya berubah jadi ini:\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\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/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Baris yang dibaca sekarang \u003Cstrong>37.500\u003C/strong> — persis yang dibutuhkan, nggak ada yang dibuang.\n\u003Ccode>Seq Scan\u003C/code> hilang, diganti \u003Ccode>Bitmap Index Scan\u003C/code>. Di level endpoint, hasilnya 8 ms.\u003C/p>\n\u003Cp>Kenapa \u003Ccode>CONCURRENTLY\u003C/code>? Di lab ini tabelnya milikmu sendiri dan nggak ada yang menulis, jadi\n\u003Ccode>CREATE INDEX\u003C/code> biasa aman. Di produksi nggak: \u003Ccode>comments\u003C/code> akan terus menerima tulisan, dan\n\u003Ccode>CREATE INDEX\u003C/code> biasa mengambil kunci yang memblokir tulisan selama prosesnya. \u003Ccode>CONCURRENTLY\u003C/code>\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/em> yang harus\nkamu buang manual sebelum mencoba lagi.\u003C/p>\n\u003Ch2 id=\"dua-tempat-berbeda-dan-hanya-satu-yang-kelihatan-di-kode\">Dua tempat berbeda, dan hanya satu yang kelihatan di kode\u003C/h2>\n\u003Cp>Ini bagian yang pantas kamu ingat.\u003C/p>\n\u003Cp>Kamu bisa membaca \u003Ccode>repositories.py\u003C/code> 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/p>\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/strong> Index mempercepat cara database \u003Cem>mencari\u003C/em>. Kalau\nmasalahnya adalah jumlah data yang harus \u003Cem>menyeberang\u003C/em>, index bukan jawabannya.\u003C/p>\n\u003Cp>Menghitung bukan butuh datanya. Butuh angkanya.\u003C/p>\n\u003Ch2 id=\"kalau-kamu-berhenti-sebelum-index\">Kalau kamu berhenti sebelum index\u003C/h2>\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/p>\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/p>\n\u003Ch2 id=\"lab-nya-kalau-kamu-mau-buktikan-sendiri\">Lab-nya, kalau kamu mau buktikan sendiri\u003C/h2>\n\u003Cp>👉 \u003Cstrong>\u003Ca href=\"https://github.com/izzudd/inva-lab/tree/main/n-plus-one\">inva-lab\u003C/a>\u003C/strong>\u003C/p>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">BASH\u003C/span>\n    \u003C/div>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"git%20clone%20https%3A%2F%2Fgithub.com%2Fizzudd%2Finva-lab.git%0Acd%20inva-lab%2Fn-plus-one%0Amake%20up%0Amake%20check%20%20%20%23%20target%3A%20%3C%3D%203%20query%2C%20p95%20%3C%3D%2030%20ms\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">git\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> clone\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://github.com/izzudd/inva-lab.git\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">cd\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> inva-lab/n-plus-one\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> up\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> check\u003C/span>\u003Cspan style=\"color:#6A737D\">   # target: &#x3C;= 3 query, p95 &#x3C;= 30 ms\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Branch \u003Ccode>solusi\u003C/code> berisi dua perbaikan di atas, plus catatan pengukurannya. Kalau kamu mau\nmembuktikan bahwa biaya ini tumbuh mengikuti jumlah baris, kalikan isi \u003Ccode>db/init/02_seed.sql\u003C/code>\njadi 3× lipat, jalankan \u003Ccode>make reset\u003C/code>, lalu \u003Ccode>make bench\u003C/code> lagi.\u003C/p>\n\u003Cp>\u003Cstrong>Pertanyaan untuk lab berikutnya:\u003C/strong> kalau tabel \u003Ccode>comments\u003C/code> sudah punya 200 juta baris dan\n\u003Ccode>count(*)\u003C/code> 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/p>\n","\u003Cul>\n\u003Cli>Query yang sedikit belum tentu cepat: 2 query bisa lebih lambat daripada 41 query.\u003C/li>\n\u003Cli>Menghitung dengan len() berarti memindahkan semua barisnya ke memori aplikasi. Minta angkanya lewat agregat.\u003C/li>\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/li>\n\u003Cli>Index mempercepat cara database mencari, bukan mengurangi jumlah data yang harus menyeberang.\u003C/li>\n\u003C/ul>\n",[82,85,98,112],{"id":44,"title":45,"slug":46,"excerpt":75,"thumbnail_image":83,"tags":84},{"id":77},[55,56,57],{"id":86,"title":87,"slug":88,"excerpt":89,"thumbnail_image":90,"tags":92},"46","Mengapa C Mulai Ditinggalkan: Duel Filosofi Keamanan Rust vs Kesederhanaan Zig","mengapa-c-mulai-ditinggalkan-duel-filosofi-rust-vs-zig","Pernah denger kalau sekitar 70% celah keamanan kritis software modern itu asalnya dari bahasa C? Iya, bahasa yang udah jadi fondasi internet ini 😅. Yuk, kita bedah duel filosofi Rust vs Zig dan cari tahu mana yang pas buat proyek kalian!",{"id":91},"1942a5c9-c1f6-476d-9913-768f78d9d8eb",[93,94,95,96,97],"system-programming","rust","zig","c","memory-safety",{"id":99,"title":100,"slug":101,"excerpt":102,"thumbnail_image":103,"tags":105},"45","Rate Limiting Algorithms: Token Bucket vs Leaky Bucket vs Sliding Window","rate-limiting-algorithms-token-bucket-leaky-bucket-sliding-window","Udah tahu kenapa *rate limiting* itu wajib, tapi masih bingung beda Token Bucket, Leaky Bucket, Fixed Window, dan Sliding Window? Yuk, kita adu empat algoritma ini bareng-bareng dan cari tahu mana yang paling pas buat API kalian!",{"id":104},"0e0ed26d-20c6-443c-bde1-4a6200f6f801",[57,106,107,108,109,110,111],"system-design","rate-limiting","redis","api-gateway","architecture","traffict-management",{"id":113,"title":114,"slug":115,"excerpt":116,"thumbnail_image":117,"tags":119},"44","Transactional Outbox Pattern: Solusi Konsistensi Database dan Message Broker Tanpa 2PC","transactional-outbox-pattern-konsistensi-database-message-broker","Pernah nggak kalian lihat pesanan udah masuk database, tapi email konfirmasinya nggak pernah kekirim? Itu gejala *dual-write problem*. Yuk, kita bedah Transactional Outbox Pattern biar data dan *event* kalian nggak lagi berselisih paham!",{"id":118},"e3c617fd-1486-4b82-b937-94fbec846c77",[57,120,110,55,121,106,122],"microservices","message-broker","postgresql",[124,128,131,134,137,140,143],{"text":125,"depth":126,"id":127},"Angka lengkapnya",2,"angka-lengkapnya",{"text":129,"depth":126,"id":130},"Perbaikan pertama: hitung, jangan pindahkan","perbaikan-pertama-hitung-jangan-pindahkan",{"text":132,"depth":126,"id":133},"Kenapa masih 62 ms","kenapa-masih-62-ms",{"text":135,"depth":126,"id":136},"Perbaikan kedua: index, dan ini di luar kode","perbaikan-kedua-index-dan-ini-di-luar-kode",{"text":138,"depth":126,"id":139},"Dua tempat berbeda, dan hanya satu yang kelihatan di kode","dua-tempat-berbeda-dan-hanya-satu-yang-kelihatan-di-kode",{"text":141,"depth":126,"id":142},"Kalau kamu berhenti sebelum index","kalau-kamu-berhenti-sebelum-index",{"text":144,"depth":126,"id":145},"Lab-nya, kalau kamu mau buktikan sendiri","lab-nya-kalau-kamu-mau-buktikan-sendiri",1791110590051]