Upsert bukan merupakan fungsi ataupun perintah bawaan dari SQL/DuckDB, upsert merupakah kepanjangan dari Update Insert, atau bisa dibilang bila sesuai dengan kriteria tertentu data di update, selain itu di insert.

Upsert tidak terbatas pada DuckDB, tetapi bisa digunakan di semua database (DBMS). Yang perlu diingat dari upsert ini adalah logicnya

if match 
  merge
else
  insert

Contoh kasus yang umum menggunakan upsert adalah menggabungkan data harian dengan data utama, sinkronisasi data antara data customer dengan data harian customer.

Pada contoh kali ini kita memiliki data customer sebanyak 10 juta data, dan data harian (delta) ada sebanyak 5 juta. Data harian disini bisa merupakan data customer yang melakukan pengkinian data, atapun nasabah yang melakukan pembelian di hari kemarin

-rw-r--r--@ 1 tommy  staff   554M Aug  3 20:32 customers-20260803.csv
-rw-r--r--@ 1 tommy  staff   1.1G Aug  1 16:33 customers.csv
# total baris/row
5000006 customers-20260803.csv
10000001 customers.csv
15000007 total

Logik yang akan kita gunakan adalah update semua data customer yang ada di customer.csv dengan data customer dari data harian (customers-20260803.csv) bila customer_id tidak sama, maka insert data tersebut ke customer.

Buat table penampung untuk kedua file tersebut, table customers untuk customers.csv dan customers_staging untuk customers-20260803.csv

CREATE TABLE customers AS SELECT * FROM read_csv('/Users/tommy/git/duckdb/generator/customers.csv');
CREATE TABLE customers_staging AS SELECT * FROM read_csv('/Users/tommy/git/duckdb/generator/customers-20260803.csv');

kita membuat table di DuckDB untuk memudahkan proses merger data, karena bila memproses data CSV itu sendiri akan butuh waktu lama, fungsi dari read_csv() sebagai informasi kepada duckdb bahwa kita akan me-load file CSV, biasanya bila file yang diload menggunakan ekstensi .csv tidak perlu memanggil fungsi tersebut, contohnya

create table customers_staging as SELECT * from '/Users/tommy/git/duckdb/generator/customers-20260803.csv';
# atau
create table customers_staging as from '/Users/tommy/git/duckdb/generator/customers-20260803.csv';

tetapi buat saya sudah menjadi kebiasaan untuk memanggil fungsi tersebut dan membuat query lebih mudah dicerna.

Lihat nama kolom yang ada di table customers dan customers_staging

select * from customers limit 10;
┌──────────────┬──────────────────────┬────────────┬───────────┬────────────────────────────┬─────────────────────┬───────────────┬───────────┬─────────────┬────────────┐
│ customer_id  │         nik          │ first_name │ last_name │           email            │     occupation      │ annual_income │ aml_score │ risk_rating │ kyc_status │
├──────────────┼──────────────────────┼────────────┼───────────┼────────────────────────────┼─────────────────────┼───────────────┼───────────┼─────────────┼────────────┤
│ CUST00000001 │ 86426737233752480063 │ Dewi       │ Santoso   │ Dewi.Santoso1@example.com  │ Business Owner      │ 50736378336        │ MEDIUM      │ VERIFIED   │
│ CUST00000002 │ 4014692691920971457  │ Siti       │ Lestari   │ Siti.Lestari2@example.com  │ Teacher             │ 99087431052        │ MEDIUM      │ VERIFIED   │
│ CUST00000003 │ 5039961615518443433  │ Budi       │ Kusuma    │ Budi.Kusuma3@example.com   │ Government Employee │ 92778634527        │ LOW         │ VERIFIED   │
│ CUST00000004 │ 4902226749561653363  │ Budi       │ Nugroho   │ Budi.Nugroho4@example.com  │ Business Owner      │ 77119838236        │ MEDIUM      │ VERIFIED   │
│ CUST00000005 │ 12283938685504891528 │ Badi       │ Hidayat   │ Badi.Hidayat5@example.com  │ Farmer              │ 73286565133        │ MEDIUM      │ VERIFIED   │
│ CUST00000006 │ 57803678760381626269 │ Putri      │ Wijaya    │ Putri.Wijaya6@example.com  │ Farmer              │ 1580026255         │ LOW         │ VERIFIED   │
│ CUST00000007 │ 32031525386391614419 │ Rizki      │ Hidayat   │ Rizki.Hidayat7@example.com │ Government Employee │ 63938106146        │ MEDIUM      │ VERIFIED   │
│ CUST00000008 │ 890138177540624232   │ Guni       │ Kusuma    │ Guni.Kusuma8@example.com   │ Student             │ 1214950173         │ LOW         │ VERIFIED   │
│ CUST00000009 │ 87859418700110855694 │ Fajar      │ Wijaya    │ Fajar.Wijaya9@example.com  │ Farmer              │ 16961010364        │ MEDIUM      │ VERIFIED   │
│ CUST00000010 │ 91679143740333945897 │ Dani       │ Hidayat   │ Dani.Hidayat10@example.com │ Doctor              │ 88374602739        │ MEDIUM      │ VERIFIED   │
└──────────────┴──────────────────────┴────────────┴───────────┴────────────────────────────┴─────────────────────┴───────────────┴───────────┴─────────────┴────────────┘

select 10 row from table

Cek Data Tabel

Check data

SELECT MAX(customer_id) FROM customer;
// CUST10000000
SELECT MAX(customer_id) FROM customers_staging;
// CUST10000006

dari hasil diatas maksimum customer_id yang ada di customers_staging lebih besar dibandingkan customers, jadi nanti bakalan ada proses insert.

Hitung jumlah data di tabel

SELECT COUNT(*) FROM customers;
// hasilnya 10000000
SELECT COUNT(*) FROM customers_staging;
// hasilnya 5000006

Cek data customer yang tidak ada di customer_staging, begitu juga sebaliknya

SELECT COUNT(target.customer_id)
FROM customers_staging target
WHERE NOT EXISTS (
    SELECT 1 
    FROM customers SOURCE
    WHERE SOURCE.customer_id = target.customer_id
);
// hasilnya 6
SELECT COUNT(SOURCE.customer_id)
FROM customers SOURCE
WHERE NOT EXISTS (
    SELECT 1
    FROM customers_staging target
    WHERE SOURCE.customer_id = target.customer_id
);
// hasilnya 50000000

cukup menjalankan salah satu perintah diatas harusnya sudah terlihat perbedaan dari datanya, di table customers_staging ada 5000006 (lima juta enam data), begitu dibandingin dengan tabel customer yang punya customer_id yang sama 5,000,000, berarti ada 6 customer_id yang belum ada (baru) di customer.

sebelum kita melakukan upsert, hal yang perlu kita ketahui adalah primary key dari data customer tersebut dan harus unik di setiap data. Dari data diatas primary key (PK) adalah customer_id, ini karena aplikasi ecommerce, sumber dari file ini menggunakan customer_id sebagai PK. Ada baiknya disini kita harus benar-benar paham tiap kolom yang ada, tipe data dan panjang (lenght) agar memudahkan untuk pembuatan upsert.

MERGE INTO customers AS target
USING customers_staging AS SOURCE
ON target.customer_id = SOURCE.customer_id
 
WHEN MATCHED THEN
    UPDATE SET
		nik = SOURCE.nik,
		first_name = SOURCE.first_name,
		last_name = SOURCE.last_name,
		email = SOURCE.email,
		occupation = SOURCE.occupation,
		annual_income = SOURCE.annual_income,
		aml_score = SOURCE.aml_score,
		risk_rating = SOURCE.risk_rating,
		kyc_status = SOURCE.kyc_status
 
WHEN NOT MATCHED THEN
    INSERT (customer_id,nik,first_name,last_name,email,occupation,annual_income,aml_score,risk_rating,kyc_status)
    VALUES (
		SOURCE.customer_id,
		SOURCE.nik,
		SOURCE.first_name,
		SOURCE.last_name,
		SOURCE.email,
		SOURCE.occupation,
		SOURCE.annual_income,
		SOURCE.aml_score,
		SOURCE.risk_rating,
		SOURCE.kyc_status
    );

pada contoh diatas, bila target.customer_id = source.customer_id maka akan menjalankan WHEN MATCHED THEN, bila tidak menjalankan bagian WHEN NOT MATCHED THEN.

Di system yang saya gunakan, butuh 4 detik untuk menjalankan upsert tersebut

Run Time (s): real 4.610 user 3.085370 sys 1.856091

bila dilihat jumlah baris customers 10000006

memory D select count(*) from customers;
┌──────────────┐
│ count_star() │
├──────────────┤
│ 10000006     │
└──────────────┘

prosesnya sangat cepat karena semua table di load ke memory, dan memory yang tersebut cukup untuk menampung data table tersebut.

Untuk ukuran data yang lebih besar dari memory, ada beberapa cara yang bisa digunakan:
1. Hapus semua data yang match antara tabel customer dan customer_staging, baru insert
2. Hanya pilih kolom yang akan di update, contoh nik dan annual_income
3. Gunakan SSD ataupun simpan data temporary di SSD (SET temp_directory = '/data/ssd/10TB';)

Terakhir simpan file tersebut ke CSV

COPY (SELECT * FROM customers) TO '/Users/tommy/git/duckdb/generator/customers-full.csv' (FORMAT csv);

Leave a comment

Your email address will not be published. Required fields are marked *