MariaDB ve MySQL’de Parametreli Stored Procedure Örnekleri
Veritabanı yönetiminde tekrar eden işlemleri otomatize etmek, hem zaman kazandırır hem de hata riskini azaltır. Stored procedure’ler (saklı yordamlar) bu noktada devreye girer ve özellikle parametreli versiyonları, uygulamalarınızla veritabanınız arasında güçlü bir köprü kurar. Bu yazıda MariaDB ve MySQL üzerinde gerçek dünya senaryolarına dayanan parametreli stored procedure örneklerini inceleyeceğiz.
Stored Procedure Nedir ve Neden Parametreli Kullanmalısınız?
Stored procedure, veritabanı sunucusunda saklanan ve ihtiyaç duyulduğunda çağrılabilen SQL kod bloklarıdır. Parametreli hale getirdiğinizde ise bu kod blokları dinamik bir yapıya kavuşur. Uygulama katmanından veri gönderir, procedure içinde işlersiniz ve sonucu geri alırsınız.
Parametreli stored procedure kullanmanın avantajları şunlardır:
- Kod tekrarını önler: Aynı mantığı defalarca yazmak yerine bir kez yazıp defalarca çağırırsınız
- SQL injection’a karşı koruma: Parametreler doğrudan sorguya eklenmediğinden injection saldırıları engellenir
- Performans artışı: Procedure’ler derlenerek önbelleğe alınır, her seferinde parse edilmez
- Merkezi iş mantığı: Uygulama kodunu değiştirmeden veritabanı katmanında güncelleme yapabilirsiniz
- Yetki yönetimi: Tablolara direkt erişim vermek yerine sadece procedure’e çağırma yetkisi verebilirsiniz
Temel Syntax ve Parametre Tipleri
MariaDB/MySQL’de üç tür parametre vardır:
- IN: Dışarıdan procedure’e değer gönderir, en yaygın kullanılan tiptir
- OUT: Procedure’den dışarıya değer döndürür
- INOUT: Hem giriş hem çıkış olarak kullanılır, içeride değiştirilebilir
Temel bir stored procedure oluşturmadan önce delimiter’ı değiştirmeniz gerekir. Çünkü procedure gövdesi noktalı virgül içerdiğinden MySQL istemcisi karışabilir.
-- Delimiter'ı değiştir
DELIMITER $$
CREATE PROCEDURE procedure_adi(
IN parametre1 INT,
IN parametre2 VARCHAR(100),
OUT sonuc INT
)
BEGIN
-- SQL kodları buraya
SELECT COUNT(*) INTO sonuc
FROM tablo
WHERE id = parametre1;
END$$
DELIMITER ;
Gerçek Dünya Senaryosu 1: Kullanıcı Kayıt İşlemi
E-ticaret uygulamanızda yeni kullanıcı kaydı sırasında birden fazla tabloya veri yazmak gerekebilir. Bunu tek bir procedure ile yönetebilirsiniz.
DELIMITER $$
CREATE PROCEDURE sp_kullanici_kayit(
IN p_ad VARCHAR(50),
IN p_soyad VARCHAR(50),
IN p_email VARCHAR(100),
IN p_sifre_hash VARCHAR(255),
IN p_telefon VARCHAR(15),
OUT p_kullanici_id INT,
OUT p_hata_mesaji VARCHAR(255)
)
BEGIN
DECLARE v_mevcut_email INT DEFAULT 0;
-- Hata yakalama
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET p_hata_mesaji = 'Kayıt sırasında bir hata oluştu';
SET p_kullanici_id = -1;
ROLLBACK;
END;
-- Email kontrolü
SELECT COUNT(*) INTO v_mevcut_email
FROM kullanicilar
WHERE email = p_email;
IF v_mevcut_email > 0 THEN
SET p_hata_mesaji = 'Bu email adresi zaten kayıtlı';
SET p_kullanici_id = 0;
ELSE
START TRANSACTION;
INSERT INTO kullanicilar (ad, soyad, email, sifre_hash, kayit_tarihi)
VALUES (p_ad, p_soyad, p_email, p_sifre_hash, NOW());
SET p_kullanici_id = LAST_INSERT_ID();
-- Kullanıcı profil tablosuna da ekle
INSERT INTO kullanici_profiller (kullanici_id, telefon, durum)
VALUES (p_kullanici_id, p_telefon, 'aktif');
-- Hoşgeldin bildirimi kuyruğuna ekle
INSERT INTO bildirim_kuyrugu (kullanici_id, tip, mesaj, olusturma_tarihi)
VALUES (p_kullanici_id, 'hosgeldin', 'Aramıza hoş geldiniz!', NOW());
SET p_hata_mesaji = '';
COMMIT;
END IF;
END$$
DELIMITER ;
-- Çağırma şekli:
SET @yeni_id = 0;
SET @hata = '';
CALL sp_kullanici_kayit('Ahmet', 'Yılmaz', '[email protected]',
'hash_deger', '05551234567', @yeni_id, @hata);
SELECT @yeni_id, @hata;
Gerçek Dünya Senaryosu 2: Sayfalandırmalı Arama
Uygulama geliştirirken sıkça ihtiyaç duyulan sayfalandırmalı ve filtrelenmiş arama işlemleri için güzel bir örnek:
DELIMITER $$
CREATE PROCEDURE sp_urun_ara(
IN p_arama_terimi VARCHAR(100),
IN p_kategori_id INT,
IN p_min_fiyat DECIMAL(10,2),
IN p_max_fiyat DECIMAL(10,2),
IN p_sayfa INT,
IN p_sayfa_boyutu INT,
OUT p_toplam_kayit INT
)
BEGIN
DECLARE v_offset INT;
-- Offset hesapla
SET v_offset = (p_sayfa - 1) * p_sayfa_boyutu;
-- Toplam kayıt sayısını öğren
SELECT COUNT(*) INTO p_toplam_kayit
FROM urunler u
INNER JOIN kategoriler k ON u.kategori_id = k.id
WHERE u.durum = 'aktif'
AND (p_arama_terimi IS NULL OR u.urun_adi LIKE CONCAT('%', p_arama_terimi, '%'))
AND (p_kategori_id IS NULL OR u.kategori_id = p_kategori_id)
AND (p_min_fiyat IS NULL OR u.fiyat >= p_min_fiyat)
AND (p_max_fiyat IS NULL OR u.fiyat <= p_max_fiyat);
-- Sayfalandırılmış sonucu getir
SELECT
u.id,
u.urun_adi,
u.fiyat,
u.stok_adedi,
k.kategori_adi,
u.guncelleme_tarihi
FROM urunler u
INNER JOIN kategoriler k ON u.kategori_id = k.id
WHERE u.durum = 'aktif'
AND (p_arama_terimi IS NULL OR u.urun_adi LIKE CONCAT('%', p_arama_terimi, '%'))
AND (p_kategori_id IS NULL OR u.kategori_id = p_kategori_id)
AND (p_min_fiyat IS NULL OR u.fiyat >= p_min_fiyat)
AND (p_max_fiyat IS NULL OR u.fiyat <= p_max_fiyat)
ORDER BY u.urun_adi
LIMIT p_sayfa_boyutu OFFSET v_offset;
END$$
DELIMITER ;
-- Kullanım: 2. sayfada, elektronik kategorisinde,
-- 100-500 TL arası "laptop" araması
SET @toplam = 0;
CALL sp_urun_ara('laptop', 3, 100.00, 500.00, 2, 10, @toplam);
SELECT @toplam AS toplam_sonuc;
Gerçek Dünya Senaryosu 3: Stok Güncelleme ve Loglama
Muhasebe veya stok takip sistemlerinde her değişikliğin loglanması kritik öneme sahiptir. INOUT parametresi kullanımını da bu örnekte göreceksiniz:
DELIMITER $$
CREATE PROCEDURE sp_stok_guncelle(
IN p_urun_id INT,
IN p_islem_tipi ENUM('giris', 'cikis', 'duzeltme'),
INOUT p_miktar INT,
IN p_aciklama VARCHAR(500),
IN p_islem_yapan_id INT,
OUT p_sonuc_mesaji VARCHAR(255)
)
BEGIN
DECLARE v_mevcut_stok INT DEFAULT 0;
DECLARE v_yeni_stok INT DEFAULT 0;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET p_sonuc_mesaji = CONCAT('Hata: Stok güncellenemedi, ürün ID: ', p_urun_id);
ROLLBACK;
END;
START TRANSACTION;
-- Mevcut stok miktarını oku (FOR UPDATE ile kilitle)
SELECT stok_adedi INTO v_mevcut_stok
FROM urunler
WHERE id = p_urun_id
FOR UPDATE;
-- Ürün bulunamadıysa
IF v_mevcut_stok IS NULL THEN
SET p_sonuc_mesaji = 'Ürün bulunamadı';
SET p_miktar = -1;
ROLLBACK;
ELSE
-- Yeni stok hesapla
CASE p_islem_tipi
WHEN 'giris' THEN
SET v_yeni_stok = v_mevcut_stok + p_miktar;
WHEN 'cikis' THEN
IF v_mevcut_stok < p_miktar THEN
SET p_sonuc_mesaji = CONCAT('Yetersiz stok. Mevcut: ', v_mevcut_stok);
SET p_miktar = v_mevcut_stok;
ROLLBACK;
LEAVE sp_stok_guncelle;
END IF;
SET v_yeni_stok = v_mevcut_stok - p_miktar;
WHEN 'duzeltme' THEN
SET v_yeni_stok = p_miktar;
SET p_miktar = v_yeni_stok - v_mevcut_stok;
END CASE;
-- Stok güncelle
UPDATE urunler
SET stok_adedi = v_yeni_stok,
son_stok_guncelleme = NOW()
WHERE id = p_urun_id;
-- Log kaydı ekle
INSERT INTO stok_hareketleri (
urun_id, islem_tipi, miktar,
onceki_stok, sonraki_stok,
aciklama, islem_yapan_id, islem_tarihi
)
VALUES (
p_urun_id, p_islem_tipi, p_miktar,
v_mevcut_stok, v_yeni_stok,
p_aciklama, p_islem_yapan_id, NOW()
);
SET p_sonuc_mesaji = CONCAT('Stok güncellendi. Yeni stok: ', v_yeni_stok);
COMMIT;
END IF;
END$$
DELIMITER ;
-- Stok çıkışı örneği
SET @miktar = 5;
SET @sonuc = '';
CALL sp_stok_guncelle(42, 'cikis', @miktar, 'Online sipariş #1234', 7, @sonuc);
SELECT @miktar AS gerceklesen_miktar, @sonuc AS mesaj;
Gerçek Dünya Senaryosu 4: Rapor Üretme Procedure’ü
Yöneticilerin sık istediği satış raporlarını dinamik tarih aralıklarıyla üretmek için:
DELIMITER $$
CREATE PROCEDURE sp_satis_raporu(
IN p_baslangic_tarihi DATE,
IN p_bitis_tarihi DATE,
IN p_kategori_id INT,
IN p_para_birimi VARCHAR(3),
OUT p_toplam_satis DECIMAL(15,2),
OUT p_siparis_sayisi INT
)
BEGIN
DECLARE v_kur_carpani DECIMAL(10,4) DEFAULT 1.0000;
-- Para birimi dönüşümü
IF p_para_birimi = 'USD' THEN
SELECT kur INTO v_kur_carpani
FROM doviz_kurlari
WHERE para_birimi = 'USD'
AND tarih = CURDATE();
ELSEIF p_para_birimi = 'EUR' THEN
SELECT kur INTO v_kur_carpani
FROM doviz_kurlari
WHERE para_birimi = 'EUR'
AND tarih = CURDATE();
END IF;
-- Özet bilgileri OUT parametrelerine yaz
SELECT
COALESCE(SUM(s.toplam_tutar * v_kur_carpani), 0),
COUNT(DISTINCT s.id)
INTO p_toplam_satis, p_siparis_sayisi
FROM siparisler s
INNER JOIN siparis_detaylari sd ON s.id = sd.siparis_id
INNER JOIN urunler u ON sd.urun_id = u.id
WHERE s.siparis_tarihi BETWEEN p_baslangic_tarihi AND p_bitis_tarihi
AND s.durum IN ('tamamlandi', 'kargolandi')
AND (p_kategori_id IS NULL OR u.kategori_id = p_kategori_id);
-- Detaylı raporu result set olarak döndür
SELECT
DATE_FORMAT(s.siparis_tarihi, '%Y-%m-%d') AS tarih,
k.kategori_adi,
u.urun_adi,
SUM(sd.adet) AS toplam_adet,
SUM(sd.birim_fiyat * sd.adet * v_kur_carpani) AS toplam_gelir,
COUNT(DISTINCT s.id) AS siparis_sayisi
FROM siparisler s
INNER JOIN siparis_detaylari sd ON s.id = sd.siparis_id
INNER JOIN urunler u ON sd.urun_id = u.id
INNER JOIN kategoriler k ON u.kategori_id = k.id
WHERE s.siparis_tarihi BETWEEN p_baslangic_tarihi AND p_bitis_tarihi
AND s.durum IN ('tamamlandi', 'kargolandi')
AND (p_kategori_id IS NULL OR u.kategori_id = p_kategori_id)
GROUP BY DATE_FORMAT(s.siparis_tarihi, '%Y-%m-%d'), k.id, u.id
ORDER BY tarih, toplam_gelir DESC;
END$$
DELIMITER ;
-- Tüm kategoriler için Ocak 2024 TL raporu
SET @toplam = 0;
SET @adet = 0;
CALL sp_satis_raporu('2024-01-01', '2024-01-31', NULL, 'TRY', @toplam, @adet);
SELECT @toplam AS toplam_satis_tl, @adet AS siparis_adedi;
Procedure Yönetimi: Listeleme, Değiştirme ve Silme
Procedure’leri yönetmek için sık kullandığım komutlar:
-- Veritabanındaki tüm procedure'leri listele
SHOW PROCEDURE STATUS WHERE Db = 'veritabani_adi';
-- Belirli bir procedure'ün kodunu görüntüle
SHOW CREATE PROCEDURE sp_kullanici_kayit;
-- Procedure'ü sil
DROP PROCEDURE IF EXISTS sp_kullanici_kayit;
-- Procedure'ü güncelle (silip yeniden oluşturmak gerekir)
DROP PROCEDURE IF EXISTS sp_urun_ara;
DELIMITER $$
CREATE PROCEDURE sp_urun_ara(
-- güncellenmiş parametreler ve kod
)
BEGIN
-- yeni kod
END$$
DELIMITER ;
-- Procedure'e yetki ver
GRANT EXECUTE ON PROCEDURE veritabani_adi.sp_kullanici_kayit TO 'uygulama_kullanici'@'%';
-- information_schema üzerinden detaylı bilgi al
SELECT
ROUTINE_NAME,
ROUTINE_TYPE,
CREATED,
LAST_ALTERED,
SECURITY_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'veritabani_adi'
AND ROUTINE_TYPE = 'PROCEDURE'
ORDER BY ROUTINE_NAME;
Hata Yönetimi ve HANDLER Kullanımı
Production ortamında hata yönetimi olmayan stored procedure kullanmak ciddi sorunlara yol açabilir. Kapsamlı bir hata yönetimi örneği:
DELIMITER $$
CREATE PROCEDURE sp_toplu_fiyat_guncelle(
IN p_kategori_id INT,
IN p_artis_orani DECIMAL(5,2),
IN p_min_fiyat_siniri DECIMAL(10,2),
OUT p_guncellenen_adet INT,
OUT p_hata_kodu INT,
OUT p_hata_detay VARCHAR(500)
)
BEGIN
-- Değişken tanımlamaları en başta olmalı
DECLARE v_sqlstate VARCHAR(5);
DECLARE v_mysql_errno INT;
DECLARE v_mesaj TEXT;
DECLARE v_etkilenen_satir INT DEFAULT 0;
-- Genel SQL hatası yakalayıcı
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
v_sqlstate = RETURNED_SQLSTATE,
v_mysql_errno = MYSQL_ERRNO,
v_mesaj = MESSAGE_TEXT;
SET p_hata_kodu = v_mysql_errno;
SET p_hata_detay = CONCAT('SQLSTATE: ', v_sqlstate, ' | Mesaj: ', v_mesaj);
SET p_guncellenen_adet = -1;
ROLLBACK;
END;
-- Parametre doğrulama
IF p_artis_orani <= -100 THEN
SET p_hata_kodu = 1001;
SET p_hata_detay = 'Artış oranı -100 den küçük olamaz';
SET p_guncellenen_adet = 0;
ELSEIF p_min_fiyat_siniri < 0 THEN
SET p_hata_kodu = 1002;
SET p_hata_detay = 'Minimum fiyat sınırı negatif olamaz';
SET p_guncellenen_adet = 0;
ELSE
START TRANSACTION;
-- Fiyat güncellemesini yap
UPDATE urunler
SET fiyat = fiyat * (1 + p_artis_orani / 100),
guncelleme_tarihi = NOW()
WHERE kategori_id = p_kategori_id
AND fiyat >= p_min_fiyat_siniri
AND durum = 'aktif';
SET v_etkilenen_satir = ROW_COUNT();
-- Değişiklik logu
INSERT INTO fiyat_degisim_logu (
kategori_id, artis_orani,
etkilenen_urun_sayisi, islem_tarihi
)
VALUES (
p_kategori_id, p_artis_orani,
v_etkilenen_satir, NOW()
);
SET p_guncellenen_adet = v_etkilenen_satir;
SET p_hata_kodu = 0;
SET p_hata_detay = '';
COMMIT;
END IF;
END$$
DELIMITER ;
-- Çağırım
SET @guncellenen = 0;
SET @hata_kod = 0;
SET @hata_detay = '';
CALL sp_toplu_fiyat_guncelle(5, 10.00, 50.00, @guncellenen, @hata_kod, @hata_detay);
SELECT
@guncellenen AS guncellenen_urun,
@hata_kod AS hata_kodu,
@hata_detay AS hata_detayi;
Procedure’leri PHP ve Python’dan Çağırmak
Gerçek uygulamalarda procedure’leri uygulama katmanından çağırmanız gerekecek. İki yaygın örnek:
# PHP PDO ile stored procedure çağırımı
# Bu örnekte PHP kodu yorum satırı olarak gösterilmiştir
# PHP kodunu test.php dosyasına yazıp çalıştırabilirsiniz:
# <?php
# $pdo = new PDO('mysql:host=localhost;dbname=veritabani', 'kullanici', 'sifre');
# $stmt = $pdo->prepare("CALL sp_kullanici_kayit(?, ?, ?, ?, ?, @kullanici_id, @hata)");
# $stmt->execute(['Ahmet', 'Yılmaz', '[email protected]', password_hash('sifre123', PASSWORD_BCRYPT), '05551234567']);
# $result = $pdo->query("SELECT @kullanici_id, @hata")->fetch();
# echo "Kullanıcı ID: " . $result['@kullanici_id'];
# ?>
# Python mysql-connector ile çağırım örneği
# python3 -c "
# import mysql.connector
# conn = mysql.connector.connect(host='localhost', database='veritabani', user='kullanici', password='sifre')
# cursor = conn.cursor()
# args = ('Ayse', 'Kaya', '[email protected]', 'hash_deger', '05559876543', 0, '')
# result_args = cursor.callproc('sp_kullanici_kayit', args)
# print('Kullanici ID:', result_args[5])
# print('Hata:', result_args[6])
# conn.close()
# "
Performans İpuçları
Production ortamında stored procedure kullanırken dikkat etmeniz gereken noktalar:
- DETERMINISTIC bildirimi: Aynı parametrelerle her zaman aynı sonucu döndüren procedure’lere DETERMINISTIC ekleyin, replikasyon için önemlidir
- SQL SECURITY: Varsayılan DEFINER yerine INVOKER kullanmayı değerlendirin, güvenlik açısından daha temizdir
- Büyük result set’lerden kaçının: Procedure içinden çok büyük veri döndürmek yerine geçici tablolar veya cursor kullanmayı düşünün
- Transaction sınırlarını küçük tutun: Uzun transaction’lar tablo kilitlemelerine yol açar
- Procedure içinde procedure çağırmayı sınırlayın: Derin iç içe çağrılar performansı olumsuz etkiler ve debug’ı zorlaştırır
- EXPLAIN ile test edin: Procedure içindeki SELECT’leri önce tek tek EXPLAIN ile analiz edin
-- Procedure içindeki bir sorguyu analiz etmek için
EXPLAIN
SELECT u.id, u.urun_adi, u.fiyat
FROM urunler u
INNER JOIN kategoriler k ON u.kategori_id = k.id
WHERE u.durum = 'aktif'
AND u.kategori_id = 3
AND u.fiyat BETWEEN 100.00 AND 500.00;
-- Procedure execution planını profile ile incele
SET profiling = 1;
CALL sp_urun_ara('laptop', 3, 100.00, 500.00, 1, 10, @toplam);
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
SET profiling = 0;
Sonuç
Parametreli stored procedure’ler, MariaDB ve MySQL ortamlarında veritabanı yönetimini ciddi ölçüde kolaylaştıran araçlardır. Özellikle IN, OUT ve INOUT parametre tiplerini doğru kombinasyonlarda kullanmak, karmaşık iş mantığını tek bir çağrıyla yönetmenizi sağlar.
Bu yazıda incelediğimiz senaryolar, gerçek dünya uygulamalarında karşılaşacağınız durumların büyük bölümünü kapsıyor: kullanıcı kaydı, sayfalandırmalı arama, stok yönetimi, rapor üretimi ve toplu güncelleme işlemleri. Hata yönetimi kısmını asla atlamayın, production’da yakalanmayan bir exception transaction’ı yarıda bırakarak tutarsız veriye yol açabilir.
Son olarak, stored procedure’lerin her derde deva olmadığını belirtmek gerekir. Çok sık değişen iş mantığını uygulama katmanında tutmak, veritabanı bağımsız mimari açısından daha sağlıklı olabilir. Ama tekrarlayan, performans kritik ve çok adımlı işlemler için stored procedure hala en iyi arkadaşınız olmaya devam ediyor.
