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:

sql buat-tabel.sql
-- 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:

text
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:

sql with-dasar.sql
-- 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:

text
 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:

  1. WITH ringkas AS (...) — kata kunci WITH membuka pernyataan; ringkas adalah nama pilihan kita untuk hasil antara; isi kurungnya query SELECT biasa yang boleh memakai GROUP BY, WHERE, join, apa pun.
  2. SELECT ... FROM ringkas — query utama memperlakukan ringkas persis seperti tabel: kolomnya bisa dipilih, disaring, diurutkan. Nama tahapan menggantikan kurung bersarang.
  3. 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 dalam SELECT boleh 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:

text
 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:

text
        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:

AspekDerived tableCTE
Mendefinisikan tahapandi dalam FROM, bersarang dalam kurungdi klausa WITH, berjajar mendatar
Arah bacadari kurung terdalam ke luardari atas ke bawah
Satu tahapan dipakai dua kalikurungnya harus ditulis ulangcukup rujuk namanya
Cocok untuksatu tahapan sederhana, sekali pakaibeberapa 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:

text
        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:

text
 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:

text
 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;