Tipe data adalah jenis isi yang boleh disimpan sebuah kolom: angka bulat, angka desimal, teks, waktu, atau ya/tidak. Bayangin dapur yang rapi: telur di wadah telur, minyak di botol, gula di toples. Semuanya memang bisa dimasukkan ke satu kantong plastik, tapi telurnya pecah dan gulanya tumpah. Tipe yang tepat bikin database menolak isi yang salah, menyimpan dengan hemat, dan menghitung dengan benar.
🧪 Tebakan cepat: kolom
pricedi SQLite bertipeINTEGER. Form admin Kedai Senja mengirim harga Roti Bakar'20rb'. Apa yang terjadi: error, atau masuk begitu saja? Tebak dulu, jawabannya ada di bagian "SQLite: tipe cuma saran".
Satu tabel, satu tipe per kolom
Ini tabel produk Kedai Senja kalau ditulis untuk PostgreSQL. Tiap kolom dapat tipe yang paling pas dengan isinya:
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL, -- nama produk
description TEXT, -- deskripsi panjang
country_code CHAR(2) NOT NULL, -- asal biji kopi: 'ID', 'BR'
price DECIMAL(12, 2) NOT NULL, -- harga, angka pasti
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
VARCHAR(100): teks dengan batas panjang, di sini maksimal 100 karakter. Cocok untuk nama, email, username.TEXT: teks panjang tanpa batas yang perlu dipikirkan. Cocok untuk deskripsi atau isi artikel blog.CHAR(2): teks yang panjangnya selalu sama, di sini 2 huruf. Cocok untuk kode negara.DECIMAL(12, 2): angka desimal yang pasti, total 12 digit dengan 2 di belakang koma. Ini tipe untuk uang.BOOLEAN: cumaTRUEatauFALSE, 1 byte di PostgreSQL. Cocok untuk status aktif/nonaktif.VARCHAR(10)berisi'aktif'makan lebih banyak tempat dan bisa salah ketik.TIMESTAMPTZ: satu titik waktu yang pasti. PostgreSQL menyimpannya sebagai UTC, lalu menampilkannya sesuai zona waktu koneksi, misalnya WIB (UTC+7). Cocok untuk waktu dibuat.BIGINT: angka bulat 8 byte.INTEGER(4 byte) cukup sampai sekitar 2,1 miliar,BIGINTjauh lebih besar, jadi aman untuk ID tabel yang terus tumbuh.
Uang: kenapa bukan FLOAT
FLOAT, REAL dan DOUBLE menyimpan angka dalam bentuk biner yang mendekati, bukan pasti. Angka seperti 0,1 gak bisa ditulis tepat dalam biner, sama seperti 1/3 gak bisa ditulis tepat sebagai 0,333... Kedai Senja juga mulai menjual produk kemasan ke luar negeri, dalam dolar. Ini yang terjadi di Lab kalau harganya disimpan sebagai REAL:
CREATE TABLE export_items (id INTEGER PRIMARY KEY, name TEXT, price_usd REAL);
INSERT INTO export_items (name, price_usd) VALUES ('Teh Tarik Sachet', 1.10), ('Gula Aren 500 g', 2.20);
SELECT SUM(price_usd) AS total, SUM(price_usd) = 3.30 AS pas FROM export_items;
| total | pas |
|---|---|
| 3.3000000000000003 | 0 |
- Total 1,10 + 2,20 melenceng sedikit, persis seperti
0.1 + 0.2yang hasilnya0.30000000000000004. pasbernilai0, artinya salah: total belanja gak sama dengan 3,30. Bayangin pengecekan "sudah lunas?" yang gagal karena selisih sepersejuta sen.
Ada dua cara yang benar. Di MySQL dan PostgreSQL, pakai DECIMAL(10, 2), yang menyimpan angka desimal persis. Atau simpan dalam satuan terkecil sebagai angka bulat: price_cents INTEGER berisi 110 dan 220, totalnya tepat 330, dan = 330 bernilai 1. Untuk rupiah yang gak pakai sen, BIGINT berisi rupiah utuh juga pas.
Teks dan waktu
- Pilih
CHAR(n)hanya kalau panjangnya pasti sama,VARCHAR(n)kalau panjangnya beda-beda tapi ada batas wajar, danTEXTkalau isinya bisa sangat panjang. - Jangan simpan angka sebagai teks.
'9'diurutkan sesudah'10', karena teks dibandingkan huruf demi huruf. - Untuk waktu,
TIMESTAMPTZlebih aman daripadaTIMESTAMPbiasa.TIMESTAMPtanpa zona cuma menyimpan angka jam, jadi kalau server pindah dari WIB ke UTC, semua jam bisa terbaca bergeser 7 jam.
SQLite: tipe cuma saran
SQLite, database yang dipakai Lab dan Playground, jauh lebih longgar. Tipe kolom di SQLite cuma saran (namanya type affinity): dia mencoba mengubah nilai ke tipe kolomnya, dan kalau gagal, nilainya disimpan apa adanya.
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER);
INSERT INTO products (name, price) VALUES ('Kopi Susu', 18000);
INSERT INTO products (name, price) VALUES ('Kopi Susu Aren', 22000);
INSERT INTO products (name, price) VALUES ('Roti Bakar', '20rb');
SELECT name, price, typeof(price) FROM products;
| name | price | typeof(price) |
|---|---|---|
| Kopi Susu | 18000 | integer |
| Kopi Susu Aren | 22000 | integer |
| Roti Bakar | 20rb | text |
'20rb' masuk tanpa error, sebagai teks. Itu jawaban tebakannya. typeof() menunjukkan tipe yang benar-benar tersimpan. Teks '20000' (dengan tanda kutip) masih diubah jadi angka 20000, tapi '20rb' gak bisa, jadi tetap teks. Akibatnya kacau diam-diam:
ORDER BY price DESCmenaruh Roti Bakar paling atas, seolah produk termahal, karena SQLite selalu mengurutkan teks sesudah angka.SUM(price)menghasilkan40020: SQLite membaca'20rb'sebagai angka 20.
Mau SQLite seketat database lain? Tambahkan STRICT di akhir CREATE TABLE ... ) STRICT;. Sekarang INSERT yang sama ditolak: cannot store TEXT value in INTEGER column products.price. Tabel STRICT cuma menerima tipe INT, INTEGER, REAL, TEXT, BLOB dan ANY. SQLite juga gak punya DECIMAL yang pasti, kolom DECIMAL tetap menyimpan pecahan sebagai REAL, jadi di SQLite uang selalu disimpan sebagai angka bulat sen.
📌 MySQL, PostgreSQL dan Laravel
MySQL dan PostgreSQL menolak isi yang gak cocok dengan tipenya. Ini jawaban PostgreSQL untuk INSERT yang sama:
ERROR: invalid input syntax for type integer: "20rb"
MySQL juga menolaknya dengan error, karena strict mode yang nyala secara bawaan sejak versi 5.7. Kalau strict mode dimatikan, MySQL menyimpan 20 dan cuma memberi warning. Batas VARCHAR(2) juga dijaga: PostgreSQL menjawab value too long for type character varying(2), sedangkan SQLite menyimpan teks yang lebih panjang tanpa protes. TIMESTAMPTZ cuma ada di PostgreSQL, di MySQL pakai TIMESTAMP (disimpan sebagai UTC) atau DATETIME. BOOLEAN di MySQL sebenarnya TINYINT(1).
Di migration Laravel: $table->string('name', 100), $table->text('description'), $table->char('country_code', 2), $table->decimal('price', 12, 2), $table->boolean('is_active'), $table->timestampTz('created_at').
⚠️ Jebakan yang sering ketemu
- FLOAT atau REAL untuk uang. Totalnya melenceng sedikit dan perbandingan
=gagal. PakaiDECIMAL, atau angka bulat dalam sen. - Mengubah dolar ke sen dengan
CAST.CAST(4.35 * 100 AS INTEGER)hasilnya434, bukan435, karena4.35 * 100tersimpan sebagai434.99999999999994danCASTmembuang pecahannya. Bulatkan dulu:CAST(ROUND(4.35 * 100) AS INTEGER). - Percaya SQLite akan menolak tipe yang salah. Tanpa
STRICT, dia menyimpan apa adanya. Cek dengantypeof(). - Di latihan, nama tipe dikembalikan persis: huruf besar, tanpa panjang, misalnya
"VARCHAR", bukan"varchar(100)".
TL;DR: Pilih tipe sesuai isi: VARCHAR(n) untuk nama, TEXT untuk tulisan panjang, CHAR(n) untuk kode yang panjangnya tetap, DECIMAL(p, s) untuk harga, BOOLEAN untuk aktif/nonaktif, TIMESTAMPTZ untuk waktu dibuat. Jangan pakai FLOAT untuk uang: 1,10 + 2,20 jadi 3,3000000000000003. Simpan dalam DECIMAL atau angka bulat sen. SQLite cuma menganggap tipe sebagai saran ('20rb' masuk ke kolom INTEGER sebagai teks) kecuali tabelnya STRICT, sedangkan MySQL dan PostgreSQL menolaknya.