TL;DR
Menangani timezone di SQL intinya nyimpan waktu dalam UTC pakai tipe timestamptz, lalu konversi ke zona lokal cuma pas nampilin. Di PostgreSQL, konversi pakai AT TIME ZONE, misalnya kolom AT TIME ZONE 'Asia/Jakarta' buat ngubah UTC ke WIB. Tipe timestamp tanpa zona nggak nyimpan info timezone, jadi rawan salah tafsir. Aturan amannya: simpan UTC, tampilkan lokal.
Menangani timezone di SQL paling aman dengan satu aturan: simpan waktu dalam UTC, konversi ke zona lokal cuma pas nampilin. Ini ngindarin data yang geser tanggal.
Masalahnya nyata. Aplikasi nyimpen transaksi dalam UTC, tapi bos minta laporan jam WIB. Kalau konversinya salah, penjualan jam 11 malam bisa kecatat di tanggal besoknya.
Kamu bakal lihat beda tipe timestamp, cara konversi pakai AT TIME ZONE, plus jebakan laporan harian yang bikin angka meleset.
Timezone adalah info zona waktu yang nempel di sebuah timestamp, yang nentuin waktu itu dibaca sebagai jam berapa di lokasi mana. Jam 10 pagi di Jakarta beda dengan jam 10 pagi di London. Tanpa info zona, satu timestamp bisa ditafsir beda-beda. UTC adalah patokan waktu global yang dipakai buat nyimpan waktu secara netral, dan WIB itu UTC ditambah 7 jam.
Kunci masalahnya, database perlu tau apakah sebuah waktu udah termasuk zona atau belum. Ini yang bedain dua tipe kolom waktu.
Di PostgreSQL, ada dua tipe. timestamp nyimpan tanggal dan jam tanpa info zona, jadi angkanya polos. timestamptz (timestamp with time zone) nyimpan waktu yang sadar zona, dan internalnya selalu disimpan dalam UTC. Waktu kamu masukin data, timestamptz otomatis nyesuain ke UTC berdasarkan zona sesi.
| Tipe | Simpan zona? | Cocok buat |
|---|---|---|
| timestamp | Nggak | Waktu lokal tetap, kayak jadwal buka toko |
| timestamptz | Ya (internal UTC) | Transaksi, log, event lintas kota |
Buat data transaksi yang bisa datang dari mana aja, hampir selalu pakai timestamptz. Buat waktu yang memang lokal dan tetap, kayak jam operasional, timestamp polos cukup. Buat dasar tipe tanggal, cek fungsi EXTRACT di SQL.
Pakai operator AT TIME ZONE dengan nama zona. Buat WIB, nama zonanya 'Asia/Jakarta'. Operator ini ngubah timestamptz yang tersimpan UTC jadi waktu lokal Jakarta. Hasilnya timestamp polos yang udah digeser 7 jam.
SELECT
id_transaksi,
waktu_bayar,
waktu_bayar AT TIME ZONE 'Asia/Jakarta' AS waktu_wib
FROM transaksi;
Kalau waktu_bayar nyimpan 2026-09-01 22:00 UTC, hasil waktu_wib jadi 2026-09-02 05:00. Udah bener, geser 7 jam maju.
Pakai nama zona 'Asia/Jakarta', jangan angka '+07'. Nama zona ngerti aturan khusus tiap wilayah, sedangkan angka offset kaku. Buat Indonesia bagian tengah pakai 'Asia/Makassar', dan timur pakai 'Asia/Jayapura'.
Ini bagian yang sering salah. Buat ngelompokkan transaksi per hari WIB, konversi dulu ke zona Jakarta, baru ambil tanggalnya. Kalau kamu grup langsung dari UTC, batas harinya jam 7 pagi WIB, bukan tengah malam.
SELECT
(waktu_bayar AT TIME ZONE 'Asia/Jakarta')::date AS tanggal_wib,
COUNT(*) AS jumlah_transaksi,
SUM(nominal) AS total
FROM transaksi
GROUP BY tanggal_wib
ORDER BY tanggal_wib;
Urutannya penting: konversi zona dulu, baru cast ke date. Kalau kebalik, tanggalnya diambil dari UTC dan hasilnya geser. Buat teknik ngelompokkan waktu lanjutan, ada bahasan fungsi DATE_TRUNC di SQL.
Toko_berkah punya toko online yang aktif sampai larut. Server nyimpen 18.400 transaksi dalam UTC. Pemiliknya bingung: laporan penjualan harian dari tim dev angkanya beda dengan feeling dia soal penjualan malam. Ternyata laporan lama ngelompokkan pakai UTC.
Setelah konversi ke WIB dulu baru dikelompokkan, angkanya berubah. Transaksi antara jam 5 sore sampai tengah malam WIB ternyata 41% dari total harian, jauh lebih gede dari laporan lama yang cuma nunjukin 28%. Selisihnya gara-gara transaksi malam UTC kelempar ke tanggal berikutnya.
SELECT
EXTRACT(HOUR FROM waktu_bayar AT TIME ZONE 'Asia/Jakarta') AS jam_wib,
COUNT(*) AS jumlah
FROM transaksi
GROUP BY jam_wib
ORDER BY jam_wib;
Query ini ngasih sebaran transaksi per jam WIB. Puncaknya ketemu jam 8 malam. Temuan ini bikin pemiliknya majuin jadwal promo flash sale dari jam 3 sore ke jam 7 malam, ngikutin jam ramai asli.
Jebakan pertama: nyimpan waktu lokal di kolom timestamp polos. Begitu server pindah zona atau data digabung dari kota lain, kamu nggak bisa tau lagi waktu itu zona apa. Simpan UTC pakai timestamptz dari awal.
Jebakan kedua: grouping harian langsung dari UTC. Ini yang bikin laporan geser kayak kasus toko_berkah. Selalu konversi ke zona lokal sebelum ambil tanggal.
Jebakan ketiga: pakai offset angka, bukan nama zona. Angka +07 kelihatan gampang, tapi nggak ngerti perubahan aturan waktu di masa lalu. Nama zona kayak 'Asia/Jakarta' selalu benar. Detail soal ini ada di dokumentasi tipe tanggal PostgreSQL.
Jebakan keempat: nyampur timestamp dan timestamptz di satu query. Bandingin dua tipe ini bikin PostgreSQL nebak zona, dan tebakannya belum tentu yang kamu mau. Samain tipe kolomnya dulu.
Karena UTC itu patokan netral yang nggak berubah tergantung lokasi server atau pengguna. Kalau kamu simpan waktu lokal, data dari kota berbeda jadi susah dibandingin dan rawan salah tafsir pas server pindah. Dengan nyimpan UTC lalu konversi ke zona lokal cuma pas nampilin, kamu punya satu sumber waktu yang konsisten buat semua laporan.
Pakai timestamptz buat waktu kejadian nyata kayak transaksi, log, atau event yang bisa datang dari zona berbeda, karena tipe ini nyimpan info zona secara internal. Pakai timestamp polos buat waktu lokal yang memang tetap dan nggak tergantung lokasi, misalnya jam buka toko atau jadwal harian. Buat kebanyakan data bisnis, timestamptz lebih aman.
Biasanya karena kamu ngelompokkan transaksi berdasarkan tanggal UTC, bukan tanggal lokal. Transaksi jam 9 malam WIB tercatat jam 2 siang UTC di hari yang sama, tapi transaksi jam 11 malam WIB tercatat jam 4 sore UTC. Masalah muncul di sekitar tengah malam. Solusinya, konversi ke zona lokal dulu pakai AT TIME ZONE, baru ambil tanggalnya.
Buat WIB pakai 'Asia/Jakarta', buat WITA pakai 'Asia/Makassar', dan buat WIT pakai 'Asia/Jayapura'. Nama-nama ini bagian dari database zona waktu standar yang dipakai PostgreSQL. Lebih aman pakai nama wilayah ini dibanding angka offset seperti +07, karena nama zona ngerti aturan waktu tiap daerah dengan benar.
Pakai perintah SET TIME ZONE 'Asia/Jakarta' di awal sesi, dan semua tampilan timestamptz bakal otomatis dikonversi ke zona itu. Ini berguna kalau kamu mau semua query di satu sesi nampil dalam WIB tanpa nulis AT TIME ZONE berulang. Tapi buat laporan yang dipakai banyak orang, mending konversi eksplisit di query biar hasilnya pasti.
Aturan emasnya cuma satu: simpan UTC pakai timestamptz, tampilkan lokal pakai AT TIME ZONE. Ingat konversi zona dulu sebelum ambil tanggal, dan selalu pakai nama zona bukan angka offset.
Mau latihan query waktu sampai lancar tanpa takut ngerusak data? Coba di NgulikSQL.
Lanjut baca: fungsi DATE_TRUNC buat ringkas tanggal dan operasi interval dan aritmetika tanggal di SQL.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.
Ngurangin tahun sekarang sama tahun lahir kelihatan gampang, tapi hasilnya sering meleset setahun kalau ulang tahunnya belum lewat. Ini cara hitung umur yang akurat di SQL.
Cara pakai DATE_TRUNC di SQL buat memotong timestamp jadi awal bulan, minggu, atau jam. Kunci bikin laporan penjualan per periode yang rapi.