COUNT() #

COUNT() adalah fungsi aggregate yang hampir selalu ada di setiap aplikasi — untuk pagination, dashboard statistik, badge notifikasi, atau laporan. Secara sintaks ia terlihat sederhana dan tidak berbahaya. Tapi di balik permukaan, COUNT() adalah salah satu operasi yang paling sering menjadi bottleneck diam-diam di sistem production: ia memaksa database membaca data jauh lebih banyak dari yang kamu bayangkan, tidak bisa dikembalikan dari index saja pada sebagian besar kasus, dan jika diletakkan di endpoint yang sering dipanggil, dampaknya berlipat ganda setiap request. Artikel ini membahas perbedaan perilaku tiap varian COUNT(), mengapa ia mahal pada tabel besar, dan lima strategi konkret untuk menggantikan atau mengefisienkan penggunaannya di production.

Tiga Varian COUNT() dan Perbedaan Perilakunya #

Sebelum membahas masalah performa, penting untuk memahami bahwa COUNT() bukan satu fungsi — ada tiga varian dengan semantik dan perilaku yang berbeda, dan memilih varian yang salah bisa memberikan hasil yang salah sekaligus lebih lambat.

COUNT(*) — Hitung Semua Baris Termasuk NULL #

COUNT(*) menghitung jumlah baris yang dikembalikan oleh query, tanpa memeriksa isi kolom manapun. Ini adalah varian yang paling efisien karena database tidak perlu mengevaluasi nilai kolom tertentu.

-- Hitung semua baris di tabel, termasuk yang punya nilai NULL di kolom manapun
SELECT COUNT(*) FROM orders;

-- Hitung baris yang memenuhi kondisi WHERE
SELECT COUNT(*) FROM orders WHERE status = 'paid';

-- Hitung baris di setiap grup
SELECT user_id, COUNT(*) as total_orders
FROM orders
GROUP BY user_id;

COUNT(kolom) — Hitung Baris dengan Nilai Non-NULL #

COUNT(kolom) menghitung baris di mana kolom tersebut tidak NULL. Ini lebih lambat dari COUNT(*) karena database harus memeriksa nilai setiap kolom.

-- Hitung order yang punya nilai di kolom shipping_at (sudah dikirim)
SELECT COUNT(shipping_at) FROM orders;
-- Berbeda dengan COUNT(*) jika ada baris dengan shipping_at = NULL

-- Perbandingan perilaku:
-- Baris: 1000 total, 400 sudah punya shipping_at, 600 masih NULL
SELECT COUNT(*)          FROM orders;  -- → 1000
SELECT COUNT(shipping_at) FROM orders;  -- → 400

COUNT(DISTINCT kolom) — Hitung Nilai Unik #

COUNT(DISTINCT kolom) adalah yang paling mahal karena database harus mengumpulkan semua nilai, mendeduplikasi, baru menghitung. Pada tabel besar tanpa optimasi, ini hampir selalu memerlukan tabel temporary.

-- Hitung berapa user berbeda yang punya order
SELECT COUNT(DISTINCT user_id) FROM orders;

-- EXPLAIN untuk COUNT(DISTINCT) di tabel besar:
-- Extra: "Using temporary"  ← tanda operasi mahal
-- Jika hasil tidak harus presisi, pertimbangkan pendekatan alternatif
Perbandingan biaya relatif tiga varian COUNT():
──────────────────────────────────────────────────────────────
  COUNT(*)            → Paling murah
                        Tidak perlu periksa nilai kolom
                        Di MySQL InnoDB: masih butuh index scan
                        Di PostgreSQL: butuh visibility check (MVCC)

  COUNT(kolom)        → Lebih mahal dari COUNT(*)
                        Harus periksa NULL/non-NULL tiap baris
                        Hampir tidak ada alasan memilih ini
                        kecuali memang ingin exclude NULL

  COUNT(DISTINCT col) → Paling mahal
                        Kumpulkan semua nilai → deduplikasi → hitung
                        Sering munculkan "Using temporary"
                        Pertimbangkan alternatif untuk tabel > 100k baris
──────────────────────────────────────────────────────────────

Mengapa COUNT() Mahal di Tabel Besar #

Perilaku di MySQL InnoDB: Tidak Ada “Magic Number” #

Sering ada miskonsepsi bahwa database menyimpan jumlah baris di suatu tempat yang bisa dibaca langsung — semacam metadata row_count = 5.000.000. Di MySQL InnoDB, ini tidak ada. Setiap kali COUNT(*) dieksekusi, database harus benar-benar menghitung:

-- Query yang terlihat sederhana
SELECT COUNT(*) FROM orders;

-- Yang terjadi di MySQL InnoDB:
-- 1. Pilih index yang paling kecil (bukan tabel utama)
-- 2. Scan seluruh index tersebut dari awal sampai akhir
-- 3. Hitung setiap entri yang valid (non-deleted)
-- 4. Kembalikan hasilnya

-- EXPLAIN-nya:
EXPLAIN SELECT COUNT(*) FROM orders;
-- +------+-------------+--------+-------+------+------+------+-------------+
-- | type | key         | rows   | Extra                                    |
-- +------+-------------+--------+-------+------+------+------+-------------+
-- | index| idx_status  | 5000000| Using index                              |
-- +------+-------------+--------+-------+------+------+------+-------------+
-- "Using index" berarti scan seluruh index (bukan tabel)
-- Tapi 5 juta entri tetap harus dibaca satu per satu

MySQL MyISAM menyimpan jumlah baris di metadata, sehingga COUNT(*) tanpa WHERE bisa O(1). Tapi InnoDB — yang dipakai hampir semua sistem modern karena transaction support-nya — tidak punya shortcut ini.

Perilaku di PostgreSQL: MVCC Memperumit Segalanya #

PostgreSQL menggunakan MVCC (Multi-Version Concurrency Control) yang memungkinkan multiple transaksi melihat snapshot data yang berbeda secara bersamaan. Konsekuensinya: tidak ada satu angka “jumlah baris yang valid” yang berlaku untuk semua transaksi.

-- Di PostgreSQL, COUNT(*) harus:
-- 1. Scan setiap baris (heap scan atau index scan)
-- 2. Periksa visibility setiap baris berdasarkan transaction ID
--    (apakah baris ini visible untuk transaksi saat ini?)
-- 3. Baru dihitung jika visible

-- Pada tabel yang sering di-UPDATE atau DELETE,
-- ada banyak "dead tuples" yang harus di-skip
-- → VACUUM rutin sangat penting untuk performa COUNT()

-- Cek dead tuples di tabel:
SELECT relname, n_live_tup, n_dead_tup,
       ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
WHERE relname = 'orders';
-- dead_pct tinggi → COUNT() lebih lambat, perlu VACUUM

COUNT() dengan WHERE: Index Scan Tetap Mahal #

Menambahkan kondisi WHERE membantu database membatasi baris yang dibaca, tapi tetap harus scan semua baris yang memenuhi kondisi:

-- Query pagination yang sangat umum
SELECT COUNT(*) FROM users WHERE status = 'active' AND deleted_at IS NULL;

-- Dengan index (status, deleted_at):
-- → type: ref, key: idx_users_status_deleted
-- → rows: 850000  (estimasi baris yang di-scan)
-- → Extra: Using index

-- 850.000 entri index harus dibaca dan dihitung.
-- Kalau endpoint ini dipanggil 500x/menit:
-- → 425 juta entri index dibaca per menit hanya untuk COUNT()
-- → Bukan beban yang kecil

Masalah Tersembunyi: COUNT() di Pagination #

Pola paling umum dan paling bermasalah adalah menggabungkan COUNT(*) dengan query pagination untuk menampilkan “Halaman 1 dari 47”:

-- Dua query yang dijalankan setiap kali user membuka halaman daftar
SELECT * FROM products WHERE category_id = 5 ORDER BY created_at DESC LIMIT 20 OFFSET 0;
SELECT COUNT(*) FROM products WHERE category_id = 5;  -- ← ini yang mahal

Yang tidak disadari: query COUNT() di sini menghitung ulang dari nol setiap request, meskipun jumlah produk di kategori 5 hampir tidak berubah dalam satu menit. Jika ada 10.000 user membuka halaman kategori bersamaan, database menjalankan 10.000 COUNT() yang masing-masing scan ratusan ribu baris — untuk hasil yang hampir identik.

Dampak COUNT() di pagination endpoint dengan traffic tinggi:
──────────────────────────────────────────────────────────────
  Tabel products: 2 juta baris
  Index (category_id, created_at): ada
  category_id = 5: 180.000 produk

  Setiap request halaman:
    → SELECT data: scan 20 baris (pakai index + LIMIT) → cepat
    → SELECT COUNT(*): scan 180.000 baris → lambat

  Pada 1.000 request/menit:
    → 180.000.000 entri index dibaca per menit hanya untuk COUNT()
    → CPU database terdongkrak
    → Query lain ikut melambat
──────────────────────────────────────────────────────────────

Lima Strategi Menggantikan COUNT() yang Mahal #

Strategi 1: LIMIT+1 — Buang Kebutuhan Total Count #

Paling sering dibutuhkan di pagination bukan “ada berapa halaman total”, tapi hanya “apakah ada halaman berikutnya?”. Untuk ini, LIMIT+1 sudah cukup dan jauh lebih efisien.

-- ANTI-PATTERN: hitung total lalu tentukan apakah ada next page
SELECT COUNT(*) FROM products WHERE category_id = 5;  -- mahal
SELECT * FROM products WHERE category_id = 5 ORDER BY id LIMIT 20 OFFSET 0;

-- BENAR: ambil satu baris lebih, cek di application layer
SELECT * FROM products WHERE category_id = 5 ORDER BY id LIMIT 21;
-- Jika hasil >= 21 baris → ada next page, tampilkan 20 saja
-- Jika hasil < 21 baris → tidak ada next page

Implementasi di Go:

func GetProducts(categoryID int, limit int) (*ProductPage, error) {
    // Minta satu lebih dari yang ditampilkan
    rows, err := db.Query(`
        SELECT id, name, price, created_at
        FROM products
        WHERE category_id = ?
        ORDER BY created_at DESC
        LIMIT ?
    `, categoryID, limit+1)

    var products []Product
    for rows.Next() {
        var p Product
        rows.Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt)
        products = append(products, p)
    }

    hasNextPage := len(products) > limit
    if hasNextPage {
        products = products[:limit]  // potong kembali ke limit asli
    }

    return &ProductPage{
        Products:    products,
        HasNextPage: hasNextPage,
        // Tidak ada TotalCount — tidak dibutuhkan
    }, nil
}

Pola ini cocok untuk cursor-based pagination maupun offset-based sederhana. Hasilnya: tidak ada COUNT() sama sekali.

Strategi 2: Precomputed Counter Table #

Untuk counter yang sering dibaca dan jarang berubah drastis — jumlah produk per kategori, jumlah user aktif, jumlah order per hari — simpan hasilnya di tabel counter dan update secara incremental.

-- Buat tabel counter khusus
CREATE TABLE category_stats (
    category_id  BIGINT UNSIGNED NOT NULL,
    product_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
    updated_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
                 ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (category_id)
);

-- Update counter setiap ada produk baru masuk kategori
INSERT INTO products (name, category_id, price) VALUES (?, ?, ?);
-- Lalu:
INSERT INTO category_stats (category_id, product_count)
VALUES (?, 1)
ON DUPLICATE KEY UPDATE product_count = product_count + 1;

-- Update counter saat produk dihapus dari kategori
UPDATE category_stats
SET product_count = GREATEST(0, product_count - 1)
WHERE category_id = ?;

-- ANTI-PATTERN: hitung ulang setiap request
SELECT COUNT(*) FROM products WHERE category_id = 5;  -- scan ratusan ribu baris

-- BENAR: baca dari counter table — O(1)
SELECT product_count FROM category_stats WHERE category_id = 5;
Counter table yang di-update di dalam transaksi yang sama dengan operasi utama bisa menyebabkan lock contention pada tabel kategori yang sangat aktif. Untuk sistem dengan ribuan write per detik ke satu counter, pertimbangkan strategi batch update atau Redis counter.

Strategi 3: Approximate Count — Estimasi yang Cukup Akurat #

Untuk banyak use case — terutama tampilan di UI seperti “sekitar 1,2 juta artikel” — presisi penuh tidak diperlukan. Estimasi yang cukup akurat bisa didapat tanpa scan apapun.

-- PostgreSQL: baca estimasi dari statistik query planner
SELECT reltuples::BIGINT AS estimated_count
FROM pg_class
WHERE relname = 'orders';
-- → Mengembalikan hasil dalam mikrodetik
-- → Akurasi biasanya dalam 5-10% dari nilai sebenarnya
-- → Diperbarui otomatis oleh ANALYZE (berjalan periodik atau manual)

-- MySQL: baca estimasi dari information_schema
SELECT TABLE_ROWS AS estimated_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'nama_database'
  AND TABLE_NAME = 'orders';
-- → Estimasi kasar, akurasi bervariasi
-- → Diperbarui saat ANALYZE TABLE dijalankan

Kapan approximate count cukup:

Use case yang cocok untuk estimasi:
  ✓ "Menampilkan sekitar X hasil untuk pencarian ini"
  ✓ Badge counter di sidebar ("~1.2rb artikel")
  ✓ Monitoring dashboard internal
  ✓ Statistik admin yang tidak perlu presisi penuh

Use case yang butuh presisi:
  ✗ Billing berdasarkan jumlah item
  ✗ Laporan keuangan
  ✗ SLA compliance (harus persis N item)
  ✗ Validasi integritas data

Strategi 4: Cache COUNT() dengan Redis #

Untuk endpoint yang butuh angka relatif akurat tapi tidak real-time, cache hasil COUNT() di Redis dengan TTL yang masuk akal.

func GetActiveUserCount(ctx context.Context) (int64, error) {
    cacheKey := "stats:active_users_count"

    // Coba ambil dari cache dulu
    val, err := rdb.Get(ctx, cacheKey).Int64()
    if err == nil {
        return val, nil  // cache hit
    }

    // Cache miss → hitung dari database
    var count int64
    err = db.QueryRowContext(ctx,
        "SELECT COUNT(*) FROM users WHERE status = 'active' AND deleted_at IS NULL",
    ).Scan(&count)
    if err != nil {
        return 0, err
    }

    // Simpan ke cache dengan TTL 5 menit
    rdb.Set(ctx, cacheKey, count, 5*time.Minute)

    return count, nil
}

// Invalidate cache saat status user berubah
func UpdateUserStatus(ctx context.Context, userID int64, status string) error {
    _, err := db.ExecContext(ctx,
        "UPDATE users SET status = ? WHERE id = ?", status, userID)
    if err != nil {
        return err
    }

    // Hapus cache agar saat dibaca ulang dapat nilai terbaru
    rdb.Del(ctx, "stats:active_users_count")
    return nil
}

Strategi ini cocok untuk counter yang berubah tidak terlalu sering. TTL 5 menit berarti angka yang ditampilkan maksimum 5 menit stale — untuk statistik dashboard, ini hampir selalu acceptable.

Strategi 5: Event-Driven Counter #

Untuk sistem yang sudah menggunakan message queue atau event streaming, counter bisa diupdate secara asynchronous berdasarkan event, bukan di dalam transaksi utama.

Arsitektur event-driven counter:
──────────────────────────────────────────────────────────────
  Saat user baru register:
    Application → INSERT users → publish event "user.created"
                                     │
                                     ▼
                               Queue/Kafka/Redis Pub-Sub
                                     │
                                     ▼
                               Counter Service
                                     │
                               UPDATE stats SET user_count = user_count + 1
                                     │
                                     ▼
                               Read via API: GET /stats/users
                               → baca dari stats table (O(1))

  Keuntungan:
    ✓ Transaksi utama tidak terbebani counter update
    ✓ Counter service bisa di-retry jika gagal
    ✓ Tidak ada lock contention di tabel utama
    ✓ Scalable secara independen

  Kelemahan:
    ✗ Eventual consistency — ada jeda antara event dan counter terupdate
    ✗ Lebih kompleks secara arsitektur
    ✗ Butuh mekanisme reconciliation untuk koreksi drift
──────────────────────────────────────────────────────────────

Perbandingan Lima Strategi #

Matriks keputusan strategi COUNT():
──────────────────────────────────────────────────────────────────────
  Strategi              │ Presisi │ Kecepatan │ Kompleksitas │ Use Case
──────────────────────────────────────────────────────────────────────
  COUNT(*) langsung     │ Presisi │ Lambat    │ Sangat mudah │ Tabel kecil,
                        │         │           │              │ admin report
  LIMIT+1               │ N/A     │ Cepat     │ Mudah        │ "Ada next?" saja
  Precomputed counter   │ Presisi │ O(1)      │ Sedang       │ Counter stabil
  Approximate count     │ ~95%    │ O(1)      │ Mudah        │ UI display
  Cache COUNT()         │ ~presisi│ Cepat     │ Sedang       │ Medium traffic
  Event-driven counter  │ ~presisi│ O(1)      │ Tinggi       │ High traffic
──────────────────────────────────────────────────────────────────────

Decision tree:
  Apakah angka harus presisi penuh?
    │
    ├─ Tidak → Approximate count (pg_class / information_schema)
    │
    └─ Ya → Apakah perlu tahu "ada next page"?
               │
               ├─ Hanya itu → LIMIT+1, tidak perlu COUNT sama sekali
               │
               └─ Perlu angka total → Seberapa sering berubah?
                    │
                    ├─ Jarang berubah → Precomputed counter table
                    ├─ Cukup sering  → Cache COUNT() dengan Redis
                    └─ Sering + traffic tinggi → Event-driven counter

Kapan COUNT() Langsung Masih Tepat #

COUNT() bukan fungsi yang harus dihindari sepenuhnya — ada konteks di mana penggunaannya langsung ke database masih tepat dan justified:

COUNT() langsung masih tepat untuk:
──────────────────────────────────────────────────────────────
  ✓ Tabel kecil (< 50.000 baris)
    → Scan cepat, tidak ada masalah performa

  ✓ Query admin yang jarang dijalankan
    → Laporan bulanan, audit, one-time analysis
    → Bukan hot path, tidak masalah jika butuh beberapa detik

  ✓ Background job / cron
    → Tidak bersaing dengan request user
    → Bisa dijalankan di jam sepi

  ✓ Pengecekan integritas data
    → Harus presisi, tidak bisa estimasi
    → Frekuensi rendah

COUNT() langsung TIDAK tepat untuk:
──────────────────────────────────────────────────────────────
  ✗ Endpoint pagination publik (dipanggil ribuan kali per menit)
  ✗ Dashboard real-time yang direfresh terus-menerus
  ✗ Badge counter di UI (notifikasi, pesan belum dibaca)
  ✗ Tabel dengan jutaan baris yang di-query via API
  ✗ Query dalam loop atau N+1 pattern

Anti-Pattern yang Harus Dihindari #

-- ✗ Anti-pattern 1: COUNT(*) di setiap request pagination
-- Setiap kali user buka halaman daftar, query ini dieksekusi:
SELECT COUNT(*) FROM articles WHERE status = 'published';
-- ✓ Solusi: LIMIT+1 untuk "next page", atau cache TTL 60 detik

-- ✗ Anti-pattern 2: COUNT(DISTINCT col) di hot path
SELECT COUNT(DISTINCT user_id) FROM page_views WHERE date = CURDATE();
-- → "Using temporary" setiap request, sangat mahal
-- ✓ Solusi: increment counter di Redis saat ada page view baru,
--   baca dari Redis (O(1)), flush ke database periodik

-- ✗ Anti-pattern 3: COUNT() dalam subquery yang dieksekusi berulang
SELECT *,
    (SELECT COUNT(*) FROM comments WHERE post_id = p.id) AS comment_count
FROM posts
WHERE status = 'published';
-- → COUNT() dieksekusi sekali untuk setiap baris posts (N+1!)
-- ✓ Solusi: simpan comment_count di tabel posts dan update incremental

-- ✗ Anti-pattern 4: COUNT() untuk cek existence
IF (SELECT COUNT(*) FROM users WHERE email = ?) > 0 THEN ...
-- → Hitung semua baris yang match, padahal hanya butuh tahu "ada atau tidak"
-- ✓ Solusi: gunakan EXISTS() — berhenti segera saat satu baris ditemukan
IF EXISTS(SELECT 1 FROM users WHERE email = ?) THEN ...

-- ✗ Anti-pattern 5: COUNT(*) tanpa index di kolom WHERE
SELECT COUNT(*) FROM events WHERE event_type = 'login' AND user_id = ?;
-- Jika tidak ada index (user_id, event_type): full table scan
-- ✓ Solusi: pastikan ada index yang tepat, atau gunakan counter table

EXISTS vs COUNT untuk Pengecekan Keberadaan #

Satu anti-pattern yang sangat umum adalah menggunakan COUNT() hanya untuk tahu apakah sebuah baris ada atau tidak. EXISTS() jauh lebih efisien untuk ini karena berhenti tepat saat baris pertama ditemukan.

-- ANTI-PATTERN: COUNT() untuk cek existence
SELECT COUNT(*) FROM orders WHERE user_id = 42 AND status = 'pending';
-- Database scan semua baris yang match, hitung semuanya
-- Padahal hanya perlu tahu "ada atau tidak"

-- BENAR: EXISTS() berhenti di baris pertama
SELECT EXISTS(
    SELECT 1 FROM orders WHERE user_id = 42 AND status = 'pending'
);
-- → Temukan satu baris → langsung return TRUE, tidak scan sisanya

-- Di Go:
var exists bool
db.QueryRowContext(ctx,
    "SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = ? AND status = 'pending')",
    userID,
).Scan(&exists)

-- Perbandingan performa untuk user yang punya 10.000 pending orders:
-- COUNT(*): scan 10.000 baris, hitung semua → lambat
-- EXISTS():  berhenti di baris pertama → hampir instant

Checklist Penggunaan COUNT() #

SEBELUM MENULIS SELECT COUNT():
  □ Apakah ini di hot path / endpoint yang sering dipanggil?
     Jika ya → pertimbangkan alternatif dulu
  □ Apakah hanya butuh tahu "ada atau tidak"?
     Jika ya → gunakan EXISTS(), bukan COUNT()
  □ Apakah hanya butuh tahu "ada halaman berikutnya"?
     Jika ya → gunakan LIMIT+1
  □ Apakah angkanya boleh estimasi (tidak harus presisi penuh)?
     Jika ya → gunakan approximate count dari pg_class / information_schema
  □ Apakah counter ini berubah tidak terlalu sering?
     Jika ya → pertimbangkan precomputed counter table

JIKA COUNT() MEMANG DIPERLUKAN:
  □ Ada index yang mencakup kolom di klausa WHERE?
  □ Sudah diverifikasi dengan EXPLAIN (tidak full table scan)?
  □ Apakah hasilnya bisa di-cache? Berapa TTL yang masuk akal?
  □ Apakah ini akan di-scale? Counter table lebih baik jangka panjang?

Ringkasan #

  • COUNT(*) tidak ada shortcut di InnoDB — database harus benar-benar membaca dan menghitung setiap baris yang valid, meskipun hasilnya terasa seperti “satu angka yang sederhana”. Tidak ada metadata row count yang bisa dibaca O(1).
  • COUNT(DISTINCT col) adalah yang paling mahal — memerlukan deduplikasi yang hampir selalu butuh tabel temporary. Gunakan hanya jika presisi distinct benar-benar diperlukan.
  • Gunakan EXISTS() untuk cek existence, bukan COUNT()EXISTS() berhenti di baris pertama yang ditemukan, COUNT() scan semua baris yang match. Untuk tabel besar bedanya bisa ribuan kali lebih cepat.
  • LIMIT+1 menghilangkan kebutuhan total count untuk pagination — jika UI hanya butuh tombol “Next”, tidak perlu tahu berapa total halaman. Minta satu baris lebih, cek di application layer.
  • Precomputed counter table adalah solusi terbaik untuk counter yang stabil — update incremental saat data berubah, baca O(1) kapanpun dibutuhkan. Jauh lebih scalable dari COUNT() per request.
  • Approximate count cukup untuk banyak use case UIpg_class.reltuples di PostgreSQL dan information_schema.TABLE_ROWS di MySQL mengembalikan estimasi dalam mikrodetik. Untuk “sekitar X artikel”, ini sudah lebih dari cukup.
  • Cache hasil COUNT() jika memang diperlukan — TTL 1-5 menit biasanya acceptable untuk statistik. Invalidate cache saat ada perubahan data yang signifikan.
  • Hindari COUNT() di hot path tanpa caching — endpoint yang dipanggil ribuan kali per menit dengan COUNT() di dalamnya bisa menjadi penyebab tunggal CPU database yang terus naik.

← Sebelumnya: Composite Index   Berikutnya: Pagination →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact