Stored Procedure Nedir ve Nasıl Yazılır?

Veritabanı yönetiminde bir noktada “bu sorguyu her seferinde tekrar yazmak zorunda mıyım?” diye kendinize sordunuz mu? Ya da aynı mantığı hem uygulama kodunda hem de başka bir script’te tekrarladığınızı fark ettiniz mi? İşte stored procedure’ler tam bu sorunu çözmek için var. MariaDB ve MySQL’de stored procedure’ler, veritabanı sunucusu üzerinde saklanan, isim verilen ve tekrar tekrar çağrılabilen SQL kod bloklarıdır. Bugün bu yapıyı sıfırdan öğrenecek, gerçek dünya senaryolarıyla pekiştirecek ve production ortamında nasıl kullanacağınızı göreceğiz.

Stored Procedure Neden Kullanmalısınız?

Önce motivasyonu netleştirelim. Uygulamanızda bir e-ticaret sistemi var diyelim. Sipariş tamamlandığında şunlar olması gerekiyor: stok düşülecek, sipariş kaydı oluşturulacak, müşteri puanı güncellenecek, log tablosuna kayıt atılacak. Bu dört işlemi her uygulama katmanında tekrar yazmak yerine bir stored procedure içine koyarsanız hem tutarlılık sağlarsınız hem de network trafiğini azaltırsınız çünkü tek bir CALL komutu gönderirsiniz, dört ayrı sorgu değil.

Stored procedure kullanmanın başlıca avantajları şunlar:

  • Performans: Procedure ilk çalıştırıldığında derlenir ve execution plan cache’lenir. Sonraki çağrılarda bu plan doğrudan kullanılır.
  • Güvenlik: Kullanıcılara tablo üzerinde doğrudan erişim vermek yerine sadece procedure’ü çalıştırma yetkisi verebilirsiniz.
  • Bakım kolaylığı: İş mantığı tek bir yerde. Değişiklik gerektiğinde sadece procedure’ü güncelliyorsunuz.
  • Network yükü azaltma: Özellikle döngü içeren işlemlerde onlarca sorgu yerine tek bir çağrı yapılır.
  • Kod tekrarını önleme: Aynı mantığı PHP, Python, Java ve başka uygulamalar aynı procedure’ü çağırır.

Dezavantajlarını da bilmek gerekir tabii:

  • Hata ayıklamak uygulama koduna göre daha zor
  • Stored logic veritabanına bağımlılık yaratır, migration’larda dikkat gerekir
  • Çok karmaşık iş mantıkları için uygulama katmanı daha uygun olabilir

Temel Syntax ve İlk Procedure

MariaDB ve MySQL’de stored procedure yazmadan önce DELIMITER kavramını anlamanız gerekiyor. Normalde MySQL istemcisi noktalı virgül görünce komutu çalıştırır. Ama procedure içinde birden fazla statement var ve hepsinin noktalı virgülü var. Bunu çözmek için delimiter’ı geçici olarak değiştiriyoruz.

mysql -u root -p veritabani_adi
-- Delimiter'ı değiştiriyoruz
DELIMITER //

CREATE PROCEDURE ilk_procedure()
BEGIN
    SELECT 'Merhaba, Stored Procedure!' AS mesaj;
    SELECT NOW() AS sunucu_zamani;
END //

DELIMITER ;

Procedure’ü çağırmak için:

CALL ilk_procedure();

Bu basit örnek çalışıyor, güzel. Şimdi parametreler konusuna geçelim çünkü gerçek işler parametrelerle yapılır.

Parametreler: IN, OUT ve INOUT

Stored procedure’lerde üç tür parametre vardır:

  • IN: Prosedüre değer gönderirsiniz, procedure içinde kullanılır ama dışarıda değişmez
  • OUT: Procedure dışına değer döndürmek için kullanılır, başlangıç değeri NULL’dır
  • INOUT: Hem içeri değer gönderir hem de değiştirilmiş haliyle dışarı döner

Gerçek bir örnek görelim. Bir müşteri arama procedure’ü yazalım:

DELIMITER //

CREATE PROCEDURE musteri_ara(
    IN p_sehir VARCHAR(100),
    IN p_aktif TINYINT,
    OUT p_toplam_musteri INT
)
BEGIN
    -- Şehre ve aktiflik durumuna göre müşterileri listele
    SELECT 
        musteri_id,
        ad,
        soyad,
        email,
        kayit_tarihi
    FROM musteriler
    WHERE sehir = p_sehir 
      AND aktif = p_aktif
    ORDER BY kayit_tarihi DESC;
    
    -- Toplam sayıyı OUT parametresine ata
    SELECT COUNT(*) INTO p_toplam_musteri
    FROM musteriler
    WHERE sehir = p_sehir 
      AND aktif = p_aktif;
END //

DELIMITER ;

Bu procedure’ü çağırırken:

-- Önce session variable tanımlıyoruz OUT parametresi için
CALL musteri_ara('Istanbul', 1, @toplam);

-- OUT parametresinin değerini okuyoruz
SELECT @toplam AS istanbul_aktif_musteri_sayisi;

Değişkenler ve Kontrol Akışı

Gerçek dünya senaryolarında sadece SELECT sorgusu yazmakla kalmaz, değişkenler kullanır, koşullara göre farklı işlemler yaparsınız. MariaDB/MySQL’de procedure içinde değişken tanımlamak için DECLARE kullanılır ve bu satırlar BEGIN’den hemen sonra gelmelidir.

DELIMITER //

CREATE PROCEDURE siparis_ozeti(IN p_musteri_id INT)
BEGIN
    -- Değişken tanımlamaları en üstte olmalı
    DECLARE v_toplam_siparis INT DEFAULT 0;
    DECLARE v_toplam_tutar DECIMAL(10,2) DEFAULT 0.00;
    DECLARE v_musteri_adi VARCHAR(200);
    DECLARE v_kategori VARCHAR(50);
    
    -- Müşteri adını al
    SELECT CONCAT(ad, ' ', soyad) INTO v_musteri_adi
    FROM musteriler
    WHERE musteri_id = p_musteri_id;
    
    -- Sipariş istatistiklerini hesapla
    SELECT 
        COUNT(*),
        COALESCE(SUM(toplam_tutar), 0)
    INTO v_toplam_siparis, v_toplam_tutar
    FROM siparisler
    WHERE musteri_id = p_musteri_id
      AND durum != 'iptal';
    
    -- IF/ELSEIF/ELSE ile müşteri kategorisi belirle
    IF v_toplam_tutar >= 10000 THEN
        SET v_kategori = 'VIP Müşteri';
    ELSEIF v_toplam_tutar >= 5000 THEN
        SET v_kategori = 'Premium Müşteri';
    ELSEIF v_toplam_tutar >= 1000 THEN
        SET v_kategori = 'Standart Müşteri';
    ELSE
        SET v_kategori = 'Yeni Müşteri';
    END IF;
    
    -- Sonuçları döndür
    SELECT 
        v_musteri_adi AS musteri,
        v_toplam_siparis AS siparis_sayisi,
        v_toplam_tutar AS toplam_harcama,
        v_kategori AS musteri_kategorisi;
END //

DELIMITER ;

Döngüler: WHILE, REPEAT ve LOOP

Bazen toplu işlemler yapmanız gerekir. Örneğin belirli bir tarih aralığında günlük rapor oluşturmak ya da toplu veri güncellemesi yapmak. Bu durumlarda döngüler devreye girer.

Gerçek bir senaryo: Her ay sonu çalışan prim hesaplama sistemi. Satış ekibindeki her çalışan için hedef tutturup tutturmadığını kontrol edip prim tablosuna kayıt atalım.

DELIMITER //

CREATE PROCEDURE aylik_prim_hesapla(IN p_yil INT, IN p_ay INT)
BEGIN
    DECLARE v_bitti INT DEFAULT 0;
    DECLARE v_personel_id INT;
    DECLARE v_gerceklesen DECIMAL(10,2);
    DECLARE v_hedef DECIMAL(10,2);
    DECLARE v_prim DECIMAL(10,2);
    DECLARE v_prim_orani DECIMAL(5,2);
    
    -- Cursor tanımı
    DECLARE personel_cursor CURSOR FOR
        SELECT p.personel_id, p.aylik_hedef
        FROM personeller p
        WHERE p.departman = 'Satis'
          AND p.aktif = 1;
    
    -- Cursor bittiğinde v_bitti = 1 yap
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_bitti = 1;
    
    -- Önceki ay kaydı varsa temizle (idempotent çalışma için)
    DELETE FROM prim_kayitlari 
    WHERE yil = p_yil AND ay = p_ay;
    
    OPEN personel_cursor;
    
    prim_dongusu: LOOP
        FETCH personel_cursor INTO v_personel_id, v_hedef;
        
        IF v_bitti = 1 THEN
            LEAVE prim_dongusu;
        END IF;
        
        -- O personelin o ayki satışlarını topla
        SELECT COALESCE(SUM(satis_tutari), 0) INTO v_gerceklesen
        FROM satislar
        WHERE personel_id = v_personel_id
          AND YEAR(satis_tarihi) = p_yil
          AND MONTH(satis_tarihi) = p_ay;
        
        -- Prim oranını belirle
        IF v_gerceklesen >= v_hedef * 1.2 THEN
            SET v_prim_orani = 0.15; -- Hedefi %20 aştı, %15 prim
        ELSEIF v_gerceklesen >= v_hedef THEN
            SET v_prim_orani = 0.10; -- Hedefi tutturdu, %10 prim
        ELSEIF v_gerceklesen >= v_hedef * 0.8 THEN
            SET v_prim_orani = 0.05; -- %80 ulaştı, %5 prim
        ELSE
            SET v_prim_orani = 0.00; -- Prim yok
        END IF;
        
        SET v_prim = v_gerceklesen * v_prim_orani;
        
        -- Prim kaydını ekle
        INSERT INTO prim_kayitlari(personel_id, yil, ay, satis_tutari, prim_tutari, hesaplama_tarihi)
        VALUES(v_personel_id, p_yil, p_ay, v_gerceklesen, v_prim, NOW());
        
    END LOOP;
    
    CLOSE personel_cursor;
    
    -- Özet bilgi döndür
    SELECT 
        COUNT(*) AS hesaplanan_personel,
        SUM(prim_tutari) AS toplam_prim,
        AVG(prim_tutari) AS ortalama_prim
    FROM prim_kayitlari
    WHERE yil = p_yil AND ay = p_ay;
    
END //

DELIMITER ;

Bu procedure’ü her ay sonunda bir cron job ile çağırabilirsiniz:

# /etc/cron.d/aylik_prim dosyasına ekleyin
# Her ayın son günü saat 23:30'da çalışır
30 23 28-31 * * root [ "$(date +%d)" = "$(cal | awk '/^[ 0-9]/{print $NF}' | tail -1)" ] && mysql -u prim_user -pSIFRE veritabani -e "CALL aylik_prim_hesapla(YEAR(NOW()), MONTH(NOW()));"

Hata Yönetimi: DECLARE HANDLER

Production ortamında hata yönetimi olmayan stored procedure yazmak felakete davetiye çıkarmaktır. Özellikle transaction içeren işlemlerde bir şey ters giderse rollback yapabilmeniz gerekir.

DELIMITER //

CREATE PROCEDURE stok_guncelle_ve_siparis_olustur(
    IN p_urun_id INT,
    IN p_musteri_id INT,
    IN p_miktar INT,
    IN p_birim_fiyat DECIMAL(10,2),
    OUT p_siparis_id INT,
    OUT p_hata_mesaji VARCHAR(500)
)
BEGIN
    DECLARE v_mevcut_stok INT;
    DECLARE v_hata_kodu INT DEFAULT 0;
    
    -- Hata handler tanımı
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        -- Hata bilgilerini al
        GET DIAGNOSTICS CONDITION 1
            v_hata_kodu = MYSQL_ERRNO;
        
        ROLLBACK;
        SET p_siparis_id = -1;
        SET p_hata_mesaji = CONCAT('Hata kodu: ', v_hata_kodu, ' - İşlem geri alındı');
    END;
    
    -- Transaction başlat
    START TRANSACTION;
    
    -- Stok kontrolü (FOR UPDATE ile satırı kilitle)
    SELECT stok_miktari INTO v_mevcut_stok
    FROM urunler
    WHERE urun_id = p_urun_id
    FOR UPDATE;
    
    -- Yeterli stok var mı?
    IF v_mevcut_stok IS NULL THEN
        SET p_hata_mesaji = 'Ürün bulunamadı';
        SET p_siparis_id = -1;
        ROLLBACK;
    ELSEIF v_mevcut_stok < p_miktar THEN
        SET p_hata_mesaji = CONCAT('Yetersiz stok. Mevcut: ', v_mevcut_stok, ', Talep: ', p_miktar);
        SET p_siparis_id = -1;
        ROLLBACK;
    ELSE
        -- Stok düş
        UPDATE urunler 
        SET stok_miktari = stok_miktari - p_miktar,
            son_guncelleme = NOW()
        WHERE urun_id = p_urun_id;
        
        -- Siparişi oluştur
        INSERT INTO siparisler(musteri_id, urun_id, miktar, birim_fiyat, toplam_tutar, siparis_tarihi, durum)
        VALUES(p_musteri_id, p_urun_id, p_miktar, p_birim_fiyat, p_miktar * p_birim_fiyat, NOW(), 'beklemede');
        
        SET p_siparis_id = LAST_INSERT_ID();
        
        -- Log kaydı
        INSERT INTO islem_loglari(tablo_adi, islem_turu, kayit_id, aciklama, tarih)
        VALUES('siparisler', 'INSERT', p_siparis_id, 
               CONCAT('Müşteri ', p_musteri_id, ' için sipariş oluşturuldu'), NOW());
        
        COMMIT;
        SET p_hata_mesaji = 'Başarılı';
    END IF;
    
END //

DELIMITER ;

Kullanımı:

CALL stok_guncelle_ve_siparis_olustur(42, 101, 3, 299.99, @siparis_id, @hata);
SELECT @siparis_id, @hata;

Procedure Yönetimi: Listeleme, Görüntüleme ve Silme

Günlük yönetim işlemleri için bilmeniz gereken komutlar:

-- Veritabanındaki tüm procedure'leri listele
SHOW PROCEDURE STATUS WHERE Db = 'veritabani_adi';

-- Procedure kodunu görüntüle
SHOW CREATE PROCEDURE musteri_ara;

-- Belirli bir procedure hakkında bilgi
SELECT 
    ROUTINE_NAME,
    CREATED,
    LAST_ALTERED,
    DEFINER,
    SECURITY_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'veritabani_adi'
  AND ROUTINE_TYPE = 'PROCEDURE';

-- Procedure'ü sil
DROP PROCEDURE IF EXISTS eski_procedure;

-- Mevcut procedure'ü güncelle (MySQL'de ALTER yok, drop-create yapıyoruz)
DROP PROCEDURE IF EXISTS musteri_ara;

DELIMITER //
CREATE PROCEDURE musteri_ara(...)
BEGIN
    -- Yeni kod buraya
END //
DELIMITER ;

Yetki Yönetimi

Stored procedure’lerin güvenlik avantajından yararlanmak için yetki yönetimini doğru kurgulamanız gerekir. Örneğin uygulama kullanıcısına tablolara doğrudan erişim vermek yerine sadece ilgili procedure’leri çağırma yetkisi verin:

-- Uygulama kullanıcısı oluştur
CREATE USER 'uygulama_user'@'10.0.1.%' IDENTIFIED BY 'guclu_sifre_buraya';

-- Tablolara doğrudan erişim verme
-- Sadece procedure çalıştırma yetkisi ver
GRANT EXECUTE ON PROCEDURE veritabani.musteri_ara TO 'uygulama_user'@'10.0.1.%';
GRANT EXECUTE ON PROCEDURE veritabani.stok_guncelle_ve_siparis_olustur TO 'uygulama_user'@'10.0.1.%';
GRANT EXECUTE ON PROCEDURE veritabani.aylik_prim_hesapla TO 'uygulama_user'@'10.0.1.%';

FLUSH PRIVILEGES;

-- Yetkileri kontrol et
SHOW GRANTS FOR 'uygulama_user'@'10.0.1.%';

SQL SECURITY kavramına da dikkat etmek gerekir. Procedure oluştururken iki seçeneğiniz var:

  • DEFINER: Procedure, onu oluşturan kullanıcının yetkileriyle çalışır. Yani çağıran kullanıcının tablolara doğrudan erişimi olmasa bile procedure çalışır.
  • INVOKER: Procedure, çağıran kullanıcının yetkileriyle çalışır.

Çoğu durumda DEFINER daha uygun ve güvenlidir:

CREATE DEFINER = 'db_admin'@'localhost' 
PROCEDURE veritabani.hassas_rapor()
SQL SECURITY DEFINER
BEGIN
    SELECT * FROM hassas_musteri_verileri;
END;

Performans İpuçları

Production ortamında stored procedure yazarken dikkat etmeniz gereken birkaç kritik nokta var.

Procedure’ün execution plan’ını görmek için:

-- Procedure'ün kullandığı sorguları test et
EXPLAIN SELECT musteri_id, ad, soyad 
FROM musteriler 
WHERE sehir = 'Istanbul' AND aktif = 1;

-- Procedure çalışma süresini ölç
SET profiling = 1;
CALL musteri_ara('Istanbul', 1, @toplam);
SHOW PROFILES;
SET profiling = 0;

Dikkat etmeniz gereken performans konuları:

  • Index kullanımı: Procedure içindeki WHERE koşullarında kullanılan sütunlarda index olduğundan emin olun. Aksi halde her CALL’da full table scan yapılır.
  • Cursor’lardan kaçının: Mümkün olduğunca set-based işlemler yapın. Cursor döngüsü satır satır işlem yaptığından yavaştır. Tek bir UPDATE ile yapabildiğinizi cursor’la yapmayın.
  • Transaction boyutu: Uzun transaction’lar lock sorunlarına yol açar. Mümkünse transaction’ları küçük tutun.
  • Temporary table: Karmaşık hesaplamalarda temporary table kullanmak bazen daha verimlidir.
DELIMITER //

CREATE PROCEDURE toplu_guncelleme_verimli(IN p_kategori_id INT, IN p_artis_orani DECIMAL(5,2))
BEGIN
    -- Cursor yerine set-based güncelleme - çok daha hızlı
    UPDATE urunler u
    JOIN kategoriler k ON u.kategori_id = k.kategori_id
    SET u.fiyat = u.fiyat * (1 + p_artis_orani / 100),
        u.son_guncelleme = NOW()
    WHERE k.kategori_id = p_kategori_id
      AND u.aktif = 1;
    
    SELECT ROW_COUNT() AS guncellenen_urun_sayisi;
END //

DELIMITER ;

Procedure’leri Komut Satırından Çağırmak

Cron job’lar veya shell script’lerden procedure çağırmak için:

#!/bin/bash
# /opt/scripts/aylik_rapor.sh

DB_HOST="localhost"
DB_USER="rapor_user"
DB_PASS="sifre"
DB_NAME="veritabani"
LOG_FILE="/var/log/aylik_rapor.log"

YIL=$(date +%Y)
AY=$(date +%m)

echo "[$(date)] Aylık prim hesaplama başladı - $YIL/$AY" >> $LOG_FILE

mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME 
  --skip-column-names 
  -e "CALL aylik_prim_hesapla($YIL, $AY);" >> $LOG_FILE 2>&1

if [ $? -eq 0 ]; then
    echo "[$(date)] İşlem başarıyla tamamlandı" >> $LOG_FILE
else
    echo "[$(date)] HATA: İşlem başarısız" >> $LOG_FILE
    # Alarm mail gönder
    echo "Aylık prim hesaplama hatası - $YIL/$AY" | mail -s "DB Alert" [email protected]
fi

Procedure Debugging

Stored procedure hata ayıklamak gerçekten can sıkıcı olabiliyor. Birkaç pratik yöntem:

DELIMITER //

CREATE PROCEDURE debug_ornegi(IN p_deger INT)
BEGIN
    DECLARE v_adim VARCHAR(100);
    
    SET v_adim = 'Başlangıç';
    
    -- Debug tablosuna log at
    INSERT INTO debug_log(procedure_adi, adim, deger, zaman)
    VALUES('debug_ornegi', v_adim, p_deger, NOW());
    
    -- İşlem yap
    SET v_adim = 'Hesaplama';
    SET p_deger = p_deger * 2;
    
    INSERT INTO debug_log(procedure_adi, adim, deger, zaman)
    VALUES('debug_ornegi', v_adim, p_deger, NOW());
    
    -- SELECT ile ara değerleri görüntüle (development ortamında)
    SELECT v_adim AS mevcut_adim, p_deger AS mevcut_deger;
    
END //

DELIMITER ;

Development ortamında SELECT’leri debug için kullanabilirsiniz ama production’a almadan önce temizleyin ya da bir debug flag parametresi ekleyin.

Sonuç

Stored procedure’ler doğru kullanıldığında veritabanı mimarinize ciddi katkı sağlar. Tekrarlayan iş mantığını tek bir yere toplar, güvenlik katmanı oluşturur ve uygulama ile veritabanı arasındaki iletişimi optimize eder. Özellikle birden fazla uygulamanın aynı veritabanını kullandığı ortamlarda iş mantığını procedure’lerde tutmak tutarlılığı garanti eder.

Bununla birlikte her şeyi stored procedure’e dökmek de doğru değil. Basit CRUD operasyonları için ORM veya uygulama katmanı yeterlidir. Stored procedure’ler özellikle karmaşık iş kuralları, toplu veri işleme, birden fazla tabloyu etkileyen atomik işlemler ve güvenlik gerektiren kritik operasyonlar için değer yaratır.

Bu yazıdaki örnekleri kendi test ortamınızda deneyimleyin, cursor’lar yerine set-based işlemler yazmayı alışkanlık haline getirin ve her zaman hata yönetimini ihmal etmeyin. Production’da “beklenmedik şeyler her zaman olur” zihniyetiyle yazan kod, uzun vadede sizi sabah 3’te uyandırmaktan kurtarır.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir