Membersihkan data & konten warisan
Databasemu berumur bertahun-tahun dan sudah melewati beberapa sistem. Migrasi platform adalah kesempatan terbaik membersihkannya — dan satu-satunya waktu di mana biayanya bisa dibenarkan.
Intisari
latin1atauutf8mb3di tabel lama berarti emoji dan sebagian karakter tidak bisa disimpan.- Konversi ke
utf8mb4menyalin seluruh tabel — jadwalkan, jangan selipkan di deploy biasa. - Bahaya terbesar: teks yang sudah rusak (mojibake) dari konversi sebelumnya yang tidak lengkap.
- HTML badan artikel biasanya campuran dari beberapa generasi editor — normalkan bertahap, jangan sekaligus.
- Inventarisasi dulu, ukur dulu, baru putuskan apa yang layak dibersihkan.
Inventarisasi charset
SELECT TABLE_NAME, TABLE_COLLATION,
ROUND(DATA_LENGTH / 1024 / 1024) AS mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'portal'
ORDER BY DATA_LENGTH DESC;
-- Per kolom, karena kolom bisa berbeda dari tabelnya
SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'portal'
AND CHARACTER_SET_NAME IS NOT NULL
AND CHARACTER_SET_NAME <> 'utf8mb4';
| Charset | Masalahnya |
|---|---|
latin1 | Tidak bisa menyimpan karakter non-Latin maupun emoji |
utf8 / utf8mb3 | Maksimal 3 byte per karakter — emoji gagal |
utf8mb4 | Yang benar |
utf8 di MySQL bukan UTF-8 yang lengkap. Ia alias untuk utf8mb3, yang
maksimal tiga byte per karakter — cukup untuk sebagian besar bahasa, tapi tidak untuk emoji dan sebagian
aksara. Gejalanya di portal berita: judul artikel yang mengandung emoji terpotong tepat di emoji itu,
atau seluruh INSERT gagal dengan "Incorrect string value".
Mendeteksi teks yang sudah rusak
-- Mojibake khas: UTF-8 yang pernah dibaca sebagai latin1
SELECT id, judul FROM artikel
WHERE judul LIKE '%Ã%' OR judul LIKE '%â€%' OR judul LIKE '%Â%'
LIMIT 50;
| Terlihat sebagai | Seharusnya |
|---|---|
“ | " (kutip kiri) |
’ | ' (apostrof) |
é | é |
 | spasi tak terputus |
Kalau kamu menemukan pola ini, jangan langsung ALTER TABLE. Konversi charset pada
data yang sudah rusak akan mengunci kerusakannya — teks yang sebelumnya bisa dipulihkan jadi
benar-benar hilang. Perbaiki dulu datanya, baru konversi tabelnya. Urutan terbalik adalah kesalahan yang
tidak bisa dibatalkan tanpa memulihkan dari cadangan.
-- Perbaikan mojibake: baca ulang byte-nya sebagai latin1, tafsirkan sebagai utf8mb4.
-- UJI DI SALINAN DULU. Selalu.
UPDATE artikel
SET judul = CONVERT(BINARY(CONVERT(judul USING latin1)) USING utf8mb4)
WHERE judul LIKE '%Ã%' OR judul LIKE '%â€%';
Konversi charset
-- Cek dulu berapa lama dan apakah bisa tanpa mengunci
ALTER TABLE artikel
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
ALGORITHM=INPLACE, LOCK=NONE;
-- Kalau ditolak: operasinya butuh menyalin tabel dan MENGUNCI.
| Ukuran tabel | Perkiraan waktu | Pendekatan |
|---|---|---|
| < 100 MB | Detik | Langsung saja |
| 100 MB – 2 GB | Menit | Jam sepi |
| > 2 GB | Puluhan menit sampai jam | gh-ost atau pt-online-schema-change |
Ingat dampaknya ke replica (Fase 2). ALTER TABLE besar dijalankan ulang di
replica, dan selama itu lag-nya melonjak. Kalau pembacaan portalmu sudah dialihkan ke replica, pembaca
akan melihat data basi selama migrasi berjalan. Alihkan pembacaan ke writer dulu, atau lakukan sebelum
memisahkan baca-tulis.
HTML dari beberapa generasi
-- Apa saja yang ada di badan artikel?
SELECT
SUM(isi LIKE '%<font%') AS tag_font,
SUM(isi LIKE '%style="%') AS style_sebaris,
SUM(isi LIKE '%<table%') AS tabel_layout,
SUM(isi LIKE '%<center%') AS tag_center,
SUM(isi LIKE '%<iframe%') AS iframe,
SUM(isi LIKE '%mso-%') AS tempel_dari_word,
SUM(isi LIKE '%<script%') AS script,
SUM(isi LIKE '%src="http://%') AS gambar_http,
COUNT(*) AS total
FROM artikel WHERE status = 'terbit';
| Temuan | Prioritas | Tindakan |
|---|---|---|
<script> di badan artikel | Kritis | Sanitasi (Fase 7) — ini XSS tersimpan |
src="http://" | Tinggi | Konten campuran; gambar diblokir di halaman HTTPS |
Tempelan dari Word (mso-) | Sedang | Bikin HTML membengkak berkali-kali lipat |
<font>, <center> | Rendah | Kosmetik; sanitasi menghapusnya |
| Tabel untuk tata letak | Rendah | Rusak di ponsel, tapi memperbaikinya manual |
-- Perbaikan gambar HTTP: cepat dan berdampak langsung
UPDATE artikel
SET isi = REPLACE(isi, 'src="http://portal.contoh.id', 'src="https://portal.contoh.id')
WHERE isi LIKE '%src="http://portal.contoh.id%';
Normalisasi bertahap
// Jangan bersihkan 200 ribu artikel dalam satu transaksi.
export async function bersihkanBatch(batas = 500) {
const artikel = await dbBaca
.selectFrom("artikel")
.select(["id", "isi"])
.where("disanitasi", "=", 0)
.orderBy("id", "asc")
.limit(batas)
.execute();
for (const a of artikel) {
const bersih = bersihkan(a.isi); // Fase 7
await dbTulis.updateTable("artikel")
.set({ isi: bersih, disanitasi: 1 })
.where("id", "=", a.id)
.execute();
}
return artikel.length;
}
Jalankan sebagai tugas berkala sampai habis. Sementara itu, artikel yang belum tersentuh tetap aman karena dibersihkan saat dirender (Fase 7). Tidak ada jendela pemeliharaan yang dibutuhkan.
Simpan versi aslinya sebelum menimpa. Satu kolom isi_asli atau satu tabel arsip
berarti keputusan sanitasi yang ternyata terlalu agresif bisa dibatalkan. Tanpa itu, satu konfigurasi
yang keliru bisa menghapus embed video dari sepuluh ribu artikel secara permanen.
Berkas yatim di uploads/
# Semua berkas yang ada
find /var/www/portal/uploads -type f -printf '%P\n' | sort > /tmp/ada.txt
# Yang dirujuk kolom database
mysql -N -e "SELECT gambar FROM artikel WHERE gambar IS NOT NULL
UNION SELECT foto FROM member WHERE foto IS NOT NULL" portal \
| sed 's|^/*uploads/||' | sort -u > /tmp/dirujuk-kolom.txt
# Yang dirujuk DI DALAM badan artikel — jangan lupa ini
mysql -N -e "SELECT isi FROM artikel" portal \
| grep -oE 'uploads/[^"'"'"' )>]+' | sed 's|^uploads/||' | sort -u > /tmp/dirujuk-inline.txt
cat /tmp/dirujuk-kolom.txt /tmp/dirujuk-inline.txt | sort -u > /tmp/dirujuk.txt
comm -23 /tmp/ada.txt /tmp/dirujuk.txt > /tmp/yatim.txt
wc -l /tmp/ada.txt /tmp/dirujuk.txt /tmp/yatim.txt
du -ch $(sed 's|^|/var/www/portal/uploads/|' /tmp/yatim.txt) 2>/dev/null | tail -1
Jangan hapus apa pun sampai migrasi benar-benar selesai. Rujukan bisa ada di tempat yang tidak kamu kueri: tabel halaman statis, template email, arsip komentar, atau berkas konfigurasi. Pindahkan yang yatim ke penyimpanan arsip murah selama enam bulan, pantau 404, baru hapus.
Urutan yang disarankan
| Urutan | Pekerjaan | Butuh jendela? |
|---|---|---|
| 1 | Inventarisasi semuanya; jangan ubah apa pun | Tidak |
| 2 | Perbaiki mojibake | Tidak |
| 3 | Perbaiki gambar http:// | Tidak |
| 4 | Konversi charset ke utf8mb4 | Ya |
| 5 | Salin uploads/ ke R2/S3 | Tidak |
| 6 | Sanitasi HTML bertahap | Tidak |
| 7 | Arsipkan berkas yatim | Tidak — setelah beberapa bulan |
Latihan: jalankan seluruh kueri inventarisasi di salinan database produksimu dan tulis hasilnya jadi satu dokumen: charset per tabel, jumlah baris ber-mojibake, statistik HTML, dan jumlah berkas yatim beserta ukurannya. Dokumen itu adalah lingkup pekerjaan migrasi datamu — dan angkanya biasanya cukup mengejutkan untuk mengubah jadwal proyek.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.