Deret harga harian adalah jenis data yang paling sering salah dirawat: dicatat hanya saat ada transaksi, disimpan menempel pada penjualan, lalu dianalisis sebagai tren padang bolong-bolong. Pada bagian ini kita merawatnya sebagaimana layaknya deret waktu — dicatat setiap hari tanpa peduli ada panen atau tidak, dilihat pergerakannya hari-ke-hari dengan LAG, diringkas per bulan dengan date_trunc, dan hari yang terlewat ditemukan lalu diisi secara eksplisit. Bonusnya satu pelajaran SQL yang jarang diajarkan: memuat 1.800 baris data deterministik tanpa mengetik 1.800 baris INSERT, memakai generate_series dan aritmetika tanggal.
Menyiapkan Tabel Harga #
Bagian ini mandiri dan hanya membutuhkan dua tabel: komoditas dan harga_pasar. Constraint kuncinya tetap sama — satu harga per komoditas per hari:
-- siapkan.sql — fondasi Bagian 3: tabel komoditas dan harga_pasar.
-- Definisi identik dengan Bagian 1 (tanpa COMMENT); lahan dan panen tidak
-- dipakai di bagian ini.
CREATE TABLE komoditas (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE harga_pasar (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
komoditas_id integer NOT NULL REFERENCES komoditas (id),
tanggal date NOT NULL,
harga_per_kg numeric(10,2) NOT NULL,
CONSTRAINT harga_pasar_positif CHECK (harga_per_kg > 0),
CONSTRAINT satu_harga_per_hari UNIQUE (komoditas_id, tanggal)
);
INSERT INTO komoditas (nama) VALUES
('padi'),
('jagung'),
('kedelai'),
('kacang tanah');Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
INSERT 0 4Mencatat Harga Manual #
Dua hari pertama dicatat sebagaimana petugas pasar sungguhan bekerja: mengamati harga, menulisnya baris demi baris. Satu hari pasar menghasilkan empat baris — satu per komoditas:
-- catat-harga-manual.sql — pencatatan harga dua hari pertama, diketik manual.
-- Satu hari pasar menghasilkan empat baris: satu per komoditas. Kolom
-- komoditas_id mengikuti urutan pemuatan di siapkan.sql.
INSERT INTO harga_pasar (komoditas_id, tanggal, harga_per_kg) VALUES
(1, DATE '2025-01-02', 7000.00),
(2, DATE '2025-01-02', 4700.00),
(3, DATE '2025-01-02', 11800.00),
(4, DATE '2025-01-02', 13650.00),
(1, DATE '2025-01-03', 6950.00),
(2, DATE '2025-01-03', 4750.00),
(3, DATE '2025-01-03', 11750.00),
(4, DATE '2025-01-03', 13700.00);Output yang Dihasilkan:
INSERT 0 8Delapan baris itu membentuk pola deret: hari 2 Januari padi Rp7.000, hari berikutnya Rp6.950. Sekarang bayangkan melanjutkan sampai 31 Maret 2026 — 453 hari lagi dikali empat komoditas. Mengetiknya bukan hanya melelahkan; setiap baris yang diketik manual adalah satu kesempatan salah, dan ON CONFLICT di bawah nanti menunjukkan bahwa ketikan manual yang bentrok justru harus tidak menimpa.
Memuat Deret Set-Based dengan generate_series #
Lima belas bulan harga harian dimuat satu pernyataan. Model harganya deterministik dan jujur tentang asumsinya — harga dasar per komoditas, tren tahunan pelan, penurunan besar saat panen raya (padi turun dalam pada Maret-April), penurunan kedua saat panen musim kedua (Oktober-November), dan fluktuasi harian kecil dari pola yang berulang:
-- harga-harian.sql — memuat deret harga harian 4 Januari 2025 sampai
-- 31 Maret 2026 secara set-based: INSERT ... SELECT dari generate_series.
-- 1.800 baris tidak realistis diketik tangan; satu pernyataan ini
-- menghitungnya dari model harga musiman yang deterministik:
-- harga = harga dasar + tren tahunan
-- - penurunan besar saat panen raya (mis. padi Maret-April)
-- - penurunan kedua saat panen musim kedua (mis. padi Oktober)
-- - fluktuasi harian kecil dari pola harian yang berulang.
-- Empat tanggal sengaja tidak dimuat (petugas tidak mencatat), dan
-- ON CONFLICT DO NOTHING melewati 3 Januari yang sudah dicatat manual.
INSERT INTO harga_pasar (komoditas_id, tanggal, harga_per_kg)
WITH param (nama, dasar, tren, a1, p1, w1, a2, p2, w2, a3, p3, w3) AS (
VALUES
('padi', 7200.0, 0.70, 900.0, 80, 60.0, 700.0, 300, 50.0, 0.0, 0, 30.0),
('jagung', 5000.0, 0.50, 650.0, 55, 55.0, 400.0, 295, 40.0, 0.0, 0, 30.0),
('kedelai', 12000.0, 0.90, 700.0, 175, 50.0, 450.0, 45, 40.0, 0.0, 0, 30.0),
('kacang tanah', 14000.0, 1.00, 600.0, 135, 40.0, 400.0, 250, 40.0, 350.0, 15, 30.0)
),
hari AS (
SELECT d::date AS tanggal
FROM generate_series(DATE '2025-01-03', DATE '2026-03-31', INTERVAL '1 day') AS d
WHERE d::date NOT IN (DATE '2026-02-16', DATE '2026-03-02',
DATE '2026-03-16', DATE '2026-03-23')
)
SELECT k.id,
h.tanggal,
round(
( p.dasar
+ p.tren * (h.tanggal - DATE '2025-01-02')
- p.a1 * exp(- power((extract(doy FROM h.tanggal)::int - p.p1) / p.w1, 2))
- p.a2 * exp(- power((extract(doy FROM h.tanggal)::int - p.p2) / p.w2, 2))
- p.a3 * exp(- power((extract(doy FROM h.tanggal)::int - p.p3) / p.w3, 2))
+ ((extract(doy FROM h.tanggal)::int % 7) - 3) * 15
+ ((extract(doy FROM h.tanggal)::int % 11) - 5) * 10
) / 50) * 50 AS harga_per_kg
FROM komoditas k
JOIN param p ON p.nama = k.nama
CROSS JOIN hari h
ON CONFLICT (komoditas_id, tanggal) DO NOTHING;
-- Ringkasan pemuatan: jumlah baris, rentang tanggal, dan rentang harga.
SELECT k.nama AS komoditas,
count(*) AS baris_harga,
min(h.tanggal) AS sejak,
max(h.tanggal) AS hingga,
min(h.harga_per_kg) AS harga_terendah,
max(h.harga_per_kg) AS harga_tertinggi
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
GROUP BY k.nama
ORDER BY k.nama;Output yang Dihasilkan:
INSERT 0 1792
komoditas | baris_harga | sejak | hingga | harga_terendah | harga_tertinggi
--------------+-------------+------------+------------+----------------+-----------------
jagung | 450 | 2025-01-02 | 2026-03-31 | 4300.00 | 5250.00
kacang tanah | 450 | 2025-01-02 | 2026-03-31 | 13450.00 | 14450.00
kedelai | 450 | 2025-01-02 | 2026-03-31 | 11400.00 | 12400.00
padi | 450 | 2025-01-02 | 2026-03-31 | 6250.00 | 7400.00
(4 rows)Bedah pernyataan ini baris demi baris:
- CTE
param: empat komoditas dengan parameter modelnya — harga dasar (dasar), tren rupiah per hari (tren), dan dua sampai tiga “lembah” musiman dengan amplitudo (a), hari puncak dalam setahun (p, angka hari ke-), dan lebar (w). Kacang tanah punya tiga lembah karena dipanen tiga kali setahun. - CTE
hari:generate_series(DATE, DATE, INTERVAL '1 day')membangkitkan setiap hari dari 3 Januari 2025 sampai 31 Maret 2026 — inilah set returning function dari chapter yang sama namanya di dokumentasi.WHERE ... NOT IN (...)menjatuhkan empat tanggal: 16 Februari serta 2, 16, dan 23 Maret 2026 — hari-hari ketika petugas tidak mencatat. Celah itu bukan kecelakaan; ia bahan ajar di bagian akhir halaman ini. - Kolom harga:
exp(- power((doy - p)/w, 2))adalah kurva bukit terbalik yang mulai turun menjelang hari puncak — padi turun terdalam sekitar hari ke-80 (akhir Maret, persis setelah panen raya).(h.tanggal - DATE '2025-01-02')menghasilkan selisih hari sebagai integer, dikalikan tren. Fluktuasi harian daridoy % 7dandoy % 11berulang dengan periode berbeda sehingga polanya tidak monoton. Seluruhnya dibulatkanround(.../50)*50— harga pasar memang bergerak dalam kelipatan lima puluhan. ON CONFLICT (komoditas_id, tanggal) DO NOTHING: deret dimulai 3 Januari — hari yang sudah dicatat manual. Klausa ini melewati pasangan komoditas-hari yang sudah ada alih-alih gagal, persis varian aman dari ON CONFLICT . Hasilnya terlihat pada angkaINSERT 0 1792: 1.796 baris dihitung, empat di antaranya dilewati karena 3 Januari milik catatan manual.
Ringkasan penutupnya memastikan muatan benar: 450 baris per komoditas, harga padi bergerak Rp6.250-7.400 — tepat di kisaran nyata gabah kering, jagung Rp4.300-5.250, kedelai sebelas-sembilan ribuan, kacang tanah empat belas ribuan.
Perubahan Hari-ke-Hari dengan LAG #
Deret menjadi berarti saat dibaca pergerakannya. LAG mengambil harga hari sebelumnya sebagai pembanding, dan kolom jeda_hari memperlihatkan jarak tanggal antar-baris — selisih lebih dari satu hari berarti ada hari yang terlewat:
-- perubahan-harian.sql — perubahan harga hari-ke-hari dengan LAG.
-- Deret padi Maret 2026; kolom jeda_hari memperlihatkan lompatan tanggal
-- ketika ada hari yang tidak tercatat, dan perubahan_persen mengukur
-- besarannya. NULL pada baris pertama artinya belum ada hari pembanding.
SELECT h.tanggal,
h.harga_per_kg,
lag(h.harga_per_kg) OVER w AS harga_lalu,
h.tanggal - lag(h.tanggal) OVER w AS jeda_hari,
round(100.0 * (h.harga_per_kg - lag(h.harga_per_kg) OVER w)
/ NULLIF(lag(h.harga_per_kg) OVER w, 0), 2) AS perubahan_persen
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE k.nama = 'padi'
AND h.tanggal BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
WINDOW w AS (PARTITION BY h.komoditas_id ORDER BY h.tanggal)
ORDER BY h.tanggal;Output yang Dihasilkan:
tanggal | harga_per_kg | harga_lalu | jeda_hari | perubahan_persen
------------+--------------+------------+-----------+------------------
2026-03-01 | 6700.00 | | |
2026-03-03 | 6750.00 | 6700.00 | 2 | 0.75
2026-03-04 | 6650.00 | 6750.00 | 1 | -1.48
2026-03-05 | 6650.00 | 6650.00 | 1 | 0.00
2026-03-06 | 6700.00 | 6650.00 | 1 | 0.75
2026-03-07 | 6600.00 | 6700.00 | 1 | -1.49
2026-03-08 | 6600.00 | 6600.00 | 1 | 0.00
2026-03-09 | 6650.00 | 6600.00 | 1 | 0.76
2026-03-10 | 6650.00 | 6650.00 | 1 | 0.00
2026-03-11 | 6550.00 | 6650.00 | 1 | -1.50
2026-03-12 | 6600.00 | 6550.00 | 1 | 0.76
2026-03-13 | 6600.00 | 6600.00 | 1 | 0.00
2026-03-14 | 6650.00 | 6600.00 | 1 | 0.76
2026-03-15 | 6650.00 | 6650.00 | 1 | 0.00
2026-03-17 | 6700.00 | 6650.00 | 2 | 0.75
2026-03-18 | 6500.00 | 6700.00 | 1 | -2.99
2026-03-19 | 6550.00 | 6500.00 | 1 | 0.77
2026-03-20 | 6550.00 | 6550.00 | 1 | 0.00
2026-03-21 | 6600.00 | 6550.00 | 1 | 0.76
2026-03-22 | 6600.00 | 6600.00 | 1 | 0.00
2026-03-24 | 6650.00 | 6600.00 | 2 | 0.76
2026-03-25 | 6600.00 | 6650.00 | 1 | -0.75
2026-03-26 | 6600.00 | 6600.00 | 1 | 0.00
2026-03-27 | 6650.00 | 6600.00 | 1 | 0.76
2026-03-28 | 6700.00 | 6650.00 | 1 | 0.75
2026-03-29 | 6600.00 | 6700.00 | 1 | -1.49
2026-03-30 | 6650.00 | 6600.00 | 1 | 0.76
2026-03-31 | 6650.00 | 6650.00 | 1 | 0.00
(28 rows)Beberapa hal terbaca langsung dari output. Harga bergerak kecil — 0,75 sampai 1,5 persen, bahkan sering 0,00 karena pembulatan pasar — dan itulah sifat harga komoditas harian. Baris pertama bernilai NULL pada tiga kolom: LAG tidak punya hari sebelumnya, dan NULLIF menjaga pembagian tetap aman. Yang paling penting: kolom jeda_hari bernilai 2 pada 3, 17, dan 24 Maret — bukti matematis bahwa 2, 16, dan 23 Maret tidak tercatat. Window bernama WINDOW w AS (...) dipakai empat kali dalam satu query tanpa menyalin definisinya, pola dari chapter LAG dan LEAD
.
Ringkasan Bulanan #
Fluktuasi harian membingungkan; tren bulanan menjelaskan. date_trunc('month', ...) memotong setiap tanggal ke tanggal 1 bulannya sehingga GROUP BY mengelompokkan sebulan penuh:
-- agregasi-bulanan.sql — deret harian menjadi ringkasan bulanan.
-- Query pertama: rata-rata bulanan per komoditas untuk kuartal I 2026.
-- Query kedua: riwayat penuh padi dengan terendah/tertinggi tiap bulan.
SELECT k.nama AS komoditas,
date_trunc('month', h.tanggal)::date AS bulan,
round(avg(h.harga_per_kg), 0) AS harga_rata,
min(h.harga_per_kg) AS terendah,
max(h.harga_per_kg) AS tertinggi,
count(*) AS baris
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE h.tanggal >= DATE '2026-01-01'
GROUP BY k.nama, date_trunc('month', h.tanggal)
ORDER BY k.nama, bulan;
SELECT date_trunc('month', h.tanggal)::date AS bulan,
round(avg(h.harga_per_kg), 0) AS harga_rata,
min(h.harga_per_kg) AS terendah,
max(h.harga_per_kg) AS tertinggi,
max(h.harga_per_kg) - min(h.harga_per_kg) AS rentang_dalam_bulan
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE k.nama = 'padi'
GROUP BY date_trunc('month', h.tanggal)
ORDER BY bulan;Output yang Dihasilkan:
komoditas | bulan | harga_rata | terendah | tertinggi | baris
--------------+------------+------------+----------+-----------+-------
jagung | 2026-01-01 | 4794 | 4650.00 | 4950.00 | 31
jagung | 2026-02-01 | 4580 | 4450.00 | 4700.00 | 27
jagung | 2026-03-01 | 4661 | 4550.00 | 4800.00 | 28
kacang tanah | 2026-01-01 | 14055 | 13950.00 | 14150.00 | 31
kacang tanah | 2026-02-01 | 14267 | 14100.00 | 14400.00 | 27
kacang tanah | 2026-03-01 | 14352 | 14250.00 | 14450.00 | 28
kedelai | 2026-01-01 | 12079 | 11950.00 | 12200.00 | 31
kedelai | 2026-02-01 | 11930 | 11850.00 | 12050.00 | 27
kedelai | 2026-03-01 | 12123 | 12000.00 | 12250.00 | 28
padi | 2026-01-01 | 7171 | 7000.00 | 7300.00 | 31
padi | 2026-02-01 | 6841 | 6650.00 | 7050.00 | 27
padi | 2026-03-01 | 6629 | 6500.00 | 6750.00 | 28
(12 rows)
bulan | harga_rata | terendah | tertinggi | rentang_dalam_bulan
------------+------------+----------+-----------+---------------------
2025-01-01 | 6908 | 6750.00 | 7050.00 | 300.00
2025-02-01 | 6586 | 6400.00 | 6800.00 | 400.00
2025-03-01 | 6381 | 6250.00 | 6500.00 | 250.00
2025-04-01 | 6533 | 6350.00 | 6750.00 | 400.00
2025-05-01 | 6908 | 6650.00 | 7150.00 | 500.00
2025-06-01 | 7195 | 7000.00 | 7350.00 | 350.00
2025-07-01 | 7295 | 7200.00 | 7400.00 | 200.00
2025-08-01 | 7263 | 7150.00 | 7350.00 | 200.00
2025-09-01 | 7027 | 6850.00 | 7200.00 | 350.00
2025-10-01 | 6752 | 6650.00 | 6900.00 | 250.00
2025-11-01 | 6833 | 6650.00 | 7000.00 | 350.00
2025-12-01 | 7181 | 7000.00 | 7400.00 | 400.00
2026-01-01 | 7171 | 7000.00 | 7300.00 | 300.00
2026-02-01 | 6841 | 6650.00 | 7050.00 | 400.00
2026-03-01 | 6629 | 6500.00 | 6750.00 | 250.00
(15 rows)Riwayat padi lima belas bulan itu adalah cerita musim dalam angka. Maret 2025 termurah (rata-rata Rp6.381) — bulan panen raya — lalu harga memanjat ke puncak Juli (Rp7.295), melandai ke lembah kedua Oktober (Rp6.752) saat panen musim kedua, dan memanjat lagi menutup tahun. Maret 2026 mengulangi pola yang sama di level lebih tinggi (Rp6.629). Perhatikan juga kolom baris: Februari 2026 hanya 27 dan Maret 28 — deret yang tampak rapat ternyata berlubang, dan lubang itu harus ditangani secara sadar.
Menemukan dan Mengisi Hari Kosong #
Query berikut memakai pola generate_series + LEFT JOIN untuk mengaudit kelengkapan deret: bangkitkan seluruh 31 hari Maret 2026, pertahankan yang tidak punya pasangan harga:
-- isi-hari-kosong.sql — menemukan dan mengisi hari tanpa catatan harga.
-- Query pertama: generate_series membangkitkan seluruh 31 hari Maret 2026,
-- LEFT JOIN mempertahankan hari tanpa baris harga padi.
-- Query kedua: mengisi hari kosong dengan harga terakhir yang diketahui
-- (subquery skalar "harga <= tanggal ini, ambil yang terbaru") — pola
-- yang akan kembali sebagai JOIN LATERAL di Bagian 4.
SELECT d.tanggal::date AS tanggal,
h.harga_per_kg,
CASE WHEN h.harga_per_kg IS NULL
THEN 'TIDAK ADA CATATAN'
ELSE 'tercatat' END AS status
FROM generate_series(DATE '2026-03-01', DATE '2026-03-31', INTERVAL '1 day') AS d(tanggal)
LEFT JOIN harga_pasar h
ON h.tanggal = d.tanggal::date
AND h.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
ORDER BY d.tanggal;
SELECT d.tanggal::date AS tanggal,
h.harga_per_kg AS harga_terekam,
COALESCE(
h.harga_per_kg,
(SELECT h2.harga_per_kg
FROM harga_pasar h2
WHERE h2.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
AND h2.tanggal <= d.tanggal::date
ORDER BY h2.tanggal DESC
LIMIT 1)
) AS harga_terisi
FROM generate_series(DATE '2026-03-14', DATE '2026-03-18', INTERVAL '1 day') AS d(tanggal)
LEFT JOIN harga_pasar h
ON h.tanggal = d.tanggal::date
AND h.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
ORDER BY d.tanggal;Output yang Dihasilkan:
tanggal | harga_per_kg | status
------------+--------------+-------------------
2026-03-01 | 6700.00 | tercatat
2026-03-02 | | TIDAK ADA CATATAN
2026-03-03 | 6750.00 | tercatat
2026-03-04 | 6650.00 | tercatat
2026-03-05 | 6650.00 | tercatat
2026-03-06 | 6700.00 | tercatat
2026-03-07 | 6600.00 | tercatat
2026-03-08 | 6600.00 | tercatat
2026-03-09 | 6650.00 | tercatat
2026-03-10 | 6650.00 | tercatat
2026-03-11 | 6550.00 | tercatat
2026-03-12 | 6600.00 | tercatat
2026-03-13 | 6600.00 | tercatat
2026-03-14 | 6650.00 | tercatat
2026-03-15 | 6650.00 | tercatat
2026-03-16 | | TIDAK ADA CATATAN
2026-03-17 | 6700.00 | tercatat
2026-03-18 | 6500.00 | tercatat
2026-03-19 | 6550.00 | tercatat
2026-03-20 | 6550.00 | tercatat
2026-03-21 | 6600.00 | tercatat
2026-03-22 | 6600.00 | tercatat
2026-03-23 | | TIDAK ADA CATATAN
2026-03-24 | 6650.00 | tercatat
2026-03-25 | 6600.00 | tercatat
2026-03-26 | 6600.00 | tercatat
2026-03-27 | 6650.00 | tercatat
2026-03-28 | 6700.00 | tercatat
2026-03-29 | 6600.00 | tercatat
2026-03-30 | 6650.00 | tercatat
2026-03-31 | 6650.00 | tercatat
(31 rows)
tanggal | harga_terekam | harga_terisi
------------+---------------+--------------
2026-03-14 | 6650.00 | 6650.00
2026-03-15 | 6650.00 | 6650.00
2026-03-16 | | 6650.00
2026-03-17 | 6700.00 | 6700.00
2026-03-18 | 6500.00 | 6500.00
(5 rows)Query pertama menemukan tiga hari kosong: 2, 16, dan 23 Maret. Query kedua menunjukkan cara mengisinya: COALESCE memakai harga terekam bila ada, dan bila tidak ada, subquery skalar mengambil harga terbaru pada atau sebelum tanggal tersebut — 15 Maret Rp6.650 menjadi harga 16 Maret. Perhatikan bahwa hari 17 Maret tetap memakai harga aslinya Rp6.700: pengisian maju (forward fill) membawa harga lama ke depan, bukan merata-ratakan dengan masa depan.
Deret harga kini lengkap, terbaca pergerakannya, dan lubangnya terkelola. Tersisa pertanyaan yang menjadi tujuan seluruh proyek: apa yang terjadi ketika deret harga ini bertemu buku panen Bagian 2? Bagian Analisis Produksi dan Harga menjawabnya dengan as-of join, rata-rata bergerak, dan korelasi produksi terhadap harga.
Referensi #
- INSERT dan ON CONFLICT
- Set Returning Functions: generate_series
- Window Functions
- Date/Time Functions: date_trunc
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
agregasi-bulanan.sql
Unduh berkas-- agregasi-bulanan.sql — deret harian menjadi ringkasan bulanan.
-- Query pertama: rata-rata bulanan per komoditas untuk kuartal I 2026.
-- Query kedua: riwayat penuh padi dengan terendah/tertinggi tiap bulan.
SELECT k.nama AS komoditas,
date_trunc('month', h.tanggal)::date AS bulan,
round(avg(h.harga_per_kg), 0) AS harga_rata,
min(h.harga_per_kg) AS terendah,
max(h.harga_per_kg) AS tertinggi,
count(*) AS baris
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE h.tanggal >= DATE '2026-01-01'
GROUP BY k.nama, date_trunc('month', h.tanggal)
ORDER BY k.nama, bulan;
SELECT date_trunc('month', h.tanggal)::date AS bulan,
round(avg(h.harga_per_kg), 0) AS harga_rata,
min(h.harga_per_kg) AS terendah,
max(h.harga_per_kg) AS tertinggi,
max(h.harga_per_kg) - min(h.harga_per_kg) AS rentang_dalam_bulan
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE k.nama = 'padi'
GROUP BY date_trunc('month', h.tanggal)
ORDER BY bulan;
catat-harga-manual.sql
Unduh berkas-- catat-harga-manual.sql — pencatatan harga dua hari pertama, diketik manual.
-- Satu hari pasar menghasilkan empat baris: satu per komoditas. Kolom
-- komoditas_id mengikuti urutan pemuatan di siapkan.sql.
INSERT INTO harga_pasar (komoditas_id, tanggal, harga_per_kg) VALUES
(1, DATE '2025-01-02', 7000.00),
(2, DATE '2025-01-02', 4700.00),
(3, DATE '2025-01-02', 11800.00),
(4, DATE '2025-01-02', 13650.00),
(1, DATE '2025-01-03', 6950.00),
(2, DATE '2025-01-03', 4750.00),
(3, DATE '2025-01-03', 11750.00),
(4, DATE '2025-01-03', 13700.00);
harga-harian.sql
Unduh berkas-- harga-harian.sql — memuat deret harga harian 4 Januari 2025 sampai
-- 31 Maret 2026 secara set-based: INSERT ... SELECT dari generate_series.
-- 1.800 baris tidak realistis diketik tangan; satu pernyataan ini
-- menghitungnya dari model harga musiman yang deterministik:
-- harga = harga dasar + tren tahunan
-- - penurunan besar saat panen raya (mis. padi Maret-April)
-- - penurunan kedua saat panen musim kedua (mis. padi Oktober)
-- - fluktuasi harian kecil dari pola harian yang berulang.
-- Empat tanggal sengaja tidak dimuat (petugas tidak mencatat), dan
-- ON CONFLICT DO NOTHING melewati 3 Januari yang sudah dicatat manual.
INSERT INTO harga_pasar (komoditas_id, tanggal, harga_per_kg)
WITH param (nama, dasar, tren, a1, p1, w1, a2, p2, w2, a3, p3, w3) AS (
VALUES
('padi', 7200.0, 0.70, 900.0, 80, 60.0, 700.0, 300, 50.0, 0.0, 0, 30.0),
('jagung', 5000.0, 0.50, 650.0, 55, 55.0, 400.0, 295, 40.0, 0.0, 0, 30.0),
('kedelai', 12000.0, 0.90, 700.0, 175, 50.0, 450.0, 45, 40.0, 0.0, 0, 30.0),
('kacang tanah', 14000.0, 1.00, 600.0, 135, 40.0, 400.0, 250, 40.0, 350.0, 15, 30.0)
),
hari AS (
SELECT d::date AS tanggal
FROM generate_series(DATE '2025-01-03', DATE '2026-03-31', INTERVAL '1 day') AS d
WHERE d::date NOT IN (DATE '2026-02-16', DATE '2026-03-02',
DATE '2026-03-16', DATE '2026-03-23')
)
SELECT k.id,
h.tanggal,
round(
( p.dasar
+ p.tren * (h.tanggal - DATE '2025-01-02')
- p.a1 * exp(- power((extract(doy FROM h.tanggal)::int - p.p1) / p.w1, 2))
- p.a2 * exp(- power((extract(doy FROM h.tanggal)::int - p.p2) / p.w2, 2))
- p.a3 * exp(- power((extract(doy FROM h.tanggal)::int - p.p3) / p.w3, 2))
+ ((extract(doy FROM h.tanggal)::int % 7) - 3) * 15
+ ((extract(doy FROM h.tanggal)::int % 11) - 5) * 10
) / 50) * 50 AS harga_per_kg
FROM komoditas k
JOIN param p ON p.nama = k.nama
CROSS JOIN hari h
ON CONFLICT (komoditas_id, tanggal) DO NOTHING;
-- Ringkasan pemuatan: jumlah baris, rentang tanggal, dan rentang harga.
SELECT k.nama AS komoditas,
count(*) AS baris_harga,
min(h.tanggal) AS sejak,
max(h.tanggal) AS hingga,
min(h.harga_per_kg) AS harga_terendah,
max(h.harga_per_kg) AS harga_tertinggi
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
GROUP BY k.nama
ORDER BY k.nama;
isi-hari-kosong.sql
Unduh berkas-- isi-hari-kosong.sql — menemukan dan mengisi hari tanpa catatan harga.
-- Query pertama: generate_series membangkitkan seluruh 31 hari Maret 2026,
-- LEFT JOIN mempertahankan hari tanpa baris harga padi.
-- Query kedua: mengisi hari kosong dengan harga terakhir yang diketahui
-- (subquery skalar "harga <= tanggal ini, ambil yang terbaru") — pola
-- yang akan kembali sebagai JOIN LATERAL di Bagian 4.
SELECT d.tanggal::date AS tanggal,
h.harga_per_kg,
CASE WHEN h.harga_per_kg IS NULL
THEN 'TIDAK ADA CATATAN'
ELSE 'tercatat' END AS status
FROM generate_series(DATE '2026-03-01', DATE '2026-03-31', INTERVAL '1 day') AS d(tanggal)
LEFT JOIN harga_pasar h
ON h.tanggal = d.tanggal::date
AND h.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
ORDER BY d.tanggal;
SELECT d.tanggal::date AS tanggal,
h.harga_per_kg AS harga_terekam,
COALESCE(
h.harga_per_kg,
(SELECT h2.harga_per_kg
FROM harga_pasar h2
WHERE h2.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
AND h2.tanggal <= d.tanggal::date
ORDER BY h2.tanggal DESC
LIMIT 1)
) AS harga_terisi
FROM generate_series(DATE '2026-03-14', DATE '2026-03-18', INTERVAL '1 day') AS d(tanggal)
LEFT JOIN harga_pasar h
ON h.tanggal = d.tanggal::date
AND h.komoditas_id = (SELECT id FROM komoditas WHERE nama = 'padi')
ORDER BY d.tanggal;
perubahan-harian.sql
Unduh berkas-- perubahan-harian.sql — perubahan harga hari-ke-hari dengan LAG.
-- Deret padi Maret 2026; kolom jeda_hari memperlihatkan lompatan tanggal
-- ketika ada hari yang tidak tercatat, dan perubahan_persen mengukur
-- besarannya. NULL pada baris pertama artinya belum ada hari pembanding.
SELECT h.tanggal,
h.harga_per_kg,
lag(h.harga_per_kg) OVER w AS harga_lalu,
h.tanggal - lag(h.tanggal) OVER w AS jeda_hari,
round(100.0 * (h.harga_per_kg - lag(h.harga_per_kg) OVER w)
/ NULLIF(lag(h.harga_per_kg) OVER w, 0), 2) AS perubahan_persen
FROM harga_pasar h
JOIN komoditas k ON k.id = h.komoditas_id
WHERE k.nama = 'padi'
AND h.tanggal BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
WINDOW w AS (PARTITION BY h.komoditas_id ORDER BY h.tanggal)
ORDER BY h.tanggal;
siapkan.sql
Unduh berkas-- siapkan.sql — fondasi Bagian 3: tabel komoditas dan harga_pasar.
-- Definisi identik dengan Bagian 1 (tanpa COMMENT); lahan dan panen tidak
-- dipakai di bagian ini.
CREATE TABLE komoditas (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE harga_pasar (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
komoditas_id integer NOT NULL REFERENCES komoditas (id),
tanggal date NOT NULL,
harga_per_kg numeric(10,2) NOT NULL,
CONSTRAINT harga_pasar_positif CHECK (harga_per_kg > 0),
CONSTRAINT satu_harga_per_hari UNIQUE (komoditas_id, tanggal)
);
INSERT INTO komoditas (nama) VALUES
('padi'),
('jagung'),
('kedelai'),
('kacang tanah');