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:

sql siapkan.sql
-- 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:

text
CREATE TABLE
CREATE TABLE
INSERT 0 4

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

text
INSERT 0 8

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

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

  1. 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.
  2. 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.
  3. 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 dari doy % 7 dan doy % 11 berulang dengan periode berbeda sehingga polanya tidak monoton. Seluruhnya dibulatkan round(.../50)*50 — harga pasar memang bergerak dalam kelipatan lima puluhan.
  4. 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 angka INSERT 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:

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

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

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

Berkas Sumber

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

agregasi-bulanan.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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');