Bulk CUD Operation #

Hampir semua aplikasi punya kebutuhan operasi data dalam jumlah besar: impor ribuan baris dari file Excel, sinkronisasi data dari API eksternal, update status ratusan ribu order sekaligus, atau pembersihan data lama secara periodik. Cara paling naif untuk menangani ini adalah loop di application layer — satu INSERT atau UPDATE per baris, diulang ribuan kali. Di dataset kecil ini tidak terasa bermasalah. Di production dengan ratusan ribu baris, ini bisa menghabiskan menit penuh, menahan lock pada baris-baris yang dimodifikasi, menyebabkan replication lag yang mempengaruhi read replica, dan mengganggu query OLTP yang berjalan bersamaan. Bulk CUD adalah tentang mengurangi overhead per-operasi dengan mengirim lebih banyak pekerjaan dalam satu statement atau satu transaksi — tapi dengan cara yang tidak merusak ketersediaan sistem. Artikel ini membahas strategi terpisah untuk bulk INSERT, UPDATE, dan DELETE, lengkap dengan implementasi Go dan penanganan partial failure.

Mengapa Operasi Per-Row Sangat Mahal #

Setiap operasi CUD yang berdiri sendiri menanggung biaya tetap yang sama, terlepas dari seberapa sedikit data yang dimodifikasi:

Biaya per-operasi untuk satu INSERT tunggal:
──────────────────────────────────────────────────────────────
  1. Parse dan validasi SQL
  2. Akuisisi row lock
  3. Tulis ke data page (mungkin butuh allocate page baru)
  4. Update SEMUA index yang relevan (B-Tree rebalancing)
  5. Tulis ke WAL/redo log (dengan fsync jika durability = full)
  6. Update MVCC metadata (PostgreSQL)
  7. Commit → fsync WAL ke disk
  8. Lepas lock
  9. Return acknowledgement ke client (network roundtrip)
──────────────────────────────────────────────────────────────
  Untuk 10.000 INSERT satu per satu:
    Langkah 1–9 diulang 10.000 kali
    fsync WAL: 10.000 kali → bisa 10–30 detik hanya dari I/O

  Untuk 10.000 INSERT dalam satu multi-row statement:
    Langkah 1–9 terjadi SEKALI
    fsync WAL: 1 kali
    Total: ratusan milidetik
──────────────────────────────────────────────────────────────

Perbedaan ini bisa mencapai 100× atau lebih tergantung konfigurasi innodb_flush_log_at_trx_commit dan ukuran data.


Bulk CREATE: Multi-Row INSERT #

Sintaks Multi-Row dan Perbedaannya #

-- ANTI-PATTERN: satu INSERT per baris — N roundtrip, N commit, N WAL fsync
INSERT INTO products (name, price, stock) VALUES ('Laptop', 9500000, 10);
INSERT INTO products (name, price, stock) VALUES ('Mouse', 150000, 50);
INSERT INTO products (name, price, stock) VALUES ('Keyboard', 350000, 30);
-- ... diulang 10.000 kali

-- BENAR: multi-row INSERT — 1 roundtrip, 1 commit, 1 WAL fsync
INSERT INTO products (name, price, stock) VALUES
    ('Laptop',   9500000, 10),
    ('Mouse',    150000,  50),
    ('Keyboard', 350000,  30),
    -- ... hingga ratusan atau ribuan baris per statement
    ('Monitor',  4500000, 15);

INSERT dengan ON DUPLICATE KEY untuk Upsert #

Saat mengimpor data yang mungkin sudah ada (misalnya sinkronisasi dari sistem lain), gunakan ON DUPLICATE KEY UPDATE untuk menggabungkan insert dan update dalam satu operasi:

-- Upsert: insert jika belum ada, update jika sudah ada
INSERT INTO products (sku, name, price, stock, updated_at)
VALUES
    ('SKU-001', 'Laptop Pro', 9500000, 10, NOW()),
    ('SKU-002', 'Mouse Wireless', 150000, 50, NOW()),
    ('SKU-003', 'Keyboard Mekanikal', 350000, 30, NOW())
ON DUPLICATE KEY UPDATE
    name       = VALUES(name),
    price      = VALUES(price),
    stock      = VALUES(stock),
    updated_at = VALUES(updated_at);
-- Jika sku sudah ada: update kolom-kolom tersebut
-- Jika sku belum ada: insert baris baru
-- Semua dalam satu roundtrip

Implementasi Bulk INSERT di Go dengan Chunking #

Untuk dataset besar (> 10.000 baris), jangan kirim semua dalam satu statement — ini bisa membuat query terlalu panjang dan menahan lock terlalu lama. Chunk data menjadi batch yang lebih kecil:

type Product struct {
    SKU   string
    Name  string
    Price int64
    Stock int
}

// BulkUpsertProducts melakukan upsert dalam batch untuk menghindari
// lock berkepanjangan dan query yang terlalu besar
func BulkUpsertProducts(ctx context.Context, db *sql.DB, products []Product) error {
    const batchSize = 500  // tuning berdasarkan ukuran row dan memory

    for i := 0; i < len(products); i += batchSize {
        end := i + batchSize
        if end > len(products) {
            end = len(products)
        }
        batch := products[i:end]

        if err := upsertBatch(ctx, db, batch); err != nil {
            return fmt.Errorf("batch %d-%d gagal: %w", i, end, err)
        }

        // Throttle antar batch: beri napas ke database dan replica
        // Sesuaikan dengan beban sistem
        if end < len(products) {
            time.Sleep(10 * time.Millisecond)
        }
    }
    return nil
}

func upsertBatch(ctx context.Context, db *sql.DB, products []Product) error {
    // Bangun query multi-row secara dinamis
    valueStrings := make([]string, len(products))
    valueArgs := make([]interface{}, 0, len(products)*4)

    for i, p := range products {
        valueStrings[i] = "(?, ?, ?, ?)"
        valueArgs = append(valueArgs, p.SKU, p.Name, p.Price, p.Stock)
    }

    query := fmt.Sprintf(`
        INSERT INTO products (sku, name, price, stock)
        VALUES %s
        ON DUPLICATE KEY UPDATE
            name  = VALUES(name),
            price = VALUES(price),
            stock = VALUES(stock)
    `, strings.Join(valueStrings, ","))

    _, err := db.ExecContext(ctx, query, valueArgs...)
    return err
}

LOAD DATA INFILE untuk Volume Sangat Besar #

Untuk impor file CSV dengan jutaan baris, LOAD DATA INFILE (MySQL) atau COPY FROM (PostgreSQL) adalah cara tercepat karena melewati banyak overhead SQL parsing:

-- MySQL: impor dari file CSV langsung ke tabel
LOAD DATA LOCAL INFILE '/tmp/products.csv'
INTO TABLE products
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS  -- skip header
(sku, name, price, stock);

-- Untuk PostgreSQL:
COPY products (sku, name, price, stock)
FROM '/tmp/products.csv'
WITH (FORMAT CSV, HEADER true, DELIMITER ',');
Perbandingan kecepatan impor 1 juta baris:
──────────────────────────────────────────────────────────────
  INSERT satu per satu:      ~30 menit
  Multi-row INSERT (500/batch): ~2 menit
  LOAD DATA INFILE / COPY:   ~15 detik
──────────────────────────────────────────────────────────────
  LOAD DATA/COPY jauh lebih cepat karena:
    - Parsing minimal (format binary/CSV, bukan SQL)
    - Index update bisa ditunda dan dilakukan sekali di akhir
    - Tidak ada roundtrip per baris

Bulk UPDATE: Strategi Berdasarkan Skenario #

Bulk UPDATE adalah yang paling beragam kasusnya. Strateginya berbeda tergantung apakah semua baris diupdate dengan nilai yang sama, nilai berbeda per baris, atau hanya baris yang memenuhi kondisi tertentu.

Update Nilai Sama untuk Banyak Baris #

-- ANTI-PATTERN: update satu per satu
UPDATE orders SET status = 'expired' WHERE id = 1001;
UPDATE orders SET status = 'expired' WHERE id = 1002;
UPDATE orders SET status = 'expired' WHERE id = 1003;
-- ... N query untuk N order

-- BENAR: WHERE IN — satu query, satu lock acquisition
UPDATE orders
SET status = 'expired', updated_at = NOW()
WHERE id IN (1001, 1002, 1003, ...)
  AND status = 'pending';  -- guard kondisi untuk safety

Update Nilai Berbeda per Baris dengan CASE #

-- ANTI-PATTERN: update per baris dengan nilai berbeda
UPDATE products SET price = 9500000 WHERE id = 1;
UPDATE products SET price = 150000  WHERE id = 2;
UPDATE products SET price = 350000  WHERE id = 3;

-- BENAR: CASE WHEN untuk update massal dengan nilai berbeda
UPDATE products
SET price = CASE id
    WHEN 1 THEN 9500000
    WHEN 2 THEN 150000
    WHEN 3 THEN 350000
    ELSE price  -- ← penting: jangan ubah baris lain
END,
updated_at = NOW()
WHERE id IN (1, 2, 3);  -- ← batasi scope dengan WHERE

Implementasi Go untuk Bulk UPDATE dengan Nilai Berbeda #

type PriceUpdate struct {
    ProductID int64
    NewPrice  int64
}

func BulkUpdatePrices(ctx context.Context, db *sql.DB, updates []PriceUpdate) error {
    if len(updates) == 0 {
        return nil
    }

    const batchSize = 300

    for i := 0; i < len(updates); i += batchSize {
        end := i + batchSize
        if end > len(updates) {
            end = len(updates)
        }
        batch := updates[i:end]

        if err := updatePricesBatch(ctx, db, batch); err != nil {
            return fmt.Errorf("price update batch %d-%d gagal: %w", i, end, err)
        }

        time.Sleep(5 * time.Millisecond)  // throttle
    }
    return nil
}

func updatePricesBatch(ctx context.Context, db *sql.DB, updates []PriceUpdate) error {
    // Bangun CASE WHEN secara dinamis
    caseExpr := strings.Builder{}
    caseExpr.WriteString("CASE id ")

    ids := make([]interface{}, 0, len(updates))
    args := make([]interface{}, 0, len(updates)+len(updates))

    for _, u := range updates {
        caseExpr.WriteString("WHEN ? THEN ? ")
        args = append(args, u.ProductID, u.NewPrice)
        ids = append(ids, u.ProductID)
    }
    caseExpr.WriteString("ELSE price END")

    // Buat placeholder untuk IN clause
    placeholders := strings.Repeat("?,", len(ids)-1) + "?"
    allArgs := append(args, ids...)

    query := fmt.Sprintf(`
        UPDATE products
        SET price = %s, updated_at = NOW()
        WHERE id IN (%s)
    `, caseExpr.String(), placeholders)

    result, err := db.ExecContext(ctx, query, allArgs...)
    if err != nil {
        return err
    }

    affected, _ := result.RowsAffected()
    if affected != int64(len(updates)) {
        // Beberapa ID tidak ditemukan — log warning tapi tidak error
        // Tergantung business requirement
        log.Warn("some products not found during bulk price update",
            "expected", len(updates), "actual", affected)
    }
    return nil
}

Bulk UPDATE dengan Tabel Sementara (untuk Jumlah Sangat Besar) #

Untuk update dengan nilai berbeda per baris dalam jumlah sangat besar (> 50.000 baris), CASE WHEN bisa menjadi terlalu panjang. Alternatifnya adalah menggunakan tabel sementara:

-- Langkah 1: buat dan isi tabel sementara
CREATE TEMPORARY TABLE temp_price_updates (
    product_id BIGINT NOT NULL,
    new_price  BIGINT NOT NULL,
    PRIMARY KEY (product_id)
);

INSERT INTO temp_price_updates (product_id, new_price) VALUES
    (1, 9500000), (2, 150000), (3, 350000), ...;

-- Langkah 2: UPDATE via JOIN ke tabel sementara
UPDATE products p
JOIN temp_price_updates t ON p.id = t.product_id
SET p.price = t.new_price, p.updated_at = NOW();

-- Langkah 3: bersihkan tabel sementara (opsional, otomatis saat session berakhir)
DROP TEMPORARY TABLE IF EXISTS temp_price_updates;

Bulk DELETE: Aman dan Reversible #

Bulk DELETE adalah operasi yang paling berisiko karena tidak bisa di-undo. Strategi yang baik harus mempertimbangkan keamanan data, dampak ke index, dan kemampuan rollback.

Soft Delete sebagai Safety Net #

-- ANTI-PATTERN: hard delete langsung — tidak bisa di-undo
DELETE FROM orders WHERE created_at < '2024-01-01';

-- BENAR — Langkah 1: soft delete dulu (tandai sebagai dihapus)
UPDATE orders
SET deleted_at = NOW()
WHERE created_at < '2024-01-01'
  AND deleted_at IS NULL;

-- Verifikasi jumlah yang akan dihapus sebelum hard delete
SELECT COUNT(*) FROM orders
WHERE deleted_at IS NOT NULL
  AND deleted_at < NOW() - INTERVAL 7 DAY;

-- Langkah 2: archive ke tabel historis sebelum hard delete
INSERT INTO orders_archive
SELECT * FROM orders
WHERE deleted_at IS NOT NULL
  AND deleted_at < NOW() - INTERVAL 7 DAY;

-- Langkah 3: hard delete setelah diverifikasi dan diarsipkan
DELETE FROM orders
WHERE deleted_at IS NOT NULL
  AND deleted_at < NOW() - INTERVAL 7 DAY;

Chunked DELETE untuk Menghindari Lock Panjang #

DELETE besar dalam satu transaksi bisa menahan lock pada banyak baris sekaligus, memblokir query lain yang butuh mengakses baris-baris tersebut. Chunked DELETE membagi operasi menjadi batch kecil:

// ChunkedDelete menghapus data lama dalam batch kecil untuk meminimalkan lock
func ChunkedDeleteOldOrders(ctx context.Context, db *sql.DB, olderThan time.Time) (int64, error) {
    const chunkSize = 1000
    var totalDeleted int64

    for {
        // Hapus maksimum chunkSize baris per iterasi
        result, err := db.ExecContext(ctx, `
            DELETE FROM orders
            WHERE created_at < ?
              AND status IN ('completed', 'cancelled')
              AND deleted_at IS NOT NULL
            LIMIT ?
        `, olderThan, chunkSize)
        if err != nil {
            return totalDeleted, fmt.Errorf("chunk delete gagal: %w", err)
        }

        affected, _ := result.RowsAffected()
        totalDeleted += affected

        if affected == 0 {
            break  // Tidak ada lagi yang perlu dihapus
        }

        // Log progress untuk operasi yang lama
        log.Info("delete progress", "deleted_so_far", totalDeleted)

        // Jeda antar chunk — beri napas ke database dan replica
        select {
        case <-ctx.Done():
            return totalDeleted, ctx.Err()  // Hormati cancellation
        case <-time.After(50 * time.Millisecond):
        }
    }

    return totalDeleted, nil
}
Jangan pernah menjalankan DELETE FROM table tanpa WHERE clause atau LIMIT untuk tabel production yang besar. Satu query DELETE tanpa batasan bisa menahan lock seluruh tabel selama menit hingga jam, memblokir semua operasi read dan write. Selalu gunakan chunked DELETE dengan LIMIT dan jeda antar batch.

Dampak Bulk CUD ke Sistem: Yang Harus Dipantau #

Replication Lag #

Bulk CUD menghasilkan volume besar di binlog/WAL yang harus direplikasi ke read replica. Tanpa throttling, replica bisa tertinggal jauh dari primary:

Dampak bulk INSERT 1 juta baris ke replikasi:
──────────────────────────────────────────────────────────────
  Primary:
    INSERT selesai dalam 30 detik
    Binlog berukuran: ~500 MB

  Read replica:
    Harus memutar ulang 500 MB operasi binlog
    Bisa membutuhkan 2-5 menit
    Selama itu: query ke replica membaca data lama

  Dampak ke aplikasi:
    Cache yang di-invalidate tapi baca dari replica
    → Mendapat data lama → stale data issue
──────────────────────────────────────────────────────────────

Pantau replication lag sebelum dan selama operasi bulk:

-- MySQL: cek replication lag di replica
SHOW SLAVE STATUS\G
-- Lihat: Seconds_Behind_Master

-- PostgreSQL: cek dari primary
SELECT
    client_addr,
    state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    (sent_lsn - replay_lsn) AS replication_lag_bytes
FROM pg_stat_replication;

Index Maintenance selama Bulk Write #

Setiap baris yang diinsert atau diupdate memerlukan update di semua index yang ada di tabel tersebut. Untuk tabel dengan banyak index, ini bisa menjadi bottleneck:

-- Lihat berapa index yang ada di tabel target
SHOW INDEX FROM orders;
-- Jika ada 8 index: setiap INSERT mengupdate 8 B-Tree

-- Untuk bulk import besar di maintenance window:
-- Langkah 1: drop index non-primary
ALTER TABLE products DROP INDEX idx_products_name;
ALTER TABLE products DROP INDEX idx_products_category_price;

-- Langkah 2: insert data (jauh lebih cepat tanpa index maintenance)
INSERT INTO products ...;  -- atau LOAD DATA INFILE

-- Langkah 3: rebuild index setelah insert selesai
ALTER TABLE products ADD INDEX idx_products_name (name);
ALTER TABLE products ADD INDEX idx_products_category_price (category_id, price);

-- Catatan: langkah ini hanya cocok untuk maintenance window
-- Jangan lakukan di saat traffic aktif karena tabel tidak punya index sementara

Statistik Table yang Usang setelah Bulk DELETE #

Setelah menghapus banyak baris, statistik yang digunakan query planner bisa menjadi tidak akurat, menyebabkan planner memilih execution plan yang salah:

-- Setelah bulk DELETE, update statistik:
-- MySQL
ANALYZE TABLE orders;

-- PostgreSQL
ANALYZE orders;
VACUUM ANALYZE orders;  -- juga bersihkan dead tuples

-- Pantau efeknya:
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > '2026-01-01';
-- Cek apakah rows estimate sekarang lebih akurat

Idempotency dalam Bulk CUD #

Operasi bulk yang dijalankan berulang (retry setelah gagal, job yang berjalan dua kali) harus menghasilkan state yang sama — tidak boleh ada duplikasi atau state yang tidak konsisten.

-- ANTI-PATTERN: INSERT yang tidak idempotent
INSERT INTO order_logs (order_id, status, created_at)
VALUES (?, 'processed', NOW());
-- Jika dijalankan dua kali: dua baris identik di order_logs

-- BENAR: idempotent dengan ON DUPLICATE KEY DO NOTHING
INSERT INTO order_logs (order_id, status, processed_at)
VALUES (?, 'processed', NOW())
ON DUPLICATE KEY UPDATE processed_at = processed_at;
-- Jika sudah ada: no-op
-- Membutuhkan UNIQUE constraint di (order_id, status)

-- Atau dengan PostgreSQL:
INSERT INTO order_logs (order_id, status, processed_at)
VALUES (?, 'processed', NOW())
ON CONFLICT (order_id, status) DO NOTHING;
// Pola idempotent untuk bulk job yang bisa di-retry:
type OrderProcessResult struct {
    OrderID   int64
    Processed bool
    Error     error
}

func ProcessOrdersBatch(ctx context.Context, db *sql.DB, orderIDs []int64) []OrderProcessResult {
    results := make([]OrderProcessResult, len(orderIDs))

    // Cek mana yang sudah diproses sebelumnya
    processed, _ := getAlreadyProcessed(ctx, db, orderIDs)
    processedSet := make(map[int64]bool)
    for _, id := range processed {
        processedSet[id] = true
    }

    // Filter hanya yang belum diproses
    var toProcess []int64
    for _, id := range orderIDs {
        if !processedSet[id] {
            toProcess = append(toProcess, id)
        }
    }

    // Proses hanya yang belum
    if len(toProcess) > 0 {
        _ = bulkMarkProcessed(ctx, db, toProcess)
    }

    // Build result — idempotent: yang sudah diproses dianggap sukses
    for i, id := range orderIDs {
        results[i] = OrderProcessResult{
            OrderID:   id,
            Processed: true,  // baik yang baru maupun yang sudah ada
        }
    }
    return results
}

Anti-Pattern yang Harus Dihindari #

-- ✗ Anti-pattern 1: INSERT satu per satu dalam loop — N roundtrip, N WAL fsync
-- (di Go)
for _, product := range products {
    db.ExecContext(ctx, "INSERT INTO products VALUES (?, ?, ?)",
        product.Name, product.Price, product.Stock)
}
-- ✓ Solusi: multi-row INSERT dengan batch 500 baris

-- ✗ Anti-pattern 2: UPDATE tanpa LIMIT atau WHERE yang membatasi scope
UPDATE orders SET status = 'reviewed';
-- Ini mengupdate SEMUA order, menahan lock seluruh tabel
-- ✓ Solusi: UPDATE ... WHERE ... LIMIT N dengan chunking

-- ✗ Anti-pattern 3: bulk DELETE tanpa archive
DELETE FROM logs WHERE created_at < '2025-01-01';
-- Data hilang permanen, tidak ada recovery
-- ✓ Solusi: soft delete → archive ke tabel history → hard delete

-- ✗ Anti-pattern 4: bulk CUD di jam peak traffic
-- Job nightly yang terjadwal pukul 19:00 saat user paling banyak aktif
-- ✓ Solusi: jadwalkan di jam sepi (02:00-05:00), atau gunakan throttling

-- ✗ Anti-pattern 5: satu transaksi untuk seluruh batch besar
tx.Begin()
for i := 0; i < 1000000; i++ {
    tx.Exec("INSERT ...")  // 1 juta baris dalam satu transaksi
}
tx.Commit()
-- Lock dipegang selama seluruh 1 juta INSERT
-- ✓ Solusi: satu transaksi per batch (500-1000 baris)

-- ✗ Anti-pattern 6: bulk operation tanpa monitoring replication lag
-- Job selesai, tapi read replica masih 5 menit tertinggal
-- Query ke replica mengembalikan data lama
-- ✓ Solusi: pantau replication lag, tambahkan jeda jika lag melebihi threshold

Checklist Bulk CUD yang Aman #

PERSIAPAN SEBELUM OPERASI:
  □ Sudah ada backup terbaru (atau snapshot) dari data yang akan dimodifikasi?
  □ Operasi dijadwalkan di luar jam peak traffic?
  □ Ada mekanisme rollback jika operasi gagal di tengah jalan?
  □ Batch size sudah ditentukan (rekomendasi: 500-1000 untuk INSERT, 100-500 untuk DELETE)?
  □ Ada throttle antar batch untuk menjaga replication lag?

UNTUK BULK INSERT:
  □ Menggunakan multi-row INSERT, bukan satu INSERT per baris?
  □ ON DUPLICATE KEY / ON CONFLICT dipakai untuk upsert yang idempotent?
  □ Batch size tidak terlalu besar (max ~1000 baris per statement)?

UNTUK BULK UPDATE:
  □ WHERE clause membatasi scope dengan jelas (bukan UPDATE semua baris)?
  □ Menggunakan IN clause atau CASE WHEN, bukan loop per baris?
  □ Kolom yang diupdate hanya yang benar-benar berubah?

UNTUK BULK DELETE:
  □ Ada soft delete sebelum hard delete?
  □ Ada archiving sebelum hard delete untuk data yang perlu disimpan?
  □ Menggunakan chunked DELETE dengan LIMIT, bukan DELETE tanpa LIMIT?

SETELAH OPERASI:
  □ Statistik tabel diupdate jika ada DELETE besar (ANALYZE)?
  □ Replication lag kembali ke normal?
  □ Query planner masih menggunakan execution plan yang benar (EXPLAIN)?

Ringkasan #

  • Satu INSERT satu per satu = N WAL fsync — setiap INSERT tunggal memerlukan flush log ke disk untuk durability. Multi-row INSERT melakukan ini sekali untuk ratusan baris. Perbedaan throughput bisa 100× atau lebih.
  • Batch size yang tepat adalah kunci — terlalu kecil (10 baris) tidak efisien, terlalu besar (100.000 baris) menahan lock terlalu lama. Angka 500-1000 baris per batch adalah titik awal yang baik untuk kebanyakan kasus.
  • Throttle antar batch melindungi replication lag — jeda 10-50ms antar batch memberi replica waktu untuk mengejar, mencegah read replica tertinggal jauh dan menyajikan data stale.
  • Bulk DELETE wajib melalui soft delete + archive — hard delete langsung tidak bisa di-undo. Soft delete dulu, verifikasi, archive jika perlu, baru hard delete dengan chunking.
  • CASE WHEN untuk update nilai berbeda per baris — lebih efisien dari loop per baris karena hanya satu roundtrip dan satu lock acquisition. Untuk jumlah sangat besar (>50.000), gunakan tabel sementara + UPDATE JOIN.
  • Idempotency adalah wajib untuk bulk job — job yang bisa di-retry harus menghasilkan state yang sama meski dijalankan dua kali. Gunakan ON DUPLICATE KEY DO NOTHING atau ON CONFLICT DO NOTHING.
  • Pantau replication lag selama dan setelah operasi — bulk CUD yang besar bisa membuat read replica tertinggal menit hingga jam, menyebabkan query ke replica mengembalikan data lama.
  • Jalankan ANALYZE setelah bulk DELETE besar — statistik query planner bisa menjadi tidak akurat setelah banyak baris dihapus, menyebabkan planner memilih execution plan yang salah.

← Sebelumnya: Database Roundtrip   Berikutnya: Full-Text Index →

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