Query SQL untuk Menghitung Safety Stock dan Titik Pesan Ulang
TL;DR
Safety stock adalah cadangan buat nutupin permintaan yang naik turun selama barang belum datang, dan rumusnya nilai Z dikali simpangan baku permintaan harian dikali akar lead time. Titik pesan ulang adalah rata-rata permintaan harian dikali lead time, ditambah safety stock. Di SQL, kamu hitung rata-rata dan simpangan bakunya pakai AVG dan STDDEV_SAMP dari tabel penjualan harian, gabungin sama lead time supplier, lalu bandingin hasilnya dengan stok sekarang buat dapat daftar pesan.
Titik pesan ulang adalah jumlah stok yang jadi tanda kamu harus pesan lagi. Rumusnya rata-rata permintaan harian dikali lead time, ditambah safety stock.
Safety stock sendiri cadangan buat nutupin permintaan yang naik turun selama barang belum datang. Rumusnya nilai Z dikali simpangan baku permintaan harian dikali akar lead time.
Dua angka itu bisa kamu hitung langsung di SQL dari data penjualan harian, tanpa keluar ke Excel. Di tutorial ini kita bikin sampai keluar daftar barang yang harus dipesan hari ini.
Apa itu safety stock dan titik pesan ulang?
Safety stock adalah stok cadangan di luar kebutuhan rata-rata, dipegang buat jaga-jaga kalau permintaan tiba-tiba naik atau kiriman supplier telat. Titik pesan ulang adalah ambang batas stok yang begitu tersentuh, kamu langsung bikin pesanan baru. Dua-duanya dihitung per SKU, bukan per toko.
Kenapa nggak cukup pesan waktu stok mau habis. Karena barang butuh waktu datang.
Kalau lead time supplier 9 hari dan kamu jual rata-rata 12 unit per hari, kamu perlu 108 unit cuma buat nutup masa tunggu. Belum termasuk hari-hari yang lakunya di atas rata-rata.
Safety stock yang nutup bagian belum termasuk itu.
Rumus yang dipakai
safety_stock = Z x simpangan_baku_harian x akar(lead_time)
titik_pesan_ulang = (rata_rata_harian x lead_time) + safety_stock
Nilai Z nentuin seberapa sering kamu siap kehabisan:
| Target layanan | Nilai Z | Artinya |
|---|---|---|
| 90% | 1,28 | Kehabisan sekitar 1 dari 10 siklus pesan |
| 95% | 1,65 | Kehabisan sekitar 1 dari 20 siklus pesan |
| 97,5% | 1,96 | Kehabisan sekitar 1 dari 40 siklus pesan |
| 99% | 2,33 | Kehabisan sekitar 1 dari 100 siklus pesan |
Satu hal yang sering ditanya: kenapa akar lead time, bukan lead time biasa. Karena yang dijumlahkan itu ragam, bukan simpangan. Ketidakpastian selama 9 hari bukan 9 kali ketidakpastian sehari, tapi 3 kali.
Kalau kamu kali langsung, cadangannya jadi tiga kali lipat dari yang perlu dan modal kamu ngendap di gudang.
Data apa yang perlu disiapin?
Tiga tabel, dan strukturnya sederhana.
penjualan_harian(tanggal, sku, qty)
supplier_sku(sku, supplier, lead_time_hari, sd_lead_time)
stok_sekarang(sku, qty_tersedia)
Ada satu jebakan di sini yang bikin banyak perhitungan meleset. Kalau tabel penjualan kamu cuma nyimpen hari yang ada transaksinya, hari dengan penjualan nol bakal hilang.
Akibatnya rata-rata kamu ketinggian dan simpangannya kekecilan. Buat barang yang lakunya jarang, efeknya besar.
Cek dulu jumlah barisnya:
SELECT sku, COUNT(*) AS jml_hari
FROM penjualan_harian
WHERE tanggal >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY sku
ORDER BY jml_hari ASC
LIMIT 20;
Kalau ada SKU yang jumlah harinya jauh di bawah 90, itu tanda hari nol nggak tercatat. Isi dulu barisnya dengan qty 0 sebelum lanjut. Cara ngecek lubang semacam ini lebih lengkap ada di panduan cek kualitas data pakai SQL.
Langkah 1: hitung rata-rata dan simpangan permintaan harian
Ambil 90 hari terakhir, kelompokkan per SKU.
SELECT
sku,
AVG(qty) AS rata_harian,
STDDEV_SAMP(qty) AS simpangan_harian,
COUNT(*) AS jml_hari
FROM penjualan_harian
WHERE tanggal >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY sku
HAVING COUNT(*) >= 60;
Dua hal yang perlu diperhatiin.
Pertama, pakai STDDEV_SAMP, bukan STDDEV_POP. Data kamu sampel dari perilaku pembeli, bukan populasi lengkap. Beda dua fungsi ini dijelasin di dokumentasi fungsi agregat PostgreSQL.
Kedua, HAVING COUNT(*) >= 60 buang SKU yang datanya terlalu tipis. Barang yang baru masuk 3 minggu lalu nggak layak dihitung pakai rumus ini.
Langkah 2: hitung safety stock dan titik pesan ulang
Gabungin hasil tadi dengan lead time supplier, lalu terapkan rumusnya. Aku pakai Z 1,65 buat target layanan 95 persen.
WITH stat AS (
SELECT sku,
AVG(qty) AS rata_harian,
STDDEV_SAMP(qty) AS sd_harian
FROM penjualan_harian
WHERE tanggal >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY sku
HAVING COUNT(*) >= 60
)
SELECT
s.sku,
ROUND(s.rata_harian, 2) AS rata_harian,
sup.lead_time_hari,
CEIL(1.65 * s.sd_harian * SQRT(sup.lead_time_hari)) AS safety_stock,
CEIL(s.rata_harian * sup.lead_time_hari
+ 1.65 * s.sd_harian * SQRT(sup.lead_time_hari)) AS titik_pesan_ulang
FROM stat s
JOIN supplier_sku sup ON sup.sku = s.sku
ORDER BY titik_pesan_ulang DESC;
Pakai CEIL, bukan ROUND. Kamu nggak bisa pesan 4,3 karung beras, dan pembulatan ke bawah bikin kamu kurang stok tepat di batas.
Hasil yang kamu harapin: satu baris per SKU dengan dua angka siap pakai.
Langkah 3: keluarkan daftar barang yang harus dipesan hari ini
Bandingin titik pesan ulang tadi sama stok sekarang.
WITH stat AS (
SELECT sku, AVG(qty) AS rata_harian, STDDEV_SAMP(qty) AS sd_harian
FROM penjualan_harian
WHERE tanggal >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY sku
HAVING COUNT(*) >= 60
),
rop AS (
SELECT s.sku,
s.rata_harian,
CEIL(1.65 * s.sd_harian * SQRT(sup.lead_time_hari)) AS safety_stock,
CEIL(s.rata_harian * sup.lead_time_hari
+ 1.65 * s.sd_harian * SQRT(sup.lead_time_hari)) AS rop
FROM stat s
JOIN supplier_sku sup ON sup.sku = s.sku
)
SELECT
r.sku,
st.qty_tersedia,
r.rop,
r.safety_stock,
r.rop - st.qty_tersedia AS kekurangan,
ROUND(st.qty_tersedia / NULLIF(r.rata_harian, 0), 1) AS sisa_hari
FROM rop r
JOIN stok_sekarang st ON st.sku = r.sku
WHERE st.qty_tersedia <= r.rop
ORDER BY sisa_hari ASC;
Kolom sisa_hari yang paling kepakai buat orang gudang. Angka 2,4 artinya barang habis dalam dua setengah hari kalau penjualan jalan seperti biasa.
NULLIF di situ mencegah pembagian nol buat SKU yang rata-ratanya nol.
Contoh kasus toko_berkah: 47 SKU, 3 yang bikin masalah
Aku jalanin query ini di data toko_berkah dengan 47 SKU aktif dan penjualan 90 hari terakhir. Lima baris teratas hasilnya:
| SKU | Rata harian | Simpangan | Lead time | Safety stock | Titik pesan |
|---|---|---|---|---|---|
| Beras 5kg | 18,4 | 6,2 | 7 hari | 28 | 157 |
| Minyak 2L | 14,1 | 5,8 | 7 hari | 26 | 125 |
| Gula 1kg | 11,7 | 3,1 | 5 hari | 12 | 71 |
| Sirup 630ml | 4,2 | 9,4 | 14 hari | 59 | 118 |
| Kopi Sachet | 22,5 | 4,9 | 3 hari | 14 | 82 |
Baris Sirup yang paling menarik. Rata-rata hariannya cuma 4,2 unit, paling kecil di daftar. Tapi safety stock-nya 59, paling besar.
Penyebabnya simpangan 9,4 yang lebih dari dua kali rata-ratanya. Sirup laku sedikit di hari biasa lalu meledak di periode tertentu, dan lead time-nya 14 hari.
Ini pola yang nggak keliatan kalau kamu cuma lihat rata-rata penjualan. Barang paling nggak laku bisa jadi barang yang paling butuh cadangan.
Sebelum query ini dipasang, toko itu pakai aturan seragam: pesan ulang waktu stok tinggal 30 unit, buat semua barang. Hasilnya dalam 90 hari, 3 SKU kehabisan total 11 hari, dan 2 SKU numpuk sampai lewat masa simpan.
Setelah pakai titik pesan per SKU, kejadian kehabisan di kuartal berikutnya turun jadi 2 hari. Nilai stok rata-rata di gudang justru turun 8 persen, karena barang yang stabil kayak Gula nggak perlu cadangan sebesar dulu.
Angka kedua itu yang biasanya bikin pemilik toko setuju. Cadangan yang dihitung per barang bukan cuma lebih aman, tapi juga lebih hemat modal.
Kalau lead time-nya juga naik turun
Rumus di atas nganggep lead time tetap. Di lapangan sering nggak.
Kalau kamu punya catatan tanggal pesan dan tanggal terima, hitung simpangan lead time-nya, lalu pakai rumus gabungan:
SELECT
s.sku,
CEIL(1.65 * SQRT(
sup.lead_time_hari * POWER(s.sd_harian, 2)
+ POWER(s.rata_harian, 2) * POWER(sup.sd_lead_time, 2)
)) AS safety_stock_gabungan
FROM stat s
JOIN supplier_sku sup ON sup.sku = s.sku;
Di toko_berkah, supplier sirup punya lead time rata-rata 14 hari dengan simpangan 4 hari. Safety stock-nya naik dari 59 jadi 104 unit setelah pakai rumus gabungan.
Selisih 45 unit itu harga dari supplier yang jadwalnya nggak pasti. Kadang lebih murah nego jadwal ke supplier daripada nambah cadangan.
Kesalahan umum
Lupa hari nol. Ini yang paling sering. Rata-rata jadi ketinggian, cadangan jadi salah buat semua barang yang lakunya jarang.
Pakai satu nilai Z buat semua barang. Barang cepat rusak nggak butuh target layanan 99 persen. Ongkos busuknya lebih mahal dari ongkos kehabisan.
Ngitung pakai data setahun penuh tanpa mikirin musim. Kalau di dalamnya ada periode Lebaran, simpangannya melar dan cadangan kamu kebesaran sepanjang tahun. Hitung terpisah buat periode musiman.
Ngaliin dengan lead time, bukan akarnya. Kesalahan aritmetika yang bikin cadangan berlipat tanpa alasan.
Nggak ngitung stok yang lagi dalam perjalanan. Kalau kamu udah pesan kemarin dan barangnya belum sampai, jangan pesan lagi hari ini. Tambahin kolom qty dalam perjalanan ke perbandingan di langkah 3.
Nganggep hasilnya berlaku selamanya. Jalanin ulang perhitungan tiap bulan. Pola penjualan bergerak.
Pertanyaan yang sering muncul
Nilai Z mana yang harus dipakai?
Tergantung seberapa sering kamu siap kehabisan. Z 1,65 setara target layanan 95 persen dan itu titik awal yang masuk akal buat kebanyakan barang. Buat barang cepat rusak turunin ke 1,28, buat barang andalan naikin ke 2,33.
Berapa hari data penjualan yang dibutuhin?
Minimal 60 hari punya penjualan, 90 hari lebih aman. Buat produk yang baru diluncurkan, jangan pakai rumus ini dulu. Tetapkan cadangan manual dari produk sejenis sampai datanya cukup.
Apakah query ini perlu dijalanin tiap hari?
Daftar barang yang harus dipesan iya, tiap pagi. Tapi angka safety stock dan titik pesan ulangnya cukup dihitung ulang mingguan atau bulanan. Simpan hasilnya di tabel terpisah biar query harian kamu ringan.
Bisa nggak dikerjain di Excel aja?
Bisa, rumusnya sama persis. SQL lebih enak kalau SKU kamu ratusan dan datanya udah ada di database. Buat toko dengan 20 sampai 30 barang, spreadsheet masih nyaman.
Langkah berikutnya
Tiga langkahnya: hitung rata-rata dan simpangan permintaan harian, terapin rumus safety stock dan titik pesan ulang per SKU, lalu bandingin sama stok sekarang buat dapat daftar pesan.
Yang paling nentuin akurasi bukan rumusnya, tapi kelengkapan hari nol di data kamu. Cek itu dulu sebelum percaya angkanya.
Buat versi spreadsheet-nya, ada template stok barang di Excel yang bisa langsung dipakai. Kalau kamu mau nyambungin ini ke peramalan, mulai dari efek Ramadan pada model peramalan, karena periode itu yang paling sering ngerusak angka cadangan.
Salin query di atas, ganti nama tabelnya, jalanin di NgulikSQL.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel terkait
Cara Membuat Pivot Dinamis di SQL (Panduan 2026)
Pivot dinamis di SQL ngubah baris jadi kolom tanpa kamu hardcode nama kolomnya. Ini cara bikinnya pakai CASE WHEN dan versi yang kolomnya ngikut data.
Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)
YTD, QTD, dan MTD ngukur total dari awal tahun, kuartal, atau bulan sampai hari ini. Ini cara ngitungnya di SQL pakai DATE_TRUNC, plus satu query gabungan.
Gap and Island Analysis di SQL
Gap and island analysis di SQL nyari deret data yang berturut-turut (island) dan celah yang bolong (gap). Cocok buat hitung streak login atau cari tanggal transaksi yang hilang.