Chapter sebelumnya menunjukkan bahwa planner memilih rencana termurah berdasarkan model biaya. Tetapi model biaya hanya menghitung; yang menentukan apa yang dihitung adalah pengetahuan planner tentang data Anda — dan pengetahuan itu bernama statistik. Tanpa statistik, planner menebak dengan asumsi default yang sering salah ribuan kali; dengan statistik yang baik, angka rows di EXPLAIN nyaris menyentuh kenyataan. Chapter ini membuka seluruh rantainya: perintah ANALYZE yang membentuk statistik lewat pengambilan sampel, isi view pg_stats yang dibaca planner, cara menaikkan kedalaman statistik per kolom, statistik ekstensif untuk kolom yang saling berkorelasi, dan autoanalyze yang menjaga semuanya tetap mutakhir. Tabel latihan kita sengaja memakai distribusi timpang — satu kota menguasai 40% baris — supaya setiap melesetnya estimasi terlihat telanjang.

sql buat-tabel.sql
-- Satu tabel pelanggan dengan distribusi yang sengaja timpang:
-- kota Jakarta menguasai 40% baris, Jayapura hanya 2%.
-- Kolom zona selalu ditentukan oleh kota (korelasi sempurna) --
-- bahan demo statistik ekstensif di bagian akhir chapter.
-- autovacuum dimatikan untuk tabel ini agar demo deterministik:
-- hanya ANALYZE manual yang boleh memperbarui statistiknya.
CREATE TABLE pelanggan (
    id    bigint PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL,
    zona  text NOT NULL,
    saldo numeric(12,2) NOT NULL
) WITH (autovacuum_enabled = off);

INSERT INTO pelanggan (id, nama, kota, zona, saldo)
SELECT k.g,
       'Pelanggan ' || k.g,
       k.kota,
       CASE k.kota
           WHEN 'Jakarta'    THEN 'WIB'
           WHEN 'Bandung'    THEN 'WIB'
           WHEN 'Surabaya'   THEN 'WIB'
           WHEN 'Medan'      THEN 'WIB'
           WHEN 'Denpasar'   THEN 'WITA'
           WHEN 'Makassar'   THEN 'WITA'
           WHEN 'Balikpapan' THEN 'WITA'
           ELSE 'WIT'
       END,
       (k.g * 137.75)::numeric(12,2)
FROM (
    SELECT g,
           CASE
               WHEN g % 100 < 40 THEN 'Jakarta'
               WHEN g % 100 < 60 THEN 'Bandung'
               WHEN g % 100 < 75 THEN 'Surabaya'
               WHEN g % 100 < 83 THEN 'Medan'
               WHEN g % 100 < 90 THEN 'Denpasar'
               WHEN g % 100 < 95 THEN 'Makassar'
               WHEN g % 100 < 98 THEN 'Balikpapan'
               ELSE 'Jayapura'
           END AS kota
    FROM generate_series(1, 10000) AS g
) AS k;

CREATE INDEX idx_pelanggan_kota ON pelanggan (kota);

-- Bukti: sebelum ANALYZE, view pg_stats belum punya entri untuk tabel ini.
SELECT count(*) AS baris_statistik_tersedia
FROM pg_stats
WHERE tablename = 'pelanggan';

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 10000
CREATE INDEX
 baris_statistik_tersedia 
--------------------------
                        0
(1 row)

Estimasi Buta Tanpa Statistik #

Blok penutup di atas adalah keadaan awal yang menarik: pg_stats kosong total. Planner memang tahu tabel ini kira-kira berisi 10.000 baris — angka itu ikut tercatat saat index dibangun — tetapi planner tidak tahu apa pun tentang distribusi kolom kota. Untuk perbandingan kesetaraan tanpa statistik, planner memakai selectivity bawaan 0,5%. Perhatikan estimasi berikut sebelum dan sesudah ANALYZE:

-- Sebelum ANALYZE, planner tidak tahu apa pun tentang distribusi kolom kota.
-- Estimasi berikut ditangkap pada keadaan tanpa statistik.
-- Angka biaya dapat sedikit berbeda di mesin Anda.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jakarta';

-- Bandingkan dengan kenyataannya:
SELECT count(nama) AS baris_jakarta
FROM pelanggan
WHERE kota = 'Jakarta';

-- Bentuk statistik dengan ANALYZE, lalu lihat estimasinya kembali.
ANALYZE pelanggan;

EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jakarta';

-- Filter rentang pada kolom numerik memakai sumber estimasi lain: histogram.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE saldo < 700000;

Output yang Dihasilkan:

text
                                       QUERY PLAN                                        
----------------------------------------------------------------------------------------
 Aggregate  (cost=87.14..87.15 rows=1 width=8)
   ->  Bitmap Heap Scan on pelanggan  (cost=4.67..87.02 rows=50 width=32)
         Recheck Cond: (kota = 'Jakarta'::text)
         ->  Bitmap Index Scan on idx_pelanggan_kota  (cost=0.00..4.66 rows=50 width=0)
               Index Cond: (kota = 'Jakarta'::text)
(5 rows)

 baris_jakarta 
---------------
          4000
(1 row)

ANALYZE
                                        QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Aggregate  (cost=205.28..205.29 rows=1 width=8)
   ->  Bitmap Heap Scan on pelanggan  (cost=51.28..195.28 rows=4000 width=14)
         Recheck Cond: (kota = 'Jakarta'::text)
         ->  Bitmap Index Scan on idx_pelanggan_kota  (cost=0.00..50.28 rows=4000 width=0)
               Index Cond: (kota = 'Jakarta'::text)
(5 rows)

                             QUERY PLAN                              
---------------------------------------------------------------------
 Aggregate  (cost=231.70..231.71 rows=1 width=8)
   ->  Seq Scan on pelanggan  (cost=0.00..219.00 rows=5081 width=14)
         Filter: (saldo < '700000'::numeric)
(3 rows)

Mari kita bedah tiga momen pada output di atas:

  1. Sebelum ANALYZE: planner memperkirakan rows=50 — hasil 0,5% × 10.000. Kenyataannya 4.000 baris: perkiraan meleset 80 kali. Biaya bitmap scan (87,02) dihitung dari perkiraan yang salah itu.
  2. Setelah ANALYZE: perkiraan melonjak ke rows=4000 — persis kenyataan. Planner kini tahu Jakarta menguasai 40% baris, dan angka biaya pun dihitung ulang menjadi 195,28.
  3. Filter rentang saldo < 700000: perkiraan rows=5081 datang dari sumber lain — histogram kolom saldo yang membagi rentang nilai menjadi kurva frekuensi. Saldo tersebar merata dari 137,75 sampai 1.377.500, sehingga 700.000 berada di sekitar 50,8% wilayahnya.

ANALYZE Mengambil Sampel #

Bagaimana ANALYZE bekerja? Perintah ini tidak membaca seluruh tabel: PostgreSQL mengambil sampel acak sekitar 300 × target statistik baris (default 100 → ±30.000 baris), lalu menghitung statistik dari sampel itu. Tabel latihan kita hanya berisi 10.000 baris — lebih kecil dari anggaran sampel — sehingga seluruhnya terbaca. Opsi VERBOSE memperlihatkan laporannya langsung:

# ANALYZE VERBOSE mencetak laporan pengambilan sampel lewat pesan INFO.
# Redireksi 2>&1 diperlukan karena psql mengirim pesan INFO ke stderr.
psql -X -c "ANALYZE (VERBOSE) pelanggan;" 2>&1

Output yang Dihasilkan:

text
INFO:  analyzing "public.pelanggan"
INFO:  "pelanggan": scanned 94 of 94 pages, containing 10000 live rows and 0 dead rows; 10000 rows in sample, 10000 estimated total rows
ANALYZE

Laporan itu menyimpan tiga fakta penting: seluruh 94 halaman dipindai, 10.000 baris hidup ditemukan tanpa satu pun baris mati, dan ke-10.000 baris itu semuanya masuk sampel. Pada tabel produksi berisi ratusan juta baris, baris kedua laporan akan berbunyi seperti “30000 rows in sample, 182000000 estimated total rows” — itulah makna sampel: statistik selalu merupakan perkiraan, dan ANALYZE selesai dalam hitungan detik berapa pun besar tabelnya. ANALYZE juga menerima daftar kolom (ANALYZE pelanggan (kota, zona)) untuk menyegarkan statistik sebagian kolom saja, opsi SKIP_LOCKED untuk melewati tabel yang sedang dikunci berat, dan tentu saja VERBOSE seperti di atas.

Membaca pg_stats #

Hasil ANALYZE tersimpan di katalog pg_statistic, tetapi format internalnya sulit dibaca — PostgreSQL menyediakan view pg_stats yang ramah manusia. Tiga kolomnya paling menentukan estimasi planner:

Kolom pg_statsBentukDipakai untuk estimasi
most_common_vals + most_common_freqsNilai terumum + frekuensinyaFilter kesetaraan kolom = nilai
histogram_boundsTitik-titik batas rentang nilaiFilter rentang <, BETWEEN
n_distinctJumlah nilai berbeda (positif), atau pecahan baris (negatif)Join, GROUP BY, pilihan index
-- Isi statistik kolom kota dan zona: daftar nilai terumum beserta frekuensinya.
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname IN ('kota', 'zona')
ORDER BY attname;

-- Kolom id dan saldo: semua nilai unik, statistiknya berbentuk histogram.
SELECT attname,
       n_distinct,
       array_length(histogram_bounds, 1) AS jumlah_batas,
       (regexp_split_to_array(btrim(histogram_bounds::text, '{}'), ','))[1:3] AS tiga_batas_pertama
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname IN ('id', 'saldo')
ORDER BY attname;

Output yang Dihasilkan:

text
 attname | n_distinct |                            most_common_vals                            |            most_common_freqs            
---------+------------+------------------------------------------------------------------------+-----------------------------------------
 kota    |          8 | {Jakarta,Bandung,Surabaya,Medan,Denpasar,Makassar,Balikpapan,Jayapura} | {0.4,0.2,0.15,0.08,0.07,0.05,0.03,0.02}
 zona    |          3 | {WIB,WITA,WIT}                                                         | {0.83,0.15,0.02}
(2 rows)

 attname | n_distinct | jumlah_batas |     tiga_batas_pertama     
---------+------------+--------------+----------------------------
 id      |         -1 |          101 | {1,100,200}
 saldo   |         -1 |          101 | {137.75,13775.00,27550.00}
(2 rows)

Baris untuk kolom kota adalah kunci menjelaskan estimasi rows=4000 di bagian sebelumnya: nilai Jakarta tercatat sebagai nilai terumum dengan frekuensi 0,4, dan planner menghitung 0,4 × 10.000 baris. Perhatikan pula dua kebiasaan tampilan pg_stats: array frekuensi ditulis mengikuti urutan nilai terumum ke terjarang, dan n_distinct bertanda negatif berarti pecahan dari jumlah baris — -1 pada kolom id dan saldo berarti setiap nilai unik (satu nilai per baris). Kolom zona menunjukkan kejutan korelasi yang kita bahas belakangan: WIB berfrekuensi 0,83 padahal Jakarta saja 0,40 — WIB menaungi empat kota.

Menargetkan Statistik yang Lebih Dalam #

Histogram default memuat sekitar 100 titik batas. Untuk filter rentang yang sempit pada tabel besar, resolusi itu bisa terlalu kasar: dua nilai berbeda jatuh ke ember yang sama sehingga keduanya dianggap sama-sama umum atau sama-sama jarang. Kedalaman statistik dikendalikan per kolom lewat ALTER TABLE ... SET STATISTICS:

-- Naikkan target statistik kolom saldo dari default 100 menjadi 1000.
ALTER TABLE pelanggan ALTER COLUMN saldo SET STATISTICS 1000;

-- ANALYZE ulang mewujudkan target baru menjadi histogram yang lebih rapat.
ANALYZE pelanggan;

SELECT attname,
       array_length(histogram_bounds, 1) AS jumlah_batas
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname = 'saldo';

Output yang Dihasilkan:

text
ALTER TABLE
ANALYZE
 attname | jumlah_batas 
---------+--------------
 saldo   |         1001
(1 row)

Histogram saldo kini memuat 1.001 titik batas — sepuluh kali lebih rapat dari sebelumnya. Biayanya juga nyata: ANALYZE mengambil sampel 300 × 1000 = 300.000 baris untuk kolom ini, penyimpanan statistik di pg_statistic membengkak, dan planner bekerja sedikit lebih lama untuk setiap rencana. Karena itu target statistik sebaiknya dinaikkan selektif — hanya pada kolom yang benar-benar menderita estimasi kasar, terlihat dari rows di EXPLAIN yang jauh dari EXPLAIN ANALYZE — bukan dinaikkan seragam untuk seluruh server. Batas atasnya 10.000 per kolom; untuk mengubah default seluruh server ada parameter default_statistics_target.

Statistik Ekstensif untuk Kolom Berkorelasi #

Semua statistik sejauh ini bersifat per kolom. Masalahnya: planner menggabungkan filter beberapa kolom dengan mengalikan selectivity masing-masing — asumsi yang hanya benar kalau kolom-kolom itu saling bebas. Data nyata jarang begitu murah hati. Tabel kita menyimpan contoh ekstremnya: zona adalah fungsi dari kota (setiap kota punya satu zona), sehingga filter kota = 'Jayapura' AND zona = 'WIT' tidak menyeleksi dua kali — keduanya menunjuk baris yang sama:

-- Dua filter pada kolom yang berkorelasi: kota selalu menentukan zona.
-- Tanpa statistik ekstensif, planner mengalikan selectivity kedua kolom
-- seolah-olah keduanya saling bebas.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Bandingkan dengan kenyataannya:
SELECT count(nama) AS baris_jayapura_wit
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Statistik ekstensif: catat ketergantungan fungsional antar kolom.
CREATE STATISTICS st_kota_zona (dependencies) ON kota, zona FROM pelanggan;

ANALYZE pelanggan;

-- Estimasi kini memakai korelasi antar kolom, bukan asumsi independen.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Isi statistik ekstensif: ketergantungan tercatat sebagai tingkat kepercayaan.
SELECT statistics_name, dependencies
FROM pg_stats_ext
WHERE statistics_name = 'st_kota_zona';

Output yang Dihasilkan:

text
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate  (cost=102.80..102.81 rows=1 width=8)
   ->  Bitmap Heap Scan on pelanggan  (cost=5.79..102.79 rows=4 width=14)
         Recheck Cond: (kota = 'Jayapura'::text)
         Filter: (zona = 'WIT'::text)
         ->  Bitmap Index Scan on idx_pelanggan_kota  (cost=0.00..5.79 rows=200 width=0)
               Index Cond: (kota = 'Jayapura'::text)
(6 rows)

 baris_jayapura_wit 
--------------------
                200
(1 row)

CREATE STATISTICS
ANALYZE
                                       QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Aggregate  (cost=103.33..103.34 rows=1 width=8)
   ->  Bitmap Heap Scan on pelanggan  (cost=5.83..102.83 rows=200 width=14)
         Recheck Cond: (kota = 'Jayapura'::text)
         Filter: (zona = 'WIT'::text)
         ->  Bitmap Index Scan on idx_pelanggan_kota  (cost=0.00..5.79 rows=200 width=0)
               Index Cond: (kota = 'Jayapura'::text)
(6 rows)

 statistics_name |               dependencies               
----------------+------------------------------------------
 st_kota_zona    | {"3 => 4": 1.000000, "4 => 3": 0.020000}
(1 row)

Sebelum statistik ekstensif, planner mengalikan 0,02 (Jayapura) × 0,02 (WIT) = 0,0004 → rows=4, padahal kenyataannya 200 — perkiraan meleset 50 kali. Setelah CREATE STATISTICS (dependencies) diikuti ANALYZE, estimasi menjadi rows=200 tepat. Baris dependencies pada pg_stats_ext terbaca dengan nomor kolom: "3 => 4": 1.000000 berarti kolom ke-3 (kota) menentukan kolom ke-4 (zona) pada 100% baris, sedangkan arah sebaliknya hanya 0,02 — wajar, karena WIB menaungi empat kota sehingga zona tidak menentukan kota. dependencies adalah satu dari tiga jenis statistik ekstensif; dua lainnya (ndistinct untuk estimasi GROUP BY multi-kolom dan mcv untuk daftar nilai terumum gabungan) dibuat dengan cara yang sama. Ingat satu hal: CREATE STATISTICS hanya membuat wadah — statistiknya baru terbentuk ketika ANALYZE berikutnya berjalan.

autoanalyze dan Gejala Statistik Usang #

Di produksi tidak ada yang mengetik ANALYZE setiap kali data berubah; tugas itu dipegang autoanalyze, bagian dari daemon autovacuum. Daemon ini membangunkan dirinya secara berkala (default tiap 60 detik) dan menganalisis tabel yang jumlah modifikasinya sudah melewati ambang autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × jumlah baris — default 50 + 10%. Mari kita amati keadaan itu dengan menambahkan 2.000 pelanggan Jakarta baru tanpa menyegarkan statistik:

-- 2.000 pelanggan baru, semuanya dari Jakarta (promo besar-besaran).
-- Statistik tabel sengaja tidak diperbarui setelahnya.
INSERT INTO pelanggan (id, nama, kota, zona, saldo)
SELECT 10000 + g,
       'Pelanggan Promo ' || g,
       'Jakarta',
       'WIB',
       ((10000 + g) * 137.75)::numeric(12,2)
FROM generate_series(1, 2000) AS g;

-- Counter statistik di-flush secara asinkron; paksa flush sesi ini
-- agar pg_stat_user_tables menampilkan keadaan mutakhir.
SELECT pg_stat_force_next_flush();

-- Tiga versi "berapa baris tabel ini?" -- ketiganya berbeda peran:
-- statistik planner, counter maintenance, dan kenyataan di tabel.
SELECT reltuples::bigint AS baris_menurut_statistik
FROM pg_class
WHERE relname = 'pelanggan';

SELECT n_live_tup AS baris_counter,
       n_mod_since_analyze AS modifikasi_sejak_analyze,
       round(50 + 0.1 * n_live_tup) AS ambang_autoanalyze,
       last_autoanalyze IS NULL AS belum_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'pelanggan';

SELECT count(*) AS baris_sebenarnya
FROM pelanggan;

Output yang Dihasilkan:

text
INSERT 0 2000
 pg_stat_force_next_flush 
--------------------------
 
(1 row)

 baris_menurut_statistik 
-------------------------
                   10000
(1 row)

 baris_counter | modifikasi_sejak_analyze | ambang_autoanalyze | belum_autoanalyze 
---------------+--------------------------+--------------------+-------------------
         12000 |                     2000 |               1250 | t
(1 row)

 baris_sebenarnya 
------------------
            12000
(1 row)

Tiga angka baris pada output menceritakan tiga lapis kebenaran: statistik planner masih menggambarkan tabel 10.000 baris (reltuples), counter maintenance sudah tahu ada 12.000 baris hidup dengan 2.000 modifikasi sebelum analisis terakhir, dan tabel sungguhan berisi 12.000. Modifikasi (2.000) sudah melampaui ambang autoanalyze (1.250) — di server biasa, daemon akan menganalisis ulang tabel ini pada jendela berikutnya. Di tabel latihan kita kolom belum_autoanalyze bernilai t karena opsi autovacuum_enabled = off sengaja dipasang saat membuat tabel; tanpa itu, output demo semacam ini tidak bisa dijamin sama antar eksekusi. Fungsi pg_stat_force_next_flush() di contoh di atas juga layak dicatat: counter pg_stat_user_tables di-flush secara asinkron, dan fungsi tersebut (tersedia sejak PostgreSQL 14) memaksa flush agar kita melihat angka mutakhir tanpa menunggu.

Gejala statistik usang inilah yang perlu diwaspadai: planner masih memperkirakan filter kota = 'Jakarta' menghasilkan 4.000 baris padahal kenyataannya kini 6.000. Selisih 50% seperti ini sering masih ditoleransi planner, tetapi pada data yang tumbuh pesat selisihnya bisa mencapai ribuan kali — dan di situlah rencana yang dipilih menjadi rencana yang salah, persis seperti demonstrasi statistik-usang.sql pada chapter EXPLAIN ANALYZE . Jika EXPLAIN dan EXPLAIN ANALYZE Anda saling bertengkar soal jumlah baris, statistik adalah tersangka pertama.

Statistik yang terus diperbarui hanyalah separuh pekerjaan rumah tangga database. Separuh lainnya menyangkut baris mati, ruang kosong, dan visibility map — wilayah kerja VACUUM. Keduanya dijalankan daemon yang sama, dan itulah topik chapter berikutnya.

Referensi #

Berkas Sumber

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

analisis-verbose.sh

Unduh berkas
bash
# ANALYZE VERBOSE mencetak laporan pengambilan sampel lewat pesan INFO.
# Redireksi 2>&1 diperlukan karena psql mengirim pesan INFO ke stderr.
psql -X -c "ANALYZE (VERBOSE) pelanggan;" 2>&1

baca-pg-stats.sql

Unduh berkas
sql
-- Isi statistik kolom kota dan zona: daftar nilai terumum beserta frekuensinya.
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname IN ('kota', 'zona')
ORDER BY attname;

-- Kolom id dan saldo: semua nilai unik, statistiknya berbentuk histogram.
SELECT attname,
       n_distinct,
       array_length(histogram_bounds, 1) AS jumlah_batas,
       (regexp_split_to_array(btrim(histogram_bounds::text, '{}'), ','))[1:3] AS tiga_batas_pertama
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname IN ('id', 'saldo')
ORDER BY attname;

buat-tabel.sql

Unduh berkas
sql
-- Satu tabel pelanggan dengan distribusi yang sengaja timpang:
-- kota Jakarta menguasai 40% baris, Jayapura hanya 2%.
-- Kolom zona selalu ditentukan oleh kota (korelasi sempurna) --
-- bahan demo statistik ekstensif di bagian akhir chapter.
-- autovacuum dimatikan untuk tabel ini agar demo deterministik:
-- hanya ANALYZE manual yang boleh memperbarui statistiknya.
CREATE TABLE pelanggan (
    id    bigint PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL,
    zona  text NOT NULL,
    saldo numeric(12,2) NOT NULL
) WITH (autovacuum_enabled = off);

INSERT INTO pelanggan (id, nama, kota, zona, saldo)
SELECT k.g,
       'Pelanggan ' || k.g,
       k.kota,
       CASE k.kota
           WHEN 'Jakarta'    THEN 'WIB'
           WHEN 'Bandung'    THEN 'WIB'
           WHEN 'Surabaya'   THEN 'WIB'
           WHEN 'Medan'      THEN 'WIB'
           WHEN 'Denpasar'   THEN 'WITA'
           WHEN 'Makassar'   THEN 'WITA'
           WHEN 'Balikpapan' THEN 'WITA'
           ELSE 'WIT'
       END,
       (k.g * 137.75)::numeric(12,2)
FROM (
    SELECT g,
           CASE
               WHEN g % 100 < 40 THEN 'Jakarta'
               WHEN g % 100 < 60 THEN 'Bandung'
               WHEN g % 100 < 75 THEN 'Surabaya'
               WHEN g % 100 < 83 THEN 'Medan'
               WHEN g % 100 < 90 THEN 'Denpasar'
               WHEN g % 100 < 95 THEN 'Makassar'
               WHEN g % 100 < 98 THEN 'Balikpapan'
               ELSE 'Jayapura'
           END AS kota
    FROM generate_series(1, 10000) AS g
) AS k;

CREATE INDEX idx_pelanggan_kota ON pelanggan (kota);

-- Bukti: sebelum ANALYZE, view pg_stats belum punya entri untuk tabel ini.
SELECT count(*) AS baris_statistik_tersedia
FROM pg_stats
WHERE tablename = 'pelanggan';

kolom-berkorelasi.sql

Unduh berkas
sql
-- Dua filter pada kolom yang berkorelasi: kota selalu menentukan zona.
-- Tanpa statistik ekstensif, planner mengalikan selectivity kedua kolom
-- seolah-olah keduanya saling bebas.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Bandingkan dengan kenyataannya:
SELECT count(nama) AS baris_jayapura_wit
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Statistik ekstensif: catat ketergantungan fungsional antar kolom.
CREATE STATISTICS st_kota_zona (dependencies) ON kota, zona FROM pelanggan;

ANALYZE pelanggan;

-- Estimasi kini memakai korelasi antar kolom, bukan asumsi independen.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jayapura' AND zona = 'WIT';

-- Isi statistik ekstensif: ketergantungan tercatat sebagai tingkat kepercayaan.
SELECT statistics_name, dependencies
FROM pg_stats_ext
WHERE statistics_name = 'st_kota_zona';

modifikasi-baru.sql

Unduh berkas
sql
-- 2.000 pelanggan baru, semuanya dari Jakarta (promo besar-besaran).
-- Statistik tabel sengaja tidak diperbarui setelahnya.
INSERT INTO pelanggan (id, nama, kota, zona, saldo)
SELECT 10000 + g,
       'Pelanggan Promo ' || g,
       'Jakarta',
       'WIB',
       ((10000 + g) * 137.75)::numeric(12,2)
FROM generate_series(1, 2000) AS g;

-- Counter statistik di-flush secara asinkron; paksa flush sesi ini
-- agar pg_stat_user_tables menampilkan keadaan mutakhir.
SELECT pg_stat_force_next_flush();

-- Tiga versi "berapa baris tabel ini?" -- ketiganya berbeda peran:
-- statistik planner, counter maintenance, dan kenyataan di tabel.
SELECT reltuples::bigint AS baris_menurut_statistik
FROM pg_class
WHERE relname = 'pelanggan';

SELECT n_live_tup AS baris_counter,
       n_mod_since_analyze AS modifikasi_sejak_analyze,
       round(50 + 0.1 * n_live_tup) AS ambang_autoanalyze,
       last_autoanalyze IS NULL AS belum_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'pelanggan';

SELECT count(*) AS baris_sebenarnya
FROM pelanggan;

statistik-1000.sql

Unduh berkas
sql
-- Naikkan target statistik kolom saldo dari default 100 menjadi 1000.
ALTER TABLE pelanggan ALTER COLUMN saldo SET STATISTICS 1000;

-- ANALYZE ulang mewujudkan target baru menjadi histogram yang lebih rapat.
ANALYZE pelanggan;

SELECT attname,
       array_length(histogram_bounds, 1) AS jumlah_batas
FROM pg_stats
WHERE tablename = 'pelanggan' AND attname = 'saldo';

tanpa-statistik.sql

Unduh berkas
sql
-- Sebelum ANALYZE, planner tidak tahu apa pun tentang distribusi kolom kota.
-- Estimasi berikut ditangkap pada keadaan tanpa statistik.
-- Angka biaya dapat sedikit berbeda di mesin Anda.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jakarta';

-- Bandingkan dengan kenyataannya:
SELECT count(nama) AS baris_jakarta
FROM pelanggan
WHERE kota = 'Jakarta';

-- Bentuk statistik dengan ANALYZE, lalu lihat estimasinya kembali.
ANALYZE pelanggan;

EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE kota = 'Jakarta';

-- Filter rentang pada kolom numerik memakai sumber estimasi lain: histogram.
EXPLAIN
SELECT count(nama)
FROM pelanggan
WHERE saldo < 700000;