Dua daftar yang berbeda sering perlu dijadikan satu: “produk yang stoknya menipis” dan “produk yang harganya mahal” adalah dua pertanyaan berbeda, tetapi laporan stok gudang kadang ingin keduanya sekaligus — tanpa duplikat, atau justru dengan duplikat sebagai penanda. Alih-alih menjalankan dua query lalu menyalin-tempel hasilnya, SQL menyediakan empat operator set yang bekerja langsung pada keluaran SELECT: UNION, UNION ALL, INTERSECT, dan EXCEPT. Perilakunya mengikuti teori himpunan — dan, seperti biasa, ada aturan teknis yang harus dipatuhi.

Data Contoh #

Chapter ini hanya membutuhkan tabel produk dari domain toko:

sql
CREATE TABLE produk (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  harga INTEGER NOT NULL,
  stok INTEGER
);

INSERT INTO produk (id, nama, harga, stok) VALUES
  (1, 'Kopi Gayo', 45000, 20), (2, 'Teh Melati', 18000, 35),
  (3, 'Gula Aren', 25000, 15), (4, 'Sirup Vanila', 25000, NULL),
  (5, 'Kopi Toraja', 52000, 10), (6, 'Teh Hijau', 22000, 0),
  (7, 'Cangkir Keramik', 35000, 8);

Kita akan memakai dua kumpulan yang sama berulang kali, jadi mari sepakati dulu isinya. Kumpulan A adalah produk dengan stok menipis (15 ke bawah) — perhatikan bahwa Sirup Vanila tidak masuk karena NULL <= 15 bernilai NULL, persis perilaku yang dibahas pada Chapter Operator Lanjutan: IN, BETWEEN, IS NULL. Kumpulan B adalah produk mahal (50000 ke atas):

sql
SELECT nama FROM produk WHERE stok <= 15 ORDER BY nama;

Output yang Dihasilkan:

text
Cangkir Keramik
Gula Aren
Kopi Toraja
Teh Hijau
sql
SELECT nama FROM produk WHERE harga >= 50000 ORDER BY nama;

Output yang Dihasilkan:

text
Kopi Toraja

UNION dan UNION ALL #

UNION menggabungkan hasil kedua query lalu membuang baris duplikat:

sql
SELECT nama FROM produk WHERE stok <= 15
UNION
SELECT nama FROM produk WHERE harga >= 50000
ORDER BY nama;

Output yang Dihasilkan:

text
Cangkir Keramik
Gula Aren
Kopi Toraja
Teh Hijau

Empat baris — Kopi Toraja hanya sekali meskipun memenuhi kedua syarat. UNION ALL mempertahankan duplikat apa adanya:

sql
SELECT nama FROM produk WHERE stok <= 15
UNION ALL
SELECT nama FROM produk WHERE harga >= 50000
ORDER BY nama;

Output yang Dihasilkan:

text
Cangkir Keramik
Gula Aren
Kopi Toraja
Kopi Toraja
Teh Hijau

Kopi Toraja kini tampil dua kali — satu dari tiap sumber. Perbedaan ini bukan detail kosmetik: penghapusan duplikat menuntut engine SQLite melakukan langkah sortir dan pembandingan ekstra, sedangkan UNION ALL hanya menempelkan hasil. Untuk data besar di mana duplikat mustahil terjadi (misalnya rentang id yang tidak tumpang tindih), UNION ALL lebih cepat.

INTERSECT dan EXCEPT #

INTERSECT mempertahankan baris yang muncul di kedua sisi — irisan himpunan:

sql
SELECT nama FROM produk WHERE stok <= 15
INTERSECT
SELECT nama FROM produk WHERE harga >= 50000;

Output yang Dihasilkan:

text
Kopi Toraja

EXCEPT mempertahankan baris sisi kiri yang tidak ada di sisi kanan — selisih himpunan. Urutan sisi penting, dan ini bukan operasi komutatif:

sql
SELECT nama FROM produk WHERE stok <= 15
EXCEPT
SELECT nama FROM produk WHERE harga >= 50000
ORDER BY nama;

Output yang Dihasilkan:

text
Cangkir Keramik
Gula Aren
Teh Hijau

Dibalik posisinya, EXCEPT akan mengembalikan himpunan B dikurangi A — dalam data kita hasilnya kosong karena B hanya berisi Kopi Toraja yang juga anggota A.

Aturan Kolom: Jumlah dan Kompatibilitas #

Kedua sisi operator set harus menghasilkan jumlah kolom yang sama, dan tipe data tiap kolom harus sebanding. Melanggarnya langsung ditolak pada tahap persiapan query:

bash
sqlite3 toko.db "SELECT nama FROM produk WHERE stok <= 15 EXCEPT SELECT id, nama FROM produk WHERE harga >= 50000;"

Output yang Dihasilkan:

text
Error: in prepare, SELECTs to the left and right of EXCEPT do not have the same number of result columns

Nama kolom hasil mengikuti sisi kiri. Sisi kanan boleh memberi nama kolom berbeda, tetapi nama itu dibuang.

ORDER BY Hanya di Ujung #

Satu ORDER BY boleh ditulis dan hanya boleh ditulis di akhir keseluruhan query — memformat gabungan akhir, bukan per sisi:

sql
SELECT nama FROM produk WHERE stok <= 15
UNION
SELECT nama FROM produk WHERE harga >= 50000
ORDER BY nama DESC;

Output yang Dihasilkan:

text
Teh Hijau
Kopi Toraja
Gula Aren
Cangkir Keramik

Menaruh ORDER BY di tengah (sebelum UNION) akan ditolak dengan galat ORDER BY clause should come after UNION not before, karena parser membaca keseluruhan gabungan sebagai satu statement dengan satu hasil akhir.

OperatorBaris hasilDuplikat
UNIONSemua baris kedua sisiDihapus
UNION ALLSemua baris kedua sisiDipertahankan
INTERSECTHanya yang ada di kedua sisiDihapus
EXCEPTSisi kiri tanpa isi sisi kananDihapus

Penyusunan hasil terakhir yang belum kita bahas adalah logika kondisional di dalam satu query — “jika harganya segini maka labelnya begitu” — yang diselesaikan oleh ekspresi CASE pada chapter penutup section ini.