Chapter sebelumnya menutup dengan keluhan yang sudah lama menyertainya: query bertingkat membuat pembaca menghitung tanda kurung. Derived table menyelesaikan separuh masalah — agregat dulu, gabung kemudian — tetapi tetap menumpuk tahapan di dalam satu kurung raksasa. Ketika kebutuhannya tiga atau empat tahap (ringkas, saring, gabungkan, hitung persentase), bentuk bersarang itu berubah menjadi teka-teki yang harus diurai dari dalam ke luar. SQL menjawab dengan memberi nama pada tiap tahapan perhitungan: WITH nama AS (query). Bentuk ini bernama Common Table Expression (CTE) — hasil antara bernama yang hanya hidup selama satu pernyataan dan bisa dipakai seperti tabel. Chapter ini membangun CTE dari bentuk dasarnya, merantainya, membandingkannya dengan derived table, lalu mengupas perilaku planner yang menentukan kapan CTE dihitung sekali atau ditempel ke query utama.
Tabel Latihan: produk dan penjualan #
Domain tetap section ini kembali: 10 produk Toko Maju Jaya dan 10 penjualan Januari–Februari 2026, termasuk baris id 8 dengan produk_id NULL yang membuat agregasi penjualan selalu memerlukan penjaga IS NOT NULL. Data ini akan kita angkat menjadi CTE bernama ringkas dan terjual, lalu dirujuk berkali-kali dalam satu pernyataan:
-- Tabel latihan chapter Common Table Expressions:
-- katalog produk dan penjualan Toko Maju Jaya.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10,2) NOT NULL, -- harga satuan, rupiah
stok integer NOT NULL
);
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
produk_id integer REFERENCES produk (id), -- NULL = barang tanpa kode produk
pelanggan_id integer NOT NULL,
jumlah integer NOT NULL -- unit terjual
);
INSERT INTO produk (id, nama, kategori, harga, stok) VALUES
( 1, 'Kopi Gayo', 'Minuman', 85000.00, 40),
( 2, 'Kopi Toraja', 'Minuman', 90000.00, 25),
( 3, 'Teh Melati', 'Minuman', 35000.00, 60),
( 4, 'Teh Hijau', 'Minuman', 40000.00, 50),
( 5, 'Gula Merah', 'Sembako', 15000.00, 100),
( 6, 'Beras Pandan Wangi', 'Sembako', 13000.00, 200),
( 7, 'Susu Kental Manis', 'Minuman', 18000.00, 30),
( 8, 'Keripik Singkong', 'Camilan', 20000.00, 45),
( 9, 'Kacang Goreng', 'Camilan', 25000.00, 55),
(10, 'Madu Hutan', 'Sembako', 55000.00, 15);
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah) VALUES
( 1, DATE '2026-01-05', 1, 1, 10),
( 2, DATE '2026-01-08', 3, 2, 25),
( 3, DATE '2026-01-12', 5, 1, 40),
( 4, DATE '2026-01-15', 2, 1, 8),
( 5, DATE '2026-01-20', 1, 4, 12),
( 6, DATE '2026-01-22', 4, 2, 15),
( 7, DATE '2026-01-25', 5, 5, 30),
( 8, DATE '2026-02-02', NULL, 1, 20),
( 9, DATE '2026-02-05', 1, 4, 18),
(10, DATE '2026-02-10', 6, 2, 22);
SELECT id, nama, kategori, harga, stok FROM produk ORDER BY id;
SELECT id, tanggal, produk_id, pelanggan_id, jumlah FROM penjualan ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
INSERT 0 10
INSERT 0 10
id | nama | kategori | harga | stok
----+--------------------+----------+----------+------
1 | Kopi Gayo | Minuman | 85000.00 | 40
2 | Kopi Toraja | Minuman | 90000.00 | 25
3 | Teh Melati | Minuman | 35000.00 | 60
4 | Teh Hijau | Minuman | 40000.00 | 50
5 | Gula Merah | Sembako | 15000.00 | 100
6 | Beras Pandan Wangi | Sembako | 13000.00 | 200
7 | Susu Kental Manis | Minuman | 18000.00 | 30
8 | Keripik Singkong | Camilan | 20000.00 | 45
9 | Kacang Goreng | Camilan | 25000.00 | 55
10 | Madu Hutan | Sembako | 55000.00 | 15
(10 rows)
id | tanggal | produk_id | pelanggan_id | jumlah
----+------------+-----------+--------------+--------
1 | 2026-01-05 | 1 | 1 | 10
2 | 2026-01-08 | 3 | 2 | 25
3 | 2026-01-12 | 5 | 1 | 40
4 | 2026-01-15 | 2 | 1 | 8
5 | 2026-01-20 | 1 | 4 | 12
6 | 2026-01-22 | 4 | 2 | 15
7 | 2026-01-25 | 5 | 5 | 30
8 | 2026-02-02 | | 1 | 20
9 | 2026-02-05 | 1 | 4 | 18
10 | 2026-02-10 | 6 | 2 | 22
(10 rows)WITH: Memberi Nama pada Tahapan Perhitungan #
Bentuk paling sederhananya: WITH nama AS (query) mendefinisikan hasil antara, lalu query utama membaca nama itu seolah-olah nama itu adalah tabel. Contoh pertama merangkum produk per kategori — pekerjaan yang di chapter Subquery dalam FROM
kita lakukan dengan derived table:
-- WITH mendefinisikan CTE: subquery bernama 'ringkas'
-- yang bisa dipakai seperti tabel di query utama.
WITH ringkas AS (
SELECT kategori,
count(*) AS jumlah_produk,
round(avg(harga), 2) AS rerata
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk, rerata
FROM ringkas
ORDER BY kategori;
-- Alias kolom juga bisa dideklarasikan langsung setelah nama CTE:
-- WITH nama (kolom1, kolom2, ...) AS (...).
-- Daftar kolom ini menamai output CTE menggantikan alias di dalam SELECT.
WITH ringkas (kategori, jumlah, rerata) AS (
SELECT kategori, count(*), round(avg(harga), 2)
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah, rerata
FROM ringkas
ORDER BY kategori;Output yang Dihasilkan:
kategori | jumlah_produk | rerata
----------+---------------+----------
Camilan | 2 | 22500.00
Minuman | 5 | 53600.00
Sembako | 3 | 27666.67
(3 rows)
kategori | jumlah | rerata
----------+--------+----------
Camilan | 2 | 22500.00
Minuman | 5 | 53600.00
Sembako | 3 | 27666.67
(3 rows)Kedua query menghasilkan isi yang sama — Camilan 2 produk dengan rata-rata 22500.00, Minuman 5 produk dengan 53600.00, Sembako 3 produk dengan 27666.67 — dan itu memang inti pesannya. Mari kita bedah strukturnya:
WITH ringkas AS (...)— kata kunciWITHmembuka pernyataan;ringkasadalah nama pilihan kita untuk hasil antara; isi kurungnya querySELECTbiasa yang boleh memakaiGROUP BY,WHERE, join, apa pun.SELECT ... FROM ringkas— query utama memperlakukanringkaspersis seperti tabel: kolomnya bisa dipilih, disaring, diurutkan. Nama tahapan menggantikan kurung bersarang.WITH ringkas (kategori, jumlah, rerata) AS (...)— varian kedua memindahkan penamaan kolom ke daftar setelah nama CTE. Daftar itu wajib memuat nama untuk setiap kolom hasil; alias di dalamSELECTboleh dihilangkan karena penamaannya sudah diambil alih dari luar. Hasilnya identik — pilihan gaya, bukan perbedaan perilaku.
Satu batasan penting: CTE hidup hanya selama pernyataan yang mendefinisikannya. Setelah titik-koma, nama ringkas lenyap — query berikutnya harus mendefinisikannya ulang.
CTE Berantai: Tahapan yang Saling Merujuk #
Satu klausa WITH boleh mendefinisikan beberapa CTE dipisah koma, dan CTE kedua serta seterusnya boleh merujuk CTE di atasnya. Inilah bentuk pipeline: hasil mengalir dari tahap ke tahap, tiap tahap punya nama yang bisa dibaca manusia:
-- Beberapa CTE boleh didefinisikan berurutan dipisah koma;
-- CTE kedua dan seterusnya boleh merujuk CTE sebelumnya.
WITH per_kategori AS (
SELECT kategori,
count(*) AS jumlah_produk,
round(avg(harga), 2) AS rerata
FROM produk
GROUP BY kategori
),
mahal AS (
SELECT kategori, jumlah_produk, rerata
FROM per_kategori
WHERE rerata > 25000
)
SELECT kategori, jumlah_produk, rerata
FROM mahal
ORDER BY kategori;Output yang Dihasilkan:
kategori | jumlah_produk | rerata
----------+---------------+----------
Minuman | 5 | 53600.00
Sembako | 3 | 27666.67
(2 rows)Alurnya terbaca dari atas ke bawah tanpa satu pun tanda kurung bersarang:
per_kategori merangkum seluruh katalog menjadi tiga baris; mahal menyaringnya menjadi dua — Minuman (53600.00) dan Sembako (27666.67) lolos, Camilan (22500.00) tersaring; query utama tinggal mengurutkan. Bandingkan dengan bentuk derived table untuk logika yang sama: FROM (SELECT * FROM (SELECT ... GROUP BY ...) AS x WHERE ...) AS y — isi kurung terdalam harus dibaca lebih dulu padahal posisinya paling dalam. Rantai CTE membalik arah baca menjadi atas-ke-bawah, dan tiap tahap bisa diberi nama yang mendeskripsikan perannya.
CTE versus Derived Table #
Kapan memakai CTE, kapan derived table? Query berikut menuliskan laporan yang sama dengan dua gaya — total unit terjual per produk, diurutkan dari yang terbanyak:
-- Query yang sama, dua gaya penulisan.
-- Gaya derived table: subquery di FROM bersarang dalam tanda kurung.
SELECT p.nama, t.total_unit
FROM (SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id) AS t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;
-- Gaya CTE: tahapan diberi nama dulu, query utama tinggal membaca nama.
WITH terjual AS (
SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id
)
SELECT p.nama, t.total_unit
FROM terjual t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;Output yang Dihasilkan:
nama | total_unit
--------------------+------------
Gula Merah | 70
Kopi Gayo | 40
Teh Melati | 25
Beras Pandan Wangi | 22
Teh Hijau | 15
Kopi Toraja | 8
(6 rows)
nama | total_unit
--------------------+------------
Gula Merah | 70
Kopi Gayo | 40
Teh Melati | 25
Beras Pandan Wangi | 22
Teh Hijau | 15
Kopi Toraja | 8
(6 rows)Kedua gaya menghasilkan enam baris yang sama persis — Gula Merah 70 unit di puncak, disusul Kopi Gayo 40, hingga Kopi Toraja 8 — karena keduanya hanyalah cara penulisan yang berbeda untuk perhitungan yang sama. Pembedanya bukan hasil, melainkan struktur:
| Aspek | Derived table | CTE |
|---|---|---|
| Mendefinisikan tahapan | di dalam FROM, bersarang dalam kurung | di klausa WITH, berjajar mendatar |
| Arah baca | dari kurung terdalam ke luar | dari atas ke bawah |
| Satu tahapan dipakai dua kali | kurungnya harus ditulis ulang | cukup rujuk namanya |
| Cocok untuk | satu tahapan sederhana, sekali pakai | beberapa tahapan atau rujukan berulang |
Ketika pipeline cuma satu tahapan, derived table ringan dan langsung. Ketika tahapan bertambah atau satu hasil antara dipakai berulang, CTE menang jauh pada keterbacaan — dan keunggulan kedua itu akan kita buktikan sekarang.
Satu CTE, Banyak Rujukan #
Derived table bersifat sekali pakai: untuk memakainya lagi Anda harus menyalin seluruh kurungnya. CTE cukup didefinisikan sekali lalu dirujuk berkali-kali dalam satu pernyataan. Contoh berikut memakai CTE terjual dua kali — sebagai tabel yang digabungkan, dan sebagai sumber total toko di dalam subquery skalar untuk menghitung persentase:
-- Satu CTE boleh dirujuk berkali-kali dalam satu pernyataan.
-- Di sini 'terjual' dipakai dua kali: sebagai tabel join utama
-- dan sebagai sumber total toko di subquery skalar.
WITH terjual AS (
SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id
)
SELECT p.nama,
t.total_unit,
round(100.0 * t.total_unit / (SELECT sum(total_unit) FROM terjual), 1) AS persen
FROM terjual t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;Output yang Dihasilkan:
nama | total_unit | persen
--------------------+------------+--------
Gula Merah | 70 | 38.9
Kopi Gayo | 40 | 22.2
Teh Melati | 25 | 13.9
Beras Pandan Wangi | 22 | 12.2
Teh Hijau | 15 | 8.3
Kopi Toraja | 8 | 4.4
(6 rows)terjual tampil di klausa FROM dan di dalam (SELECT sum(total_unit) FROM terjual) — dua rujukan, satu definisi. Hasilnya: Gula Merah menyumbang 38.9% dari seluruh unit terjual, Kopi Gayo 22.2%, sampai Kopi Toraja 4.4%. Menulis pola yang sama dengan derived table berarti menggandakan kurung agregasi di dua tempat — dan bila logika hitungannya berubah, dua tempat itu harus berubah serempak. Satu definisi CTE menghilangkan risiko salinan yang berbeda.
Materialisasi: Dihitung Sekali atau Ditempel? #
Pertanyaan lanjutannya pertanyaan performa: ketika terjual dirujuk dua kali, apakah agregasinya dihitung dua kali? Jawabannya ada di planner. Sejak PostgreSQL 12, aturan bawaannya: CTE yang dirujuk tepat sekali di-inline — isinya ditempel ke query utama seperti subquery biasa; CTE yang dirujuk lebih dari sekali dimaterialisasi — dihitung sekali menjadi hasil antara, lalu hasil itu dibaca berulang. Kata kunci MATERIALIZED dan NOT MATERIALIZED memaksa pilihan itu secara eksplisit:
-- PostgreSQL 12+ memungkinkan CTE di-inline (dihitung ulang
-- per rujukan) atau dimaterialisasi (dihitung sekali).
-- Kata kunci MATERIALIZED / NOT MATERIALIZED memaksa pilihan itu.
-- NOT MATERIALIZED: planner bebas menempelkan isi CTE ke query utama.
WITH ringkas AS NOT MATERIALIZED (
SELECT kategori, count(*) AS jumlah_produk
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk FROM ringkas
ORDER BY kategori;
-- MATERIALIZED: CTE dihitung sekali menjadi hasil perantara tersendiri.
WITH ringkas AS MATERIALIZED (
SELECT kategori, count(*) AS jumlah_produk
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk FROM ringkas
ORDER BY kategori;Output yang Dihasilkan:
kategori | jumlah_produk
----------+---------------
Camilan | 2
Minuman | 5
Sembako | 3
(3 rows)
kategori | jumlah_produk
----------+---------------
Camilan | 2
Minuman | 5
Sembako | 3
(3 rows)Hasil kedua query identik — Camilan 2, Minuman 5, Sembako 3 — karena kata kunci ini sama sekali tidak mengubah logika, hanya cara eksekusinya. NOT MATERIALIZED membuka pintu optimasi: planner boleh menyaring atau mengurutkan lebih dulu supaya agregasi bekerja pada data lebih kecil. MATERIALIZED menutup pintu itu tetapi memberi jaminan lain: CTE dihitung tepat sekali, dan hasil antaranya terpisah dari query utama — berguna saat isi CTE mahal atau saat Anda sengaja ingin memagari bagian query.
CTE Pengubah Data: INSERT … RETURNING #
Satu kemampuan CTE yang tidak dimiliki derived table: CTE boleh berisi pernyataan yang mengubah data — INSERT, UPDATE, atau DELETE — asalkan mengembalikan baris lewat RETURNING. Baris yang dikembalikan itu menjadi isi CTE dan bisa langsung dibaca query utama, satu pernyataan tanpa bolak-balik:
-- CTE pengubah data: INSERT ... RETURNING menghasilkan baris yang
-- langsung bisa dibaca oleh query utama -- satu pernyataan, satu putaran.
-- UPDATE dan DELETE juga bisa berperan sebagai CTE dengan pola yang sama.
WITH penjualan_baru AS (
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah)
VALUES (11, DATE '2026-02-15', 7, 3, 5)
RETURNING id, tanggal, produk_id, jumlah
)
SELECT pb.id, p.nama, pb.jumlah
FROM penjualan_baru pb
INNER JOIN produk p ON p.id = pb.produk_id
ORDER BY pb.id;Output yang Dihasilkan:
id | nama | jumlah
----+-------------------+--------
11 | Susu Kental Manis | 5
(1 row)Satu baris penjualan baru (id 11, 5 unit) benar-benar masuk ke tabel penjualan, dan RETURNING menyerahkan barisnya ke query utama yang langsung menggabungkannya ke produk — Susu Kental Manis akhirnya terjual. Pola “ubah lalu laporkan” seperti ini menempati satu pernyataan atomik: laporan dan perubahan tidak bisa terpisah. Pembahasan penuh perintah pengubah data menunggu section Manipulasi Data
; yang perlu diingat sekarang: kapanpun satu tahapan perhitungan butuh nama, WITH menyediakannya.
CTE memberi nama pada tahapan, dan itu menyelesaikan masalah keterbacaan. Namun ada kelas masalah yang tidak bisa dipecahkan tahapan datar mana pun: data yang berbentuk pohon — karyawan yang punya atasan yang juga punya atasan. Menelusuri rantai berhierarki butuh query yang memanggil dirinya sendiri, dan PostgreSQL menyediakannya lewat satu kata kunci tambahan: WITH RECURSIVE. Chapter berikutnya membahasnya: Recursive CTE
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Tabel latihan chapter Common Table Expressions:
-- katalog produk dan penjualan Toko Maju Jaya.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10,2) NOT NULL, -- harga satuan, rupiah
stok integer NOT NULL
);
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
produk_id integer REFERENCES produk (id), -- NULL = barang tanpa kode produk
pelanggan_id integer NOT NULL,
jumlah integer NOT NULL -- unit terjual
);
INSERT INTO produk (id, nama, kategori, harga, stok) VALUES
( 1, 'Kopi Gayo', 'Minuman', 85000.00, 40),
( 2, 'Kopi Toraja', 'Minuman', 90000.00, 25),
( 3, 'Teh Melati', 'Minuman', 35000.00, 60),
( 4, 'Teh Hijau', 'Minuman', 40000.00, 50),
( 5, 'Gula Merah', 'Sembako', 15000.00, 100),
( 6, 'Beras Pandan Wangi', 'Sembako', 13000.00, 200),
( 7, 'Susu Kental Manis', 'Minuman', 18000.00, 30),
( 8, 'Keripik Singkong', 'Camilan', 20000.00, 45),
( 9, 'Kacang Goreng', 'Camilan', 25000.00, 55),
(10, 'Madu Hutan', 'Sembako', 55000.00, 15);
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah) VALUES
( 1, DATE '2026-01-05', 1, 1, 10),
( 2, DATE '2026-01-08', 3, 2, 25),
( 3, DATE '2026-01-12', 5, 1, 40),
( 4, DATE '2026-01-15', 2, 1, 8),
( 5, DATE '2026-01-20', 1, 4, 12),
( 6, DATE '2026-01-22', 4, 2, 15),
( 7, DATE '2026-01-25', 5, 5, 30),
( 8, DATE '2026-02-02', NULL, 1, 20),
( 9, DATE '2026-02-05', 1, 4, 18),
(10, DATE '2026-02-10', 6, 2, 22);
SELECT id, nama, kategori, harga, stok FROM produk ORDER BY id;
SELECT id, tanggal, produk_id, pelanggan_id, jumlah FROM penjualan ORDER BY id;
cte-dipakai-dua-kali.sql
Unduh berkas-- Satu CTE boleh dirujuk berkali-kali dalam satu pernyataan.
-- Di sini 'terjual' dipakai dua kali: sebagai tabel join utama
-- dan sebagai sumber total toko di subquery skalar.
WITH terjual AS (
SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id
)
SELECT p.nama,
t.total_unit,
round(100.0 * t.total_unit / (SELECT sum(total_unit) FROM terjual), 1) AS persen
FROM terjual t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;
cte-vs-derived.sql
Unduh berkas-- Query yang sama, dua gaya penulisan.
-- Gaya derived table: subquery di FROM bersarang dalam tanda kurung.
SELECT p.nama, t.total_unit
FROM (SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id) AS t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;
-- Gaya CTE: tahapan diberi nama dulu, query utama tinggal membaca nama.
WITH terjual AS (
SELECT produk_id, sum(jumlah) AS total_unit
FROM penjualan
WHERE produk_id IS NOT NULL
GROUP BY produk_id
)
SELECT p.nama, t.total_unit
FROM terjual t
INNER JOIN produk p ON p.id = t.produk_id
ORDER BY t.total_unit DESC, p.nama;
materialisasi.sql
Unduh berkas-- PostgreSQL 12+ memungkinkan CTE di-inline (dihitung ulang
-- per rujukan) atau dimaterialisasi (dihitung sekali).
-- Kata kunci MATERIALIZED / NOT MATERIALIZED memaksa pilihan itu.
-- NOT MATERIALIZED: planner bebas menempelkan isi CTE ke query utama.
WITH ringkas AS NOT MATERIALIZED (
SELECT kategori, count(*) AS jumlah_produk
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk FROM ringkas
ORDER BY kategori;
-- MATERIALIZED: CTE dihitung sekali menjadi hasil perantara tersendiri.
WITH ringkas AS MATERIALIZED (
SELECT kategori, count(*) AS jumlah_produk
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk FROM ringkas
ORDER BY kategori;
with-berantai.sql
Unduh berkas-- Beberapa CTE boleh didefinisikan berurutan dipisah koma;
-- CTE kedua dan seterusnya boleh merujuk CTE sebelumnya.
WITH per_kategori AS (
SELECT kategori,
count(*) AS jumlah_produk,
round(avg(harga), 2) AS rerata
FROM produk
GROUP BY kategori
),
mahal AS (
SELECT kategori, jumlah_produk, rerata
FROM per_kategori
WHERE rerata > 25000
)
SELECT kategori, jumlah_produk, rerata
FROM mahal
ORDER BY kategori;
with-dasar.sql
Unduh berkas-- WITH mendefinisikan CTE: subquery bernama 'ringkas'
-- yang bisa dipakai seperti tabel di query utama.
WITH ringkas AS (
SELECT kategori,
count(*) AS jumlah_produk,
round(avg(harga), 2) AS rerata
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah_produk, rerata
FROM ringkas
ORDER BY kategori;
-- Alias kolom juga bisa dideklarasikan langsung setelah nama CTE:
-- WITH nama (kolom1, kolom2, ...) AS (...).
-- Daftar kolom ini menamai output CTE menggantikan alias di dalam SELECT.
WITH ringkas (kategori, jumlah, rerata) AS (
SELECT kategori, count(*), round(avg(harga), 2)
FROM produk
GROUP BY kategori
)
SELECT kategori, jumlah, rerata
FROM ringkas
ORDER BY kategori;
with-insert-returning.sql
Unduh berkas-- CTE pengubah data: INSERT ... RETURNING menghasilkan baris yang
-- langsung bisa dibaca oleh query utama -- satu pernyataan, satu putaran.
-- UPDATE dan DELETE juga bisa berperan sebagai CTE dengan pola yang sama.
WITH penjualan_baru AS (
INSERT INTO penjualan (id, tanggal, produk_id, pelanggan_id, jumlah)
VALUES (11, DATE '2026-02-15', 7, 3, 5)
RETURNING id, tanggal, produk_id, jumlah
)
SELECT pb.id, p.nama, pb.jumlah
FROM penjualan_baru pb
INNER JOIN produk p ON p.id = pb.produk_id
ORDER BY pb.id;