MySQL ve MariaDB’de Fonksiyon (FUNCTION) Oluşturma

Veritabanı yönetiminde tekrar eden hesaplama ve işlem mantıklarını her sorguda yeniden yazmak hem zaman kaybı hem de hata riski demek. MySQL ve MariaDB’de fonksiyonlar (FUNCTION), bu tekrar eden mantığı bir kez yazıp defalarca kullanmana olanak tanır. Stored procedure’lardan farklı olarak fonksiyonlar bir değer döndürür ve doğrudan SELECT sorgularında kullanılabilir. Bu yazıda gerçek dünya senaryolarıyla MySQL fonksiyonlarını nasıl oluşturacağını, yöneteceğini ve optimize edeceğini adım adım inceleyeceğiz.

MySQL Fonksiyonları Nedir ve Ne Zaman Kullanılır?

MySQL’de User Defined Function (UDF) ya da daha yaygın adıyla stored function, belirli bir işlem mantığını kapsülleyip tek bir isimle çağırılabilir hale getiren veritabanı nesneleridir. Bir fonksiyon her zaman bir değer döndürür; bu değer integer, varchar, decimal, datetime gibi herhangi bir MySQL veri tipi olabilir.

Fonksiyonların en çok işe yaradığı durumlar şunlardır:

  • Tekrar eden hesaplamalar: KDV hesaplama, para birimi dönüşümü, yaş hesaplama gibi işlemler
  • Veri temizleme ve formatlama: Telefon numarası formatlama, TC kimlik doğrulama, e-posta validasyonu
  • Karmaşık iş mantığı: Müşteri segmentasyonu, puanlama sistemleri, fiyatlandırma kuralları
  • String manipülasyonları: Slug oluşturma, metin kısaltma, özel karakter temizleme

Stored procedure ile karıştırılmaması gereken önemli bir fark şu: Fonksiyonlar SELECT içinde kullanılabilir, procedure’lar kullanamaz. Ama fonksiyonlar DML işlemi (INSERT, UPDATE, DELETE) yapamaz, procedure’lar yapabilir. Bu temel ayrımı aklında tut.

Temel Sözdizimi ve İlk Fonksiyon

MySQL’de fonksiyon oluşturmadan önce log_bin_trust_function_creators ayarına dikkat etmem gerekiyor. Binary log açıksa bu ayarı etkinleştirmen gerekebilir.

-- Binary log aktifse bu ayarı yap
SET GLOBAL log_bin_trust_function_creators = 1;

-- Kalıcı yapmak için my.cnf dosyasına ekle
# [mysqld]
# log_bin_trust_function_creators = 1

Şimdi en basit fonksiyon yapısına bakalım:

DELIMITER $$

CREATE FUNCTION fonksiyon_adi(parametre1 TIP, parametre2 TIP)
RETURNS donus_tipi
DETERMINISTIC
BEGIN
    DECLARE degisken TIP;
    -- İşlem mantığı buraya
    SET degisken = parametre1 + parametre2;
    RETURN degisken;
END $$

DELIMITER ;

Buradaki DELIMITER $$ ifadesi çok önemli. MySQL normalde ; karakterini sorgu sonu olarak yorumlar, ama fonksiyon gövdesinde birden fazla ; kullanıyoruz. Bu yüzden geçici olarak ayırıcıyı $$ olarak değiştiriyoruz.

DETERMINISTIC anahtar kelimesi de dikkat gerektiriyor. Bu ifade, aynı giriş parametreleri için fonksiyonun her zaman aynı sonucu döndüreceğini belirtir. Eğer fonksiyonun sonucu rastgele değer, tarih/saat gibi dış faktörlere bağlıysa NOT DETERMINISTIC kullanmalısın. Bu MySQL’in query optimizer’ının doğru çalışması için kritik.

Gerçek Dünya Senaryosu 1: KDV Hesaplama Fonksiyonu

Bir e-ticaret uygulamasında düşün: Ürün fiyatları veritabanında KDV hariç tutuluyor ama raporlarda KDV dahil fiyat göstermen gerekiyor. Her sorguda aynı hesabı yapmak yerine bir fonksiyon yazalım:

DELIMITER $$

CREATE FUNCTION kdv_hesapla(fiyat DECIMAL(10,2), kdv_orani DECIMAL(5,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
COMMENT 'Verilen fiyata KDV ekleyerek son fiyatı döndürür'
BEGIN
    DECLARE kdv_dahil_fiyat DECIMAL(10,2);
    
    IF fiyat < 0 THEN
        RETURN NULL;
    END IF;
    
    IF kdv_orani IS NULL THEN
        SET kdv_orani = 18.00;
    END IF;
    
    SET kdv_dahil_fiyat = fiyat * (1 + kdv_orani / 100);
    
    RETURN ROUND(kdv_dahil_fiyat, 2);
END $$

DELIMITER ;

-- Kullanım örnekleri
SELECT 
    urun_adi,
    birim_fiyat,
    kdv_hesapla(birim_fiyat, 18.00) AS kdv_dahil_fiyat,
    kdv_hesapla(birim_fiyat, 8.00) AS indirimli_kdv_fiyat
FROM urunler
WHERE aktif = 1;

Bu fonksiyon negatif fiyat kontrolü ve NULL değer yönetimi gibi temel doğrulamaları da içeriyor. Gerçek prodüksiyonda bu tür guard clause’lar olmadan fonksiyon yazmak sorun çıkarır.

Gerçek Dünya Senaryosu 2: Türk Telefon Numarası Formatlama

Müşteri veritabanında telefon numaraları farklı formatlarda giriliyor: 05321234567, 5321234567, +905321234567, 532-123-45-67 gibi. Bunları standart bir formata çevirelim:

DELIMITER $$

CREATE FUNCTION telefon_formatla(telefon VARCHAR(20))
RETURNS VARCHAR(15)
DETERMINISTIC
BEGIN
    DECLARE temiz_telefon VARCHAR(15);
    DECLARE sonuc VARCHAR(15);
    
    -- NULL kontrolü
    IF telefon IS NULL THEN
        RETURN NULL;
    END IF;
    
    -- Sadece rakamları bırak
    SET temiz_telefon = REGEXP_REPLACE(telefon, '[^0-9]', '');
    
    -- Başındaki 90 veya 0'ı temizle
    IF LEFT(temiz_telefon, 2) = '90' THEN
        SET temiz_telefon = SUBSTRING(temiz_telefon, 3);
    ELSEIF LEFT(temiz_telefon, 1) = '0' THEN
        SET temiz_telefon = SUBSTRING(temiz_telefon, 2);
    END IF;
    
    -- 10 haneli olmalı
    IF LENGTH(temiz_telefon) != 10 THEN
        RETURN NULL;
    END IF;
    
    -- Formatlı çıktı: 0532 123 45 67
    SET sonuc = CONCAT(
        '0',
        SUBSTRING(temiz_telefon, 1, 3),
        ' ',
        SUBSTRING(temiz_telefon, 4, 3),
        ' ',
        SUBSTRING(temiz_telefon, 7, 2),
        ' ',
        SUBSTRING(temiz_telefon, 9, 2)
    );
    
    RETURN sonuc;
END $$

DELIMITER ;

-- Test edelim
SELECT 
    telefon_formatla('05321234567') AS test1,
    telefon_formatla('+905321234567') AS test2,
    telefon_formatla('532-123-45-67') AS test3,
    telefon_formatla('invalid') AS test4;

Kontrol Yapıları ve Döngüler

Fonksiyonlarda tüm MySQL kontrol yapılarını kullanabilirsin. İşte bir müşteri segmentasyon fonksiyonu:

DELIMITER $$

CREATE FUNCTION musteri_segmenti(toplam_harcama DECIMAL(12,2), uye_ay INT)
RETURNS VARCHAR(20)
DETERMINISTIC
BEGIN
    DECLARE segment VARCHAR(20);
    DECLARE aylik_ortalama DECIMAL(10,2);
    
    -- Sıfır bölme koruması
    IF uye_ay IS NULL OR uye_ay = 0 THEN
        RETURN 'BELIRSIZ';
    END IF;
    
    SET aylik_ortalama = toplam_harcama / uye_ay;
    
    -- CASE yapısıyla segment belirleme
    CASE
        WHEN aylik_ortalama >= 5000 AND toplam_harcama >= 50000 THEN
            SET segment = 'PLATINUM';
        WHEN aylik_ortalama >= 2000 AND toplam_harcama >= 20000 THEN
            SET segment = 'GOLD';
        WHEN aylik_ortalama >= 500 THEN
            SET segment = 'SILVER';
        WHEN aylik_ortalama >= 100 THEN
            SET segment = 'BRONZE';
        ELSE
            SET segment = 'STANDART';
    END CASE;
    
    RETURN segment;
END $$

DELIMITER ;

-- Raporlamada kullanım
SELECT 
    m.musteri_adi,
    m.musteri_soyadi,
    SUM(s.tutar) AS toplam_harcama,
    TIMESTAMPDIFF(MONTH, m.uyelik_tarihi, NOW()) AS uye_ay,
    musteri_segmenti(
        SUM(s.tutar), 
        TIMESTAMPDIFF(MONTH, m.uyelik_tarihi, NOW())
    ) AS segment
FROM musteriler m
LEFT JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id, m.musteri_adi, m.musteri_soyadi, m.uyelik_tarihi;

Gerçek Dünya Senaryosu 3: URL Slug Oluşturma

Blog veya e-ticaret sistemlerinde Türkçe karakterler içeren başlıkları URL dostu hale getirmen gerekir. Bu işlemi veritabanı seviyesinde yapmak bazen çok pratik:

DELIMITER $$

CREATE FUNCTION url_slug_olustur(metin VARCHAR(500))
RETURNS VARCHAR(500)
DETERMINISTIC
BEGIN
    DECLARE slug VARCHAR(500);
    
    SET slug = LOWER(metin);
    
    -- Türkçe karakterleri değiştir
    SET slug = REPLACE(slug, 'ş', 's');
    SET slug = REPLACE(slug, 'ı', 'i');
    SET slug = REPLACE(slug, 'ğ', 'g');
    SET slug = REPLACE(slug, 'ü', 'u');
    SET slug = REPLACE(slug, 'ö', 'o');
    SET slug = REPLACE(slug, 'ç', 'c');
    SET slug = REPLACE(slug, 'â', 'a');
    SET slug = REPLACE(slug, 'î', 'i');
    SET slug = REPLACE(slug, 'û', 'u');
    
    -- Özel karakterleri tire ile değiştir
    SET slug = REGEXP_REPLACE(slug, '[^a-z0-9]+', '-');
    
    -- Baştaki ve sondaki tireleri temizle
    SET slug = TRIM(BOTH '-' FROM slug);
    
    -- Birden fazla tireyi tek tireye indir
    WHILE slug LIKE '%---%' OR slug LIKE '%--%' DO
        SET slug = REPLACE(slug, '--', '-');
    END WHILE;
    
    RETURN slug;
END $$

DELIMITER ;

-- Test
SELECT 
    url_slug_olustur('MySQL Fonksiyon Oluşturma Rehberi') AS slug1,
    url_slug_olustur('Türkçe Karakter Desteği ile SEO') AS slug2,
    url_slug_olustur('  Boşluklu   Başlık   ') AS slug3;

Fonksiyon Yönetimi: Görüntüleme, Değiştirme ve Silme

Fonksiyonları yönetmek için kullanacağın temel komutlar:

-- Mevcut fonksiyonları listele
SHOW FUNCTION STATUS WHERE Db = 'veritabani_adi';

-- Fonksiyon detayını görüntüle
SHOW CREATE FUNCTION kdv_hesapla;

-- Tüm fonksiyonları information_schema üzerinden listele
SELECT 
    ROUTINE_NAME,
    ROUTINE_TYPE,
    DATA_TYPE,
    CREATED,
    LAST_ALTERED,
    SECURITY_TYPE,
    COMMENT
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'veritabani_adi'
  AND ROUTINE_TYPE = 'FUNCTION'
ORDER BY ROUTINE_NAME;

-- Fonksiyon silme
DROP FUNCTION IF EXISTS kdv_hesapla;

-- Fonksiyon güncelleme (önce silip yeniden oluşturman gerekir)
DROP FUNCTION IF EXISTS telefon_formatla;
-- Ardından CREATE FUNCTION ile yeniden oluştur

MySQL’de ALTER FUNCTION komutu sadece özellikleri değiştirebilir, fonksiyon gövdesini değiştiremez. Bu yüzden değişiklik yapmak istediğinde DROP + CREATE döngüsü kullanmak zorundasın.

Fonksiyon Güvenliği: DEFINER ve SQL SECURITY

Prodüksiyon ortamında fonksiyon güvenliğine dikkat etmem gerekiyor:

DELIMITER $$

-- DEFINER: Fonksiyonu hangi kullanıcının yetkileriyle çalışacağını belirtir
-- SQL SECURITY DEFINER: Fonksiyon sahibinin yetkileriyle çalışır
-- SQL SECURITY INVOKER: Çağıran kullanıcının yetkileriyle çalışır

CREATE DEFINER = 'app_user'@'localhost'
FUNCTION stok_durumu_kontrol(urun_id INT)
RETURNS VARCHAR(20)
READS SQL DATA
SQL SECURITY DEFINER
COMMENT 'Ürün stok durumunu döndürür'
BEGIN
    DECLARE stok_miktari INT;
    DECLARE kritik_stok INT DEFAULT 10;
    
    SELECT stok INTO stok_miktari
    FROM urunler
    WHERE id = urun_id;
    
    IF stok_miktari IS NULL THEN
        RETURN 'BULUNAMADI';
    ELSEIF stok_miktari = 0 THEN
        RETURN 'STOK_YOK';
    ELSEIF stok_miktari <= kritik_stok THEN
        RETURN 'KRITIK';
    ELSEIF stok_miktari <= 50 THEN
        RETURN 'DUSUK';
    ELSE
        RETURN 'YETERLI';
    END IF;
END $$

DELIMITER ;

READS SQL DATA karakteristiği de önemli. MySQL’e fonksiyonun veritabanından veri okuduğunu ama değiştirmediğini söyler. Diğer seçenekler:

  • NO SQL: Hiç SQL ifadesi içermiyor
  • CONTAINS SQL: SQL içeriyor ama okuma/yazma yok
  • READS SQL DATA: Sadece okuma yapıyor
  • MODIFIES SQL DATA: Veri değiştiriyor (fonksiyonlarda önerilmez)

Performans İpuçları ve Yaygın Hatalar

Fonksiyonlar kolayca performans sorununa yol açabilir. Dikkat etmen gereken birkaç kritik nokta var:

Her satır için çalışır: Fonksiyonları WHERE koşulunda kullanırken dikkatli ol. Eğer bir fonksiyon WHERE clause’unda indeksli kolonu işliyorsa, index kullanılamaz ve full table scan yapar.

-- KÖTÜ: Her satır için fonksiyon çalışır, index kullanılamaz
SELECT * FROM siparisler 
WHERE kdv_hesapla(tutar, 18) > 1000;

-- İYİ: Tersine çevirerek hesapla
SELECT * FROM siparisler 
WHERE tutar > 1000 / 1.18;

-- Fonksiyon performansını test etmek için
EXPLAIN SELECT 
    urun_adi,
    kdv_hesapla(birim_fiyat, 18) AS kdv_fiyat
FROM urunler;

DETERMINISTIC vs NOT DETERMINISTIC: Bu ayrımı doğru yapmazsan hem optimizer hem de binary log’da sorunlar yaşarsın.

DELIMITER $$

-- Bu fonksiyon NOT DETERMINISTIC çünkü NOW() kullanıyor
CREATE FUNCTION uye_gun_sayisi(uyelik_tarihi DATE)
RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
    RETURN DATEDIFF(CURDATE(), uyelik_tarihi);
END $$

DELIMITER ;

Hata yönetimi: Fonksiyonlarda DECLARE ... HANDLER ile hataları yakalayabilirsin:

DELIMITER $$

CREATE FUNCTION guvenli_bolme(bolunen DECIMAL(10,2), bolen DECIMAL(10,2))
RETURNS DECIMAL(10,4)
DETERMINISTIC
BEGIN
    -- Sıfıra bölme koruması
    IF bolen = 0 OR bolen IS NULL THEN
        RETURN NULL;
    END IF;
    
    RETURN ROUND(bolunen / bolen, 4);
END $$

DELIMITER ;

-- Kullanım
SELECT 
    satis_miktari,
    hedef_miktar,
    guvenli_bolme(satis_miktari, hedef_miktar) * 100 AS hedef_yuzdesi
FROM satis_raporlari;

Fonksiyonları Yedekleme ve Taşıma

Prodüksiyon ortamında fonksiyonları yedeklemek kritik:

# mysqldump ile sadece routines'leri yedekle
mysqldump --routines --no-data -u root -p veritabani_adi > routines_backup.sql

# Tüm veritabanı ile birlikte routines yedekle
mysqldump --routines -u root -p veritabani_adi > full_backup_with_routines.sql

# Belirli bir fonksiyonu manuel olarak yedeklemek için
mysql -u root -p -e "SHOW CREATE FUNCTION veritabani_adi.kdv_hesaplaG" > kdv_fonksiyon_backup.sql

# Fonksiyonları başka bir sunucuya taşıma
mysqldump --routines --no-data -u root -p kaynak_db > routines.sql
mysql -u root -p hedef_db < routines.sql

--routines flag’ini unutmak çok yaygın bir hata. Varsayılan olarak mysqldump stored procedure ve fonksiyonları yedeklemez, bunu özellikle belirtmen gerekir.

Fonksiyon vs Stored Procedure: Hangisini Kullanmalısın?

Bu soruya pratik bir yaklaşımla cevap verelim:

Fonksiyon kullan eğer:

  • Tek bir değer döndürmen gerekiyorsa ve bu değeri SELECT içinde kullanacaksan
  • Hesaplama veya dönüşüm yapıyorsan ve sonucu başka sorgulara gömeceksen
  • DML işlemi yapmıyorsan yani sadece okuma ve hesaplama söz konusuysa
  • Tekrar eden formül veya mantığı birden fazla yerde kullanacaksan

Stored Procedure kullan eğer:

  • Birden fazla değer döndürmen gerekiyorsa (OUT parametreler)
  • INSERT, UPDATE, DELETE işlemi yapacaksan
  • Transaction yönetimi gerekiyorsa
  • Uygulama tarafından CALL ile çağırılacaksa

Sonuç

MySQL fonksiyonları, tekrar eden iş mantığını veritabanı seviyesinde kapsüllemek için güçlü bir araç. Doğru kullanıldığında sorgu kodunu ciddi ölçüde sadeleştirir, tutarsızlıkları önler ve merkezi bir bakım noktası sağlar.

Pratik önerilerim şunlar: Fonksiyonlarını her zaman COMMENT ile belgele, IF EXISTS kontrollerini kullan, güvenlik için DEFINER ve SQL SECURITY ayarlarını ihmal etme. Performans tarafında ise fonksiyonları WHERE koşulunda kullanırken dikkatli ol ve EXPLAIN ile sorgu planını kontrol et.

Prodüksiyona almadan önce fonksiyonlarını geliştirme ortamında yeterince test et. Özellikle NULL değerler, negatif sayılar ve boş string gibi edge case’leri mutlaka kapsayan test sorguları yaz. Bir fonksiyonun düzinelerce sorgu tarafından kullanıldığı bir ortamda hata bulmak gerçekten zahmetli oluyor.

Son olarak, fonksiyonlarını düzenli olarak yedeklemeyi alışkanlık haline getir. --routines parametresini mysqldump scriptlerine eklemek küçük bir detay gibi görünse de sunucu göçü veya felaket kurtarma senaryolarında hayat kurtarıcı oluyor.

Bir yanıt yazın

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