Sebuah timestamp menyimpan momen secara utuh — 2026-01-15 08:30:45 — tetapi laporan bisnis hampir tidak pernah bertanya secara utuh seperti itu. Laporan penjualan ingin tahu “berapa transaksi di bulan Januari”, aplikasi memberi diskon ulang tahun dengan “berapa usia pelanggan ini”, dan tampilan web ingin menampilkan “15 Januari 2026, pukul 08.30 WIB”. Ketiga kebutuhan itu adalah pekerjaan fungsi tanggal dan waktu: membaca komponen, memotong ke awal periode, menghitung jarak, dan memformat tampilan. Chapter ini mengulas fungsi-fungsi tersebut dengan literal tetap seperti TIMESTAMP '2026-01-15 08:30:45' di seluruh contoh, sehingga setiap output bisa Anda reproduksi persis di server latihan.

Membaca Komponen dengan EXTRACT #

EXTRACT(bagian FROM nilai) mengambil satu komponen dari timestamp, date, atau interval dan mengembalikannya sebagai angka. Komponen yang paling sering dipakai:

KomponenMaknaContoh untuk 2026-01-15 08:30:45Hasil
YEARTahunEXTRACT(YEAR FROM ts)2026
MONTHBulan 1-12EXTRACT(MONTH FROM ts)1
DAYTanggal 1-31EXTRACT(DAY FROM ts)15
HOURJam 0-23EXTRACT(HOUR FROM ts)8
MINUTEMenit 0-59EXTRACT(MINUTE FROM ts)30
DOWHari dalam minggu, 0 = MingguEXTRACT(DOW FROM ts)4 (Kamis)
sql extract.sql
-- EXTRACT mengambil satu komponen dari timestamp
SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-01-15 08:30:45') AS tahun,
       EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS bulan,
       EXTRACT(DAY FROM TIMESTAMP '2026-01-15 08:30:45') AS hari,
       EXTRACT(HOUR FROM TIMESTAMP '2026-01-15 08:30:45') AS jam;

-- DOW: hari dalam minggu, 0 = Minggu sampai 6 = Sabtu
SELECT EXTRACT(DOW FROM DATE '2026-01-15') AS hari_kamis,
       EXTRACT(DOW FROM DATE '2026-01-18') AS hari_minggu;

-- date_part() adalah padanan EXTRACT dengan argumen string
SELECT date_part('month', TIMESTAMP '2026-01-15 08:30:45') AS dp_bulan,
       EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS ex_bulan;

Output yang Dihasilkan:

text
 tahun | bulan | hari | jam 
-------+-------+------+-----
  2026 |     1 |   15 |   8
(1 row)

 hari_kamis | hari_minggu 
------------+-------------
          4 |           0
(1 row)

 dp_bulan | ex_bulan 
----------+----------
        1 |        1
(1 row)

Query pertama memperlihatkan empat komponen inti yang diambil dari satu literal tetap. Query kedua memperkenalkan DOW — 15 Januari 2026 jatuh pada Kamis sehingga hasilnya 4, dan 18 Januari 2026 adalah Minggu sehingga hasilnya 0. Penomoran yang dimulai dari Minggu ini penting diingat ketika menyaring hari kerja: kondisi “Senin sampai Jumat” ditulis WHERE EXTRACT(DOW FROM waktu) BETWEEN 1 AND 5. Query ketiga membuktikan bahwa date_part('month', ...) dan EXTRACT(MONTH FROM ...) menghasilkan angka yang sama — date_part adalah notasi fungsi lawas, sedangkan EXTRACT adalah sintaks standar SQL; pilih salah satu dan gunakan secara konsisten.

Memotong ke Awal Periode dengan date_trunc #

Jika EXTRACT mengambil satu komponen sebagai angka, date_trunc('periode', nilai) memotong timestamp ke awal periode dan tetap mengembalikan timestamp utuh. Ini adalah fungsi kunci untuk mengelompokkan data ke dalam ember waktu — per jam, per hari, per bulan:

EkspresiHasilYang dipotong
date_trunc('hour', ts)2026-01-15 08:00:00Menit dan detik
date_trunc('day', ts)2026-01-15 00:00:00Seluruh jam
date_trunc('month', ts)2026-01-01 00:00:00Tanggal ke awal bulan
date_trunc('year', ts)2026-01-01 00:00:00Semua ke awal tahun
sql date-trunc.sql
-- date_trunc() memotong timestamp ke awal periode yang diminta
SELECT date_trunc('month', TIMESTAMP '2026-01-15 08:30:45') AS awal_bulan,
       date_trunc('day', TIMESTAMP '2026-01-15 08:30:45') AS awal_hari,
       date_trunc('hour', TIMESTAMP '2026-01-15 08:30:45') AS awal_jam,
       date_trunc('year', TIMESTAMP '2026-01-15 08:30:45') AS awal_tahun;

-- EXTRACT menghasilkan angka, date_trunc menghasilkan timestamp
SELECT EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS bulan_angka,
       date_trunc('month', TIMESTAMP '2026-01-15 08:30:45') AS bulan_timestamp;

-- pemakaian pada tabel: lompat ke awal bulan untuk tiap baris
CREATE TABLE kunjungan (
    id      integer PRIMARY KEY,
    waktu   timestamp NOT NULL
);

INSERT INTO kunjungan (id, waktu) VALUES
    (1, TIMESTAMP '2026-01-05 09:15:00'),
    (2, TIMESTAMP '2026-01-28 16:40:00'),
    (3, TIMESTAMP '2026-02-02 10:05:00');

SELECT id, waktu, date_trunc('month', waktu) AS awal_bulan
FROM kunjungan
ORDER BY id;

Output yang Dihasilkan:

text
     awal_bulan      |      awal_hari      |      awal_jam       |     awal_tahun      
---------------------+---------------------+---------------------+---------------------
 2026-01-01 00:00:00 | 2026-01-15 00:00:00 | 2026-01-15 08:00:00 | 2026-01-01 00:00:00
(1 row)

 bulan_angka |   bulan_timestamp   
-------------+---------------------
           1 | 2026-01-01 00:00:00
(1 row)

CREATE TABLE
INSERT 0 3
 id |        waktu        |     awal_bulan      
----+---------------------+---------------------
  1 | 2026-01-05 09:15:00 | 2026-01-01 00:00:00
  2 | 2026-01-28 16:40:00 | 2026-01-01 00:00:00
  3 | 2026-02-02 10:05:00 | 2026-02-01 00:00:00
(3 rows)

Query kedua menunjukkan perbedaan hasil yang paling mendasar: EXTRACT(MONTH FROM ...) mengembalikan angka 1, sedangkan date_trunc('month', ...) mengembalikan timestamp 2026-01-01 00:00:00. Pada tabel kunjungan, dua baris pertama yang jatuh di bulan yang berbeda hari (5 dan 28 Januari) keduanya terpotong ke 2026-01-01 00:00:00 — itulah sifat yang membuat date_trunc menjadi pasangan sempurna untuk GROUP BY ketika kita ingin menghitung jumlah kunjungan per bulan. Pola lengkap GROUP BY date_trunc(...) dibahas tuntas di section Agregasi Data .

Menghitung Jarak Waktu dengan age dan justify #

Fungsi AGE(nilai_akhir, nilai_awal) menghitung jarak dua titik waktu dan menghasilkan interval yang sudah dinormalisasi menjadi tahun-bulan-hari — format yang persis seperti cara manusia membaca usia:

-- AGE() menghitung jarak dua waktu sebagai interval tahun-bulan-hari
SELECT AGE(TIMESTAMP '2026-03-20', TIMESTAMP '1998-07-25') AS usia_pelanggan;

-- jarak lebih pendek: di bawah dua tahun
SELECT AGE(DATE '2026-03-20', DATE '2024-12-25') AS selisih;

-- justify_hours(): rapikan jam yang melewati batas 24 menjadi hari
SELECT INTERVAL '30 hours' AS mentah,
       justify_hours(INTERVAL '30 hours') AS jam_dirapikan;

-- justify_days(): rapikan hari menjadi bulan kalender
SELECT INTERVAL '45 days' AS mentah_hari,
       justify_days(INTERVAL '45 days') AS hari_dirapikan;

-- justify_interval() merapikan hari dan jam sekaligus
SELECT justify_interval(INTERVAL '45 days 30 hours') AS lengkap;

Output yang Dihasilkan:

text
     usia_pelanggan      
-------------------------
 27 years 7 mons 26 days
(1 row)

        selisih        
-----------------------
 1 year 2 mons 26 days
(1 row)

  mentah  | jam_dirapikan  
----------+----------------
 30:00:00 | 1 day 06:00:00
(1 row)

 mentah_hari | hari_dirapikan 
-------------+----------------
 45 days     | 1 mon 15 days
(1 row)

        lengkap         
------------------------
 1 mon 16 days 06:00:00
(1 row)

Usia pelanggan lahir 25 Juli 1998 per 20 Maret 2026 terbaca 27 years 7 mons 26 days — tahun dan bulan mengikuti kalender, bukan konversi 365 hari mekanis. Keluarga justify melanjutkan tema perapikan itu pada interval: justify_hours mengubah 30 hours menjadi 1 day 06:00:00, justify_days mengubah 45 days menjadi 1 mon 15 days, dan justify_interval merapikan keduanya sekaligus menjadi 1 mon 16 days 06:00:00. Aturan lengkap tipe interval — bulan yang mengikuti kalender, operasi antar tipe waktu — sudah dibedah di chapter Interval .

Memformat Tampilan dengan to_char #

Fungsi to_char(nilai, pola) mengubah tanggal/waktu menjadi teks sesuai pola yang Anda susun sendiri. Tabel berikut merangkum pola yang paling sering dipakai untuk TIMESTAMP '2026-01-15 08:30:00':

PolaMaknaHasil
YYYYTahun empat digit2026
MMBulan dua digit01
DDTanggal dua digit15
MonthNama bulan (rata kanan 9 karakter)January
FMMonthNama bulan tanpa pengisi spasiJanuary
DayNama hari (rata kanan 9 karakter)Thursday
DYNama hari tiga huruf kapitalTHU
HH24Jam 00-2308
MIMenit 00-5930
AMPenanda pagi/siangAM
sql to-char.sql
-- pola dasar: susunan tanggal
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'YYYY-MM-DD') AS pola_iso,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'DD/MM/YYYY') AS pola_hari_bulan;

-- nama bulan dan hari (selalu dalam bahasa Inggris di server ini)
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'Month') AS bulan_panjang,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'FMMonth') AS bulan_tanpa_pad,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'Day') AS hari_panjang,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'DY') AS hari_singkat;

-- jam dan menit dalam dua format
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'HH24:MI') AS jam_24,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'HH12:MI AM') AS jam_12;

-- teks tetap ditulis di dalam kutip ganda
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', '"jam" HH24 "lewat" MI "menit"') AS kalimat;

Output yang Dihasilkan:

text
  pola_iso  | pola_hari_bulan 
------------+-----------------
 2026-01-15 | 15/01/2026
(1 row)

 bulan_panjang | bulan_tanpa_pad | hari_panjang | hari_singkat 
---------------+-----------------+--------------+--------------
 January       | January         | Thursday     | THU
(1 row)

 jam_24 |  jam_12  
--------+----------
 08:30  | 08:30 AM
(1 row)

        kalimat        
-----------------------
 jam 08 lewat 30 menit
(1 row)

Empat perilaku patut dibedah dari hasil di atas:

  1. 'Month' vs 'FMMonth': tanpa prefiks FM, nama bulan dan hari dirata-kanan ke lebar 9 karakter — hasil January memuat dua spasi di belakangnya agar semua nama bulan sama lebar. Prefiks FM (fill mode) memangkas pengisi itu.
  2. Nama selalu bahasa Inggris: server latihan berjalan dengan locale C, sehingga to_char menghasilkan January dan Thursday, bukan Januari dan Kamis. Nama lokal bergantung pada konfigurasi lc_time yang ditetapkan saat database dibuat.
  3. HH12:MI AM: format 12 jam lengkap dengan penanda AM/PM; 08:30:00 menjadi 08:30 AM.
  4. Teks tetap dalam kutip ganda: bagian pola yang diapit "..." dikeluarkan apa adanya, sehingga kalimat jam 08 lewat 30 menit bisa dibangun langsung dari satu pola.

Parsing String menjadi Tanggal dengan to_date dan to_timestamp #

Arah sebaliknya dari to_char adalah parsing: menerima teks dari pengguna atau berkas ekspor, lalu mengubahnya menjadi date atau timestamptz. Pola parsing memakai kode yang sama seperti pola format:

-- to_date() mem-parsing string menjadi date
SELECT to_date('15-01-2026', 'DD-MM-YYYY') AS tanggal;

-- pemisah yang berbeda dari pola tetap diterima
SELECT to_date('15/01/2026', 'DD-MM-YYYY') AS pemisah_miring;

-- to_timestamp() mem-parsing string lengkap menjadi timestamptz
SELECT to_timestamp('15-01-2026 08:30', 'DD-MM-YYYY HH24:MI') AS waktu_lengkap;

-- round-trip: hasil to_char bisa diparsing balik
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'YYYY-MM-DD HH24:MI') AS teks,
       to_timestamp('2026-01-15 08:30', 'YYYY-MM-DD HH24:MI') AS kembali;

Output yang Dihasilkan:

text
  tanggal   
------------
 2026-01-15
(1 row)

 pemisah_miring 
----------------
 2026-01-15
(1 row)

     waktu_lengkap      
------------------------
 2026-01-15 08:30:00+07
(1 row)

       teks       |        kembali         
------------------+------------------------
 2026-01-15 08:30 | 2026-01-15 08:30:00+07
(1 row)

String '15-01-2026' yang ambigu bagi DATE literal (lihat kembali jebakan DateStyle di chapter Date dan Time ) menjadi tidak ambigu lagi begitu pola DD-MM-YYYY disebutkan secara eksplisit — tanggal 15, bulan 01. Perhatikan dua hal dari hasil di atas: pemisah boleh berbeda antara data dan pola (15/01/2026 tetap terbaca), dan to_timestamp menghasilkan timestamptz yang ditampilkan dengan offset zona waktu sesi (+07). Query terakhir memperlihatkan pola round-trip: teks hasil to_char dengan pola YYYY-MM-DD HH24:MI berhasil diparsing kembali menjadi nilai waktu yang sama.

Meski toleran terhadap pemisah, parser tetap memvalidasi isi kalendernya:

-- tanggal 32 tidak pernah ada di kalender mana pun
SELECT to_date('32-01-2026', 'DD-MM-YYYY') AS gagal;

Output yang Dihasilkan:

text
psql:to-date-error.sql:2: ERROR:  date/time field value out of range: "32-01-2026"

Error date/time field value out of range menegaskan bahwa parsing gagal pada tahap validasi nilai, bukan pada pola. Pola DD-MM-YYYY sah, tetapi tidak ada bulan dengan 32 hari. Bila data eksternal kerap menyampurkan format yang berbeda, parsinglah di satu tempat yang sama dengan pola eksplisit seperti ini — dan biarkan kolom tabel tetap bertipe date, bukan teks.

Itulah perangkat lengkap untuk membaca, memotong, menghitung, memformat, dan mem-parsing nilai waktu. Perhatikan bahwa sepanjang chapter ini kita bolak-balik mengubah wujud data — timestamp menjadi teks, teks menjadi tanggal, angka menjadi interval. transformasi antar tipe seperti itu punya aturan formalnya sendiri di PostgreSQL, dan itulah topik chapter berikutnya: Fungsi Konversi Tipe Data .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

age-dan-justify.sql

Unduh berkas
sql
-- AGE() menghitung jarak dua waktu sebagai interval tahun-bulan-hari
SELECT AGE(TIMESTAMP '2026-03-20', TIMESTAMP '1998-07-25') AS usia_pelanggan;

-- jarak lebih pendek: di bawah dua tahun
SELECT AGE(DATE '2026-03-20', DATE '2024-12-25') AS selisih;

-- justify_hours(): rapikan jam yang melewati batas 24 menjadi hari
SELECT INTERVAL '30 hours' AS mentah,
       justify_hours(INTERVAL '30 hours') AS jam_dirapikan;

-- justify_days(): rapikan hari menjadi bulan kalender
SELECT INTERVAL '45 days' AS mentah_hari,
       justify_days(INTERVAL '45 days') AS hari_dirapikan;

-- justify_interval() merapikan hari dan jam sekaligus
SELECT justify_interval(INTERVAL '45 days 30 hours') AS lengkap;

date-trunc.sql

Unduh berkas
sql
-- date_trunc() memotong timestamp ke awal periode yang diminta
SELECT date_trunc('month', TIMESTAMP '2026-01-15 08:30:45') AS awal_bulan,
       date_trunc('day', TIMESTAMP '2026-01-15 08:30:45') AS awal_hari,
       date_trunc('hour', TIMESTAMP '2026-01-15 08:30:45') AS awal_jam,
       date_trunc('year', TIMESTAMP '2026-01-15 08:30:45') AS awal_tahun;

-- EXTRACT menghasilkan angka, date_trunc menghasilkan timestamp
SELECT EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS bulan_angka,
       date_trunc('month', TIMESTAMP '2026-01-15 08:30:45') AS bulan_timestamp;

-- pemakaian pada tabel: lompat ke awal bulan untuk tiap baris
CREATE TABLE kunjungan (
    id      integer PRIMARY KEY,
    waktu   timestamp NOT NULL
);

INSERT INTO kunjungan (id, waktu) VALUES
    (1, TIMESTAMP '2026-01-05 09:15:00'),
    (2, TIMESTAMP '2026-01-28 16:40:00'),
    (3, TIMESTAMP '2026-02-02 10:05:00');

SELECT id, waktu, date_trunc('month', waktu) AS awal_bulan
FROM kunjungan
ORDER BY id;

extract.sql

Unduh berkas
sql
-- EXTRACT mengambil satu komponen dari timestamp
SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-01-15 08:30:45') AS tahun,
       EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS bulan,
       EXTRACT(DAY FROM TIMESTAMP '2026-01-15 08:30:45') AS hari,
       EXTRACT(HOUR FROM TIMESTAMP '2026-01-15 08:30:45') AS jam;

-- DOW: hari dalam minggu, 0 = Minggu sampai 6 = Sabtu
SELECT EXTRACT(DOW FROM DATE '2026-01-15') AS hari_kamis,
       EXTRACT(DOW FROM DATE '2026-01-18') AS hari_minggu;

-- date_part() adalah padanan EXTRACT dengan argumen string
SELECT date_part('month', TIMESTAMP '2026-01-15 08:30:45') AS dp_bulan,
       EXTRACT(MONTH FROM TIMESTAMP '2026-01-15 08:30:45') AS ex_bulan;

to-char.sql

Unduh berkas
sql
-- pola dasar: susunan tanggal
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'YYYY-MM-DD') AS pola_iso,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'DD/MM/YYYY') AS pola_hari_bulan;

-- nama bulan dan hari (selalu dalam bahasa Inggris di server ini)
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'Month') AS bulan_panjang,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'FMMonth') AS bulan_tanpa_pad,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'Day') AS hari_panjang,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'DY') AS hari_singkat;

-- jam dan menit dalam dua format
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'HH24:MI') AS jam_24,
       to_char(TIMESTAMP '2026-01-15 08:30:00', 'HH12:MI AM') AS jam_12;

-- teks tetap ditulis di dalam kutip ganda
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', '"jam" HH24 "lewat" MI "menit"') AS kalimat;

to-date-dan-to-timestamp.sql

Unduh berkas
sql
-- to_date() mem-parsing string menjadi date
SELECT to_date('15-01-2026', 'DD-MM-YYYY') AS tanggal;

-- pemisah yang berbeda dari pola tetap diterima
SELECT to_date('15/01/2026', 'DD-MM-YYYY') AS pemisah_miring;

-- to_timestamp() mem-parsing string lengkap menjadi timestamptz
SELECT to_timestamp('15-01-2026 08:30', 'DD-MM-YYYY HH24:MI') AS waktu_lengkap;

-- round-trip: hasil to_char bisa diparsing balik
SELECT to_char(TIMESTAMP '2026-01-15 08:30:00', 'YYYY-MM-DD HH24:MI') AS teks,
       to_timestamp('2026-01-15 08:30', 'YYYY-MM-DD HH24:MI') AS kembali;

to-date-error.sql

Unduh berkas
sql
-- tanggal 32 tidak pernah ada di kalender mana pun
SELECT to_date('32-01-2026', 'DD-MM-YYYY') AS gagal;