Tanpa ORDER BY, urutan baris hasil query tidak terdefinisi — PostgreSQL mengembalikan baris dalam urutan apa pun yang paling efisien bagi rencana eksekusinya, dan urutan itu bisa berbeda antara tabel yang identik di dua mesin. Dalam laporan bisnis, urutan bukan kosmetik: daftar gaji yang dimulai dari nilai terbesar, rekap per departemen yang tersusun alfabetis, dan riwayat pesanan dari yang terbaru semuanya adalah kebutuhan pembaca yang nyata. Chapter ini membahas klausa ORDER BY secara penuh: arah sort ASC/DESC, banyak kolom dengan arah campuran, perlakuan khusus terhadap NULL, pengurutan berdasar ekspresi dan alias, sampai satu kebiasaan course ini yang membuat hasil query selalu bisa diprediksi — pemutus seri (tie-break) di kolom terakhir.
Menyiapkan Tabel Latihan #
Tabel pegawai berikut memuat delapan karyawan Toko Maju Jaya dengan tiga sengajaan desain: dua pasang gaji identik (untuk demo pemutus seri), dua tanggal masuk NULL (untuk demo NULLS FIRST/LAST), dan nama dengan panjang bervariasi (untuk demo urutan ekspresi):
CREATE TABLE pegawai (
id integer PRIMARY KEY,
nama text NOT NULL,
departemen text NOT NULL,
gaji numeric(12, 2) NOT NULL,
tanggal_masuk date
);
INSERT INTO pegawai (id, nama, departemen, gaji, tanggal_masuk) VALUES
(1, 'Budi Santoso', 'gudang', 4500000, DATE '2024-03-01'),
(2, 'Siti Rahayu', 'kasir', 5000000, DATE '2023-07-15'),
(3, 'Andi Wijaya', 'gudang', 5000000, DATE '2025-01-20'),
(4, 'Dewi Lestari', 'penjualan', 6000000, NULL),
(5, 'Rudi Hartono', 'gudang', 4500000, DATE '2025-06-01'),
(6, 'Maya Putri', 'penjualan', 5500000, DATE '2024-11-10'),
(7, 'Agus Salim', 'kasir', 5200000, NULL),
(8, 'Rina Melati', 'penjualan', 6000000, DATE '2023-02-01');Output yang Dihasilkan:
CREATE TABLE
INSERT 0 8Kolom tanggal_masuk sengaja dibuat boleh NULL — dua karyawan direkrut lewat jalur yang datanya belum lengkap di arsip.
Dasar: ASC dan DESC #
Kata kunci ASC mengurutkan dari nilai terkecil ke terbesar, dan ini adalah arah default bila arah tidak ditulis. DESC membaliknya. Setiap kata kunci arah hanya berlaku untuk satu kolom — kolom lain tetap mengikuti arah default-nya sendiri:
-- ASC adalah arah default: dari nilai terkecil ke terbesar (A ke Z).
SELECT nama, gaji
FROM pegawai
ORDER BY nama ASC, id;
-- DESC membalik arah: dari gaji terbesar ke terkecil.
-- id dipasang sebagai pemutus seri karena dua pasang gaji sama persis.
SELECT nama, gaji
FROM pegawai
ORDER BY gaji DESC, id;
-- Banyak kolom dengan arah campuran: departemen naik (A-Z),
-- lalu gaji turun di dalam tiap departemen, lalu id pemutus seri.
SELECT nama, departemen, gaji
FROM pegawai
ORDER BY departemen ASC, gaji DESC, id;Output yang Dihasilkan:
nama | gaji
--------------+------------
Agus Salim | 5200000.00
Andi Wijaya | 5000000.00
Budi Santoso | 4500000.00
Dewi Lestari | 6000000.00
Maya Putri | 5500000.00
Rina Melati | 6000000.00
Rudi Hartono | 4500000.00
Siti Rahayu | 5000000.00
(8 rows)
nama | gaji
--------------+------------
Dewi Lestari | 6000000.00
Rina Melati | 6000000.00
Maya Putri | 5500000.00
Agus Salim | 5200000.00
Siti Rahayu | 5000000.00
Andi Wijaya | 5000000.00
Budi Santoso | 4500000.00
Rudi Hartono | 4500000.00
(8 rows)
nama | departemen | gaji
--------------+------------+------------
Andi Wijaya | gudang | 5000000.00
Budi Santoso | gudang | 4500000.00
Rudi Hartono | gudang | 4500000.00
Agus Salim | kasir | 5200000.00
Siti Rahayu | kasir | 5000000.00
Dewi Lestari | penjualan | 6000000.00
Rina Melati | penjualan | 6000000.00
Maya Putri | penjualan | 5500000.00
(8 rows)Query pertama mengurutkan nama alfabetis — Agus Salim sampai Siti Rahayu. Query kedua menempatkan gaji 6.000.000 di dua baris teratas. Query ketiga memperlihatkan pola “urut berlapis”: departemen diurutkan naik, dan di dalam tiap departemen, gaji diurutkan turun — hasilnya gudang tampil dulu, tetapi pegawai termahalnya (Andi Wijaya, 5.000.000) berdiri paling atas di kelompoknya. Perhatikan bahwa DESC pada gaji tidak menular ke departemen; tiap kolom membawa arahnya sendiri.
Perlakuan NULL: NULLS FIRST dan NULLS LAST #
Bagaimana urutan bila kolom sort memuat NULL? PostgreSQL memperlakukan NULL seolah-olah lebih besar dari semua nilai lain, sehingga default-nya bergantung pada arah sort:
| Arah sort | Default NULL | Akibat |
|---|---|---|
ASC | NULLS LAST | baris NULL menggerombol di akhir |
DESC | NULLS FIRST | baris NULL menggerombol di awal |
Kedua default itu bisa ditimpa secara eksplisit dengan NULLS FIRST atau NULLS LAST — opsi khas PostgreSQL yang berguna ketika baris “belum ada datanya” justru harus tampil di posisi tertentu:
-- ASC: default-nya NULLS LAST — dua baris ber-tanggal NULL menggerombol di akhir.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk ASC, id;
-- DESC: default-nya NULLS FIRST — baris NULL pindah ke depan.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk DESC, id;
-- NULLS FIRST memaksa NULL ke depan meski arah sort naik.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk ASC NULLS FIRST, id;
-- NULLS LAST memaksa NULL ke belakang meski arah sort turun.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk DESC NULLS LAST, id;Output yang Dihasilkan:
nama | tanggal_masuk
--------------+---------------
Rina Melati | 2023-02-01
Siti Rahayu | 2023-07-15
Budi Santoso | 2024-03-01
Maya Putri | 2024-11-10
Andi Wijaya | 2025-01-20
Rudi Hartono | 2025-06-01
Dewi Lestari |
Agus Salim |
(8 rows)
nama | tanggal_masuk
--------------+---------------
Dewi Lestari |
Agus Salim |
Rudi Hartono | 2025-06-01
Andi Wijaya | 2025-01-20
Maya Putri | 2024-11-10
Budi Santoso | 2024-03-01
Siti Rahayu | 2023-07-15
Rina Melati | 2023-02-01
(8 rows)
nama | tanggal_masuk
--------------+---------------
Dewi Lestari |
Agus Salim |
Rina Melati | 2023-02-01
Siti Rahayu | 2023-07-15
Budi Santoso | 2024-03-01
Maya Putri | 2024-11-10
Andi Wijaya | 2025-01-20
Rudi Hartono | 2025-06-01
(8 rows)
nama | tanggal_masuk
--------------+---------------
Rudi Hartono | 2025-06-01
Andi Wijaya | 2025-01-20
Maya Putri | 2024-11-10
Budi Santoso | 2024-03-01
Siti Rahayu | 2023-07-15
Rina Melati | 2023-02-01
Dewi Lestari |
Agus Salim |
(8 rows)Empat output di atas layak dibandingkan baris demi baris. Query pertama (ASC) menutup daftar dengan Dewi Lestari dan Agus Salim yang tanggalnya NULL; query kedua (DESC) justru membuka daftar dengan keduanya. Query ketiga memaksa NULL ke depan pada urutan naik — pas untuk daftar “berkas yang belum lengkap, tunjukkan dulu” — dan query keempat memaksa NULL ke belakang pada urutan turun. Tanggal yang sama-sama NULL tetap dipecah urutannya oleh id di ujung klausa.
Urut Berdasarkan Ekspresi, Alias, dan Posisi #
Klausa ORDER BY tidak terbatas pada kolom mentah. Ekspresi seperti length(nama), alias yang didefinisikan di daftar SELECT, dan bahkan nomor posisi kolom semuanya sah dipakai:
-- Urut berdasarkan ekspresi: panjang nama terpendek lebih dulu,
-- lalu nama sebagai pemutus seri di dalam kelompok panjang yang sama.
SELECT nama, length(nama) AS panjang
FROM pegawai
ORDER BY length(nama), nama;
-- Urut berdasarkan alias hasil SELECT: gaji_tahunan adalah kolom hitungan.
SELECT nama, gaji * 12 AS gaji_tahunan
FROM pegawai
ORDER BY gaji_tahunan DESC, id;
-- Urut berdasarkan nomor posisi kolom: 2 = kolom gaji, 1 = kolom nama.
-- Valid, tapi rapuh — cukup sekadar tahu bentuknya, hindari di kode nyata.
SELECT nama, gaji
FROM pegawai
ORDER BY 2 DESC, 1;Output yang Dihasilkan:
nama | panjang
--------------+---------
Agus Salim | 10
Maya Putri | 10
Andi Wijaya | 11
Rina Melati | 11
Siti Rahayu | 11
Budi Santoso | 12
Dewi Lestari | 12
Rudi Hartono | 12
(8 rows)
nama | gaji_tahunan
--------------+--------------
Dewi Lestari | 72000000.00
Rina Melati | 72000000.00
Maya Putri | 66000000.00
Agus Salim | 62400000.00
Siti Rahayu | 60000000.00
Andi Wijaya | 60000000.00
Budi Santoso | 54000000.00
Rudi Hartono | 54000000.00
(8 rows)
nama | gaji
--------------+------------
Dewi Lestari | 6000000.00
Rina Melati | 6000000.00
Maya Putri | 5500000.00
Agus Salim | 5200000.00
Andi Wijaya | 5000000.00
Siti Rahayu | 5000000.00
Budi Santoso | 4500000.00
Rudi Hartono | 4500000.00
(8 rows)Tiga catatan untuk contoh di atas:
- Ekspresi
length(nama)mengurutkan nama terpendek lebih dulu —Agus SalimdanMaya Putri(10 karakter) membuka daftar — dengannamamemecah seri di dalam tiap kelompok panjang.ORDER BYboleh memakai ekspresi apa pun yang valid di konteks query. - Alias
gaji_tahunanbisa dirujuk langsung diORDER BYmeski alias itu baru diciptakan di barisSELECTyang sama — fitur yang membuat query hitungan jauh lebih terbaca dibanding mengulanggaji * 12di klausa sort. - Nomor posisi
2berarti “kolom kedua daftar SELECT” (gaji). Bentuk ini warisan SQL lama: rapuh terhadap penyusunan ulang kolom dan tidak membawa informasi makna.
Bila nomor posisi merujuk kolom yang tidak ada, query ditolak:
-- Nomor posisi harus merujuk kolom yang benar-benar ada di daftar SELECT.
-- Daftar SELECT hanya punya 2 kolom (nama, gaji), sehingga posisi 3 tidak sah.
SELECT nama, gaji
FROM pegawai
ORDER BY 3;Output yang Dihasilkan:
psql:order-by-error.sql:5: ERROR: ORDER BY position 3 is not in select list
LINE 3: ORDER BY 3;
^Error ORDER BY position 3 is not in select list muncul pada saat parse — query tidak pernah dieksekusi. Pesan ini juga mengingatkan bahwa nomor posisi dihitung terhadap daftar SELECT, bukan terhadap kolom tabel.
Kebiasaan Course Ini: id Sebagai Pemutus Seri #
Perhatikan bahwa hampir semua query di chapter ini diakhiri , id di klausa ORDER BY. Bukan hiasan gaya — itu jaminan determinisme. Dua pasang gaji di data kita identik (Dewi dan Rina sama-sama 6.000.000; Budi dan Rudi sama-sama 4.500.000), dan klausa ORDER BY gaji DESC saja tidak mengatakan apa-apa tentang urutan dua baris yang serinya sama. Server boleh mengembalikan Dewi dulu hari ini dan Rina dulu besok — keduanya sah menurut standar SQL:
-- Dua pasang gaji identik: (Budi, Rudi) = 4.500.000 dan (Dewi, Rina) = 6.000.000.
-- Kolom sort terakhir "id" memastikan urutan di dalam tiap kelompok seri
-- selalu sama, di mesin mana pun, kapan pun.
SELECT id, nama, gaji
FROM pegawai
ORDER BY gaji DESC, id;
-- Pola yang sama bekerja untuk multi-kolom: gaji dulu, departemen kedua,
-- id sebagai pemutus seri terakhir.
SELECT id, nama, departemen, gaji
FROM pegawai
ORDER BY gaji DESC, departemen, id;Output yang Dihasilkan:
id | nama | gaji
----+--------------+------------
4 | Dewi Lestari | 6000000.00
8 | Rina Melati | 6000000.00
6 | Maya Putri | 5500000.00
7 | Agus Salim | 5200000.00
2 | Siti Rahayu | 5000000.00
3 | Andi Wijaya | 5000000.00
1 | Budi Santoso | 4500000.00
5 | Rudi Hartono | 4500000.00
(8 rows)
id | nama | departemen | gaji
----+--------------+------------+------------
4 | Dewi Lestari | penjualan | 6000000.00
8 | Rina Melati | penjualan | 6000000.00
6 | Maya Putri | penjualan | 5500000.00
7 | Agus Salim | kasir | 5200000.00
3 | Andi Wijaya | gudang | 5000000.00
2 | Siti Rahayu | kasir | 5000000.00
1 | Budi Santoso | gudang | 4500000.00
5 | Rudi Hartono | gudang | 4500000.00
(8 rows)Pada query pertama, kelompok 6.000.000 selalu tampil sebagai Dewi (id 4) lalu Rina (id 8), dan kelompok 4.500.000 sebagai Budi (id 1) lalu Rudi (id 5) — bukan karena kebetulan penyimpanan, melainkan karena id menuntaskan urutan yang gaji tidak bisa putuskan. Query kedua memperlihatkan pola berlapisnya: gaji menempatkan kedua pegawai 6.000.000 di puncak, departemen baru bekerja setelah gaji seri, dan id menutup sisa ambiguitas.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkasCREATE TABLE pegawai (
id integer PRIMARY KEY,
nama text NOT NULL,
departemen text NOT NULL,
gaji numeric(12, 2) NOT NULL,
tanggal_masuk date
);
INSERT INTO pegawai (id, nama, departemen, gaji, tanggal_masuk) VALUES
(1, 'Budi Santoso', 'gudang', 4500000, DATE '2024-03-01'),
(2, 'Siti Rahayu', 'kasir', 5000000, DATE '2023-07-15'),
(3, 'Andi Wijaya', 'gudang', 5000000, DATE '2025-01-20'),
(4, 'Dewi Lestari', 'penjualan', 6000000, NULL),
(5, 'Rudi Hartono', 'gudang', 4500000, DATE '2025-06-01'),
(6, 'Maya Putri', 'penjualan', 5500000, DATE '2024-11-10'),
(7, 'Agus Salim', 'kasir', 5200000, NULL),
(8, 'Rina Melati', 'penjualan', 6000000, DATE '2023-02-01');
null-first-last.sql
Unduh berkas-- ASC: default-nya NULLS LAST — dua baris ber-tanggal NULL menggerombol di akhir.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk ASC, id;
-- DESC: default-nya NULLS FIRST — baris NULL pindah ke depan.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk DESC, id;
-- NULLS FIRST memaksa NULL ke depan meski arah sort naik.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk ASC NULLS FIRST, id;
-- NULLS LAST memaksa NULL ke belakang meski arah sort turun.
SELECT nama, tanggal_masuk
FROM pegawai
ORDER BY tanggal_masuk DESC NULLS LAST, id;
order-by-error.sql
Unduh berkas-- Nomor posisi harus merujuk kolom yang benar-benar ada di daftar SELECT.
-- Daftar SELECT hanya punya 2 kolom (nama, gaji), sehingga posisi 3 tidak sah.
SELECT nama, gaji
FROM pegawai
ORDER BY 3;
tie-break.sql
Unduh berkas-- Dua pasang gaji identik: (Budi, Rudi) = 4.500.000 dan (Dewi, Rina) = 6.000.000.
-- Kolom sort terakhir "id" memastikan urutan di dalam tiap kelompok seri
-- selalu sama, di mesin mana pun, kapan pun.
SELECT id, nama, gaji
FROM pegawai
ORDER BY gaji DESC, id;
-- Pola yang sama bekerja untuk multi-kolom: gaji dulu, departemen kedua,
-- id sebagai pemutus seri terakhir.
SELECT id, nama, departemen, gaji
FROM pegawai
ORDER BY gaji DESC, departemen, id;
urutan-dasar.sql
Unduh berkas-- ASC adalah arah default: dari nilai terkecil ke terbesar (A ke Z).
SELECT nama, gaji
FROM pegawai
ORDER BY nama ASC, id;
-- DESC membalik arah: dari gaji terbesar ke terkecil.
-- id dipasang sebagai pemutus seri karena dua pasang gaji sama persis.
SELECT nama, gaji
FROM pegawai
ORDER BY gaji DESC, id;
-- Banyak kolom dengan arah campuran: departemen naik (A-Z),
-- lalu gaji turun di dalam tiap departemen, lalu id pemutus seri.
SELECT nama, departemen, gaji
FROM pegawai
ORDER BY departemen ASC, gaji DESC, id;
urutan-ekspresi.sql
Unduh berkas-- Urut berdasarkan ekspresi: panjang nama terpendek lebih dulu,
-- lalu nama sebagai pemutus seri di dalam kelompok panjang yang sama.
SELECT nama, length(nama) AS panjang
FROM pegawai
ORDER BY length(nama), nama;
-- Urut berdasarkan alias hasil SELECT: gaji_tahunan adalah kolom hitungan.
SELECT nama, gaji * 12 AS gaji_tahunan
FROM pegawai
ORDER BY gaji_tahunan DESC, id;
-- Urut berdasarkan nomor posisi kolom: 2 = kolom gaji, 1 = kolom nama.
-- Valid, tapi rapuh — cukup sekadar tahu bentuknya, hindari di kode nyata.
SELECT nama, gaji
FROM pegawai
ORDER BY 2 DESC, 1;