Kita sudah berkali-kali menulis UNSIGNED pada chapter-chapter sebelumnya untuk memperluas rentang positif. Atribut tersebut punya sisi gelap yang mengejutkan pemula: pengurangan biasa pada kolom UNSIGNED bisa gagal dengan galat yang menyebut BIGINT UNSIGNED padahal kolomnya TINYINT. Pada Chapter penutup section ini kita akan membedah perbedaan rentang signed versus unsigned, belajar membaca atributnya pada struktur tabel, memicu dan memperbaiki galat pengurangan ERROR 1690, lalu menutup dengan kesalahan umum yang muncul saat data berpindah antarsistem.

Rentang Signed dan Unsigned #

Tipe integer menyimpan angka dalam byte biner; byte yang sama bisa dibaca dua cara. Signed memakai satu bit tanda sehingga nilainya bisa negatif, sedangkan unsigned membaca semua bit sebagai nilai positif — akibatnya batas atasnya lipat dua plus satu. Perhatikan pemetaannya:

TipeRentang SIGNEDRentang UNSIGNED
TINYINT-128 s.d. 1270 s.d. 255
SMALLINT-32.768 s.d. 32.7670 s.d. 65.535
MEDIUMINT-8.388.608 s.d. 8.388.6070 s.d. 16.777.215
INT-2.147.483.648 s.d. 2.147.483.6470 s.d. 4.294.967.295
BIGINT± 9,22 × 10¹⁸0 s.d. 18.446.744.073.709.551.615

Menulis tipe tanpa atribut berarti signed. Menambahkan kata UNSIGNED setelah nama tipe memindahkan seluruh rentang ke wilayah non-negatif. Dalam skema, atribut ini dibaca sebagai bagian dari tipe pada hasil DESC — kolom bertanda int adalah signed, sedangkan int unsigned adalah unsigned. Perlu dicatat bahwa atribut ini hanya berlaku untuk keluarga integer: DECIMAL, CHAR, dan tipe lain tidak punya varian unsigned.

Praktik Membaca dan Menggunakan #

Kita buat tabel inventaris yang sengaja mencampur kolom unsigned (stok, yang tak mungkin negatif) dan signed (penyesuaian, yang boleh naik-turun dua arah):

sql
DROP DATABASE IF EXISTS belajar_tipe;
CREATE DATABASE belajar_tipe;
USE belajar_tipe;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE inventaris (
  id INT,
  nama VARCHAR(40),
  stok TINYINT UNSIGNED,
  penyesuaian TINYINT
);

INSERT INTO inventaris VALUES
  (1, 'Pensil Kayu', 5, -3),
  (2, 'Buku Tulis', 10, 2);

DESC inventaris;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

+-------------+------------------+------+-----+---------+-------+
| Field       | Type             | Null | Key | Default | Extra |
+-------------+------------------+------+-----+---------+-------+
| id          | int              | YES  |     | NULL    |       |
| nama        | varchar(40)      | YES  |     | NULL    |       |
| stok        | tinyint unsigned | YES  |     | NULL    |       |
| penyesuaian | tinyint          | YES  |     | NULL    |       |
+-------------+------------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

Pada kolom Type hasil DESC, baris stok terbaca tinyint unsigned sedangkan penyesuaian terbaca tinyint polos — itulah cara membaca atribut tanda pada tabel apa pun. Kedua jenis kolom hidup damai dalam satu tabel. Penjumlahan lintas tanda juga aman selama hasilnya tetap non-negatif:

sql
SELECT id, nama, stok, penyesuaian,
       stok + penyesuaian AS stok_setelah_penyesuaian
FROM inventaris
ORDER BY id;

Output yang Dihasilkan:

text
+------+-------------+------+-------------+--------------------------+
| id   | nama        | stok | penyesuaian | stok_setelah_penyesuaian |
+------+-------------+------+-------------+--------------------------+
|    1 | Pensil Kayu |    5 |          -3 |                        2 |
|    2 | Buku Tulis  |   10 |           2 |                       12 |
+------+-------------+------+-------------+--------------------------+
2 rows in set (0.00 sec)

Baris pertama menghasilkan 5 + (-3) = 2 dan baris kedua 10 + 2 = 12 — penjumlahan kolom unsigned dengan signed berjalan normal karena hasilnya tidak menyentuh wilayah terlarang. Masalah baru muncul pada pengurangan yang hasilnya negatif.

Jebakan Pengurangan: ERROR 1690 #

Sekarang kita kurangi kolom stok dengan angka 10 padahal stok tertinggi hanya 10:

sql
-- @error
SELECT stok - 10 AS sisa FROM inventaris;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(`belajar_tipe`.`inventaris`.`stok` - 10)'
Bye

Query sepele itu gagal dengan galat BIGINT UNSIGNED value is out of range. Galat ini sering membingungkan: kolom kita TINYINT, kok yang disebut BIGINT UNSIGNED? Penjelasannya ada dua lapis. Pertama, MySQL menaikkan semua operasi aritmetika integer ke tipe BIGINT sebelum menghitung, jadi pesan galat selalu menyebut BIGINT. Kedua, begitu satu operan pengurangan bertipe unsigned, hasil dihitung sebagai unsigned juga — dan nilai negatif tidak ada dalam wilayah unsigned, sehingga hasil -5 langsung ditolak. Penolakan terjadi pada baris pertama saja, tetapi satu baris cukup untuk menggagalkan seluruh query.

Solusinya adalah melepas atribut unsigned pada nilai sebelum dikurangkan memakai CAST(stok AS SIGNED):

sql
SELECT id, nama, stok,
       CAST(stok AS SIGNED) - 10 AS sisa_signed
FROM inventaris
ORDER BY id;

Output yang Dihasilkan:

text
+------+-------------+------+-------------+
| id   | nama        | stok | sisa_signed |
+------+-------------+------+-------------+
|    1 | Pensil Kayu |    5 |          -5 |
|    2 | Buku Tulis  |   10 |           0 |
+------+-------------+------+-------------+
2 rows in set (0.00 sec)

Hasil -5 dan 0 kini dihitung lancar karena CAST(... AS SIGNED) mengubah cara pembacaan nilai menjadi signed untuk ekspresi ini saja — kolom aslinya tetap TINYINT UNSIGNED. Pola CAST berguna untuk laporan selisih stok, hutang, dan delta penghitung yang memang bisa negatif. Untuk UPDATE yang mengurangi stok, pencegahan yang lebih alami adalah menjaga di klausa WHERE supaya baris dengan stok kurang tidak pernah tersentuh:

sql
UPDATE inventaris SET stok = stok - 1 WHERE id = 1;

SELECT id, nama, stok FROM inventaris ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+------+-------------+------+
| id   | nama        | stok |
+------+-------------+------+
|    1 | Pensil Kayu |    4 |
|    2 | Buku Tulis  |   10 |
+------+-------------+------+
2 rows in set (0.00 sec)

Stok pensil turun dari 5 menjadi 4 tanpa galat karena baris yang dikurangi memang masih punya stok. Prinsipnya sama dengan aplikasi nyata: UPDATE ... SET stok = stok - 1 WHERE id = ? AND stok >= 1 tidak pernah mencoba menghasilkan nilai negatif, sehingga kolom unsigned bekerja sesuai fungsinya sebagai penjaga batas bawah nol.

Kesalahan Umum Migrasi #

Atribut unsigned paling sering menyakitkan justru saat data berpindah tempat. Mari kita lihat kejutan klasiknya — nilai negatif yang dipaksa menjadi unsigned lewat CAST:

sql
SELECT CAST(-1 AS UNSIGNED) AS hasil_paksaan;

Output yang Dihasilkan:

text
+----------------------+
| hasil_paksaan        |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

Nilai -1 tidak ditolak melainkan terbungkus menjadi 18446744073709551615 — byte biner milik -1 dibaca ulang sebagai angka unsigned terbesar. Bacaan ulang dua arah inilah yang membuat migrasi berbahaya: skrip impor yang memakai CAST memindahkan angka -1 dari sistem lama menjadi angka 18 kuintiliun di kolom unsigned tanpa satu pun galat. Pada mode strict, INSERT langsung dengan -1 memang ditolak keras dengan galat Out of range; sayangnya banyak jalur impor (konversi tipe eksplisit, mode longgar, alat ETL) melewati pemeriksaan itu.

Dari semua kasus tersebut, tiga aturan migrasi bisa kita tarik. Pertama, audit tanda data sumber sebelum mengimpor ke kolom unsigned — bersihkan nilai negatif di sumber, jangan biarkan terbungkus diam-diam. Kedua, samakan atribut signed/unsigned pada pasangan kolom relasi antartabel; id INT UNSIGNED di tabel induk berpasangan dengan pelanggan_id INT (signed) di tabel anak membuat index bekerja ekstra dan foreign key bisa menolak dipasang. Ketiga, uji query aritmetika hasil migrasi (selisih, delta, penyesuaian) dengan nilai nol dan ekstrem sebelum dipercaya — titik itulah galat 1690 dan pembungkusan nilai paling sering muncul.

Terakhir, kita bereskan database latihan:

sql
DROP DATABASE belajar_tipe;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Dengan atribut tanda tuntas dibedah, seluruh kotak perkakas tipe data MySQL kini sudah kita kenali — dari bilangan sampai byte mentah. Section berikutnya meninggalkan level tipe menuju level struktur: bagaimana beberapa tabel dirancang agar saling terhubung, dinormalisasi, dan diberi constraint supaya data tetap konsisten saat aplikasi tumbuh besar.