MariaDB ve MySQL’de MIN ve MAX ile En Küçük ve En Büyük Değeri Bulma
Veritabanı sorgularında en sık ihtiyaç duyulan işlemlerden biri, bir veri kümesindeki en küçük veya en büyük değeri bulmaktır. MariaDB ve MySQL’de bu iş için MIN() ve MAX() aggregate fonksiyonları kullanılır. Bu fonksiyonlar ilk bakışta basit görünse de, gerçek dünya senaryolarında GROUP BY ile birleştiğinde, alt sorgularda kullanıldığında veya NULL değerlerle karşılaşıldığında bazı nüanslar ortaya çıkar. Bu yazıda temel kullanımdan ileri seviye senaryolara kadar her şeyi ele alacağız.
MIN() ve MAX() Fonksiyonlarına Giriş
MIN() ve MAX() fonksiyonları, SQL standardının bir parçasıdır ve hem MariaDB hem de MySQL’de tam olarak desteklenir. Temel görevleri şudur: bir sütundaki tüm değerlere bakarak en küçük veya en büyük olanı döndürmek.
Bu fonksiyonlar sadece sayısal değerler için değil, tarih/saat ve metin değerleri için de çalışır. Metin değerlerinde alfabetik sıralama esas alınır. NULL değerler ise her iki fonksiyon tarafından da görmezden gelinir, bu önemli bir detaydır.
Yazı boyunca kullanacağımız örnek veritabanı şemasını önce oluşturalım:
-- Örnek veritabanı ve tablolar
CREATE DATABASE satis_db;
USE satis_db;
CREATE TABLE urunler (
id INT AUTO_INCREMENT PRIMARY KEY,
urun_adi VARCHAR(100),
kategori VARCHAR(50),
fiyat DECIMAL(10,2),
stok INT,
olusturma_tarihi DATE
);
CREATE TABLE siparisler (
id INT AUTO_INCREMENT PRIMARY KEY,
musteri_id INT,
urun_id INT,
miktar INT,
toplam_tutar DECIMAL(10,2),
siparis_tarihi DATETIME,
teslimat_tarihi DATETIME
);
CREATE TABLE musteriler (
id INT AUTO_INCREMENT PRIMARY KEY,
ad VARCHAR(50),
soyad VARCHAR(50),
il VARCHAR(50),
kayit_tarihi DATE
);
-- Örnek veri ekleyelim
INSERT INTO urunler (urun_adi, kategori, fiyat, stok, olusturma_tarihi) VALUES
('Laptop A', 'Elektronik', 15000.00, 25, '2023-01-15'),
('Laptop B', 'Elektronik', 22000.00, 10, '2023-02-20'),
('Telefon X', 'Elektronik', 8500.00, 50, '2023-01-10'),
('Klavye Pro', 'Aksesuar', 750.00, 100, '2023-03-05'),
('Mouse Wireless', 'Aksesuar', 450.00, 150, '2023-03-10'),
('Monitor 27"', 'Elektronik', 9800.00, 30, '2023-02-01'),
('Kulaklık Z', 'Aksesuar', 1200.00, 75, '2023-04-15');
Temel MIN() ve MAX() Kullanımı
En basit kullanım biçimiyle başlayalım. Tüm ürünler arasındaki en düşük ve en yüksek fiyatı bulmak istiyoruz:
-- En düşük ve en yüksek fiyatı tek sorguda getir
SELECT
MIN(fiyat) AS en_dusuk_fiyat,
MAX(fiyat) AS en_yuksek_fiyat,
MAX(fiyat) - MIN(fiyat) AS fiyat_araligi
FROM urunler;
Bu sorgu tek bir satır döndürür ve tüm tablodaki min/max değerlerini gösterir. fiyat_araligi sütunu ise en yüksek ile en düşük fiyat arasındaki farkı hesaplar, bu da veri yayılımını anlamak için kullanışlıdır.
Tarih değerleriyle kullanım da aynı mantıkla çalışır. En eski ve en yeni ürünü bulmak için:
-- En eski ve en yeni ürün kayıt tarihleri
SELECT
MIN(olusturma_tarihi) AS ilk_urun_tarihi,
MAX(olusturma_tarihi) AS son_urun_tarihi,
DATEDIFF(MAX(olusturma_tarihi), MIN(olusturma_tarihi)) AS gun_farki
FROM urunler;
Metin sütunlarında da çalışır. Alfabetik olarak ilk ve son gelen ürün adını bulmak için:
-- Alfabetik olarak ilk ve son ürün adı
SELECT
MIN(urun_adi) AS alfabetik_ilk,
MAX(urun_adi) AS alfabetik_son
FROM urunler;
GROUP BY ile Kategoriye Göre MIN ve MAX
Gerçek dünya uygulamalarında genellikle tüm tablonun değil, belirli grupların min/max değerlerine ihtiyaç duyarsınız. Örneğin her kategori için en ucuz ve en pahalı ürünü bulmak:
-- Her kategori için min ve max fiyat
SELECT
kategori,
COUNT(*) AS urun_sayisi,
MIN(fiyat) AS en_ucuz,
MAX(fiyat) AS en_pahali,
AVG(fiyat) AS ortalama_fiyat,
SUM(stok) AS toplam_stok
FROM urunler
GROUP BY kategori
ORDER BY MAX(fiyat) DESC;
Bu sorgu her kategori için kapsamlı bir özet sunar. ORDER BY MAX(fiyat) DESC ile en pahalı ürüne sahip kategori en üstte listelenir.
Stok durumuna göre de benzer bir analiz yapabiliriz. Kategorilere göre en düşük stoklu ürünü bulmak operasyonel kararlar için kritiktir:
-- Kategorilere göre stok analizi
SELECT
kategori,
MIN(stok) AS kritik_stok,
MAX(stok) AS max_stok,
MIN(stok) / MAX(stok) * 100 AS stok_dagilim_yuzdesi
FROM urunler
GROUP BY kategori
HAVING MIN(stok) < 30;
Buradaki HAVING ifadesine dikkat edin. WHERE ile aggregate fonksiyon sonuçlarını filtreleyemezsiniz, bunun yerine HAVING kullanmanız gerekir. Bu sorgu yalnızca minimum stoğu 30’un altında olan kategorileri getirir.
Alt Sorgularla MIN ve MAX Kullanımı
Çoğu zaman sadece min veya max değeri değil, o değere sahip kaydın tüm bilgilerini isteriz. Örneğin en ucuz ürünün sadece fiyatını değil, adını ve diğer özelliklerini de görmek istiyoruz. Bunun için alt sorgu kullanmak gerekir:
-- En ucuz ürünün tüm bilgilerini getir
SELECT
id,
urun_adi,
kategori,
fiyat,
stok
FROM urunler
WHERE fiyat = (SELECT MIN(fiyat) FROM urunler);
Alt sorgu MIN(fiyat) değerini hesaplar, dış sorgu ise o değere eşit fiyata sahip tüm kayıtları döndürür. Eğer birden fazla ürün aynı minimum fiyata sahipse hepsi listelenir.
Daha karmaşık bir senaryo: Her kategoride en pahalı ürünün tüm bilgilerini getirmek istiyoruz. Bu kez korelasyonlu alt sorgu kullanacağız:
-- Her kategorideki en pahalı ürünün detayları
SELECT
u1.id,
u1.urun_adi,
u1.kategori,
u1.fiyat,
u1.stok
FROM urunler u1
WHERE u1.fiyat = (
SELECT MAX(u2.fiyat)
FROM urunler u2
WHERE u2.kategori = u1.kategori
);
Bu sorgu her satır için o kategorideki maksimum fiyatı hesaplar ve yalnızca bu maksimum fiyata sahip kayıtları döndürür. u1 ve u2 takma adlarını dikkatli kullandık, dış sorgu ile iç sorgu aynı tabloyu farklı perspektiften okuyuyor.
JOIN ile MIN ve MAX Kombinasyonu
Gerçek sistemlerde veriler genellikle birden fazla tabloya yayılmış durumdadır. Siparişler tablosuyla birleştirerek her müşterinin en yüksek sipariş tutarını bulalım:
-- Her müşterinin en yüksek sipariş tutarı
SELECT
m.id,
m.ad,
m.soyad,
m.il,
COUNT(s.id) AS toplam_siparis,
MIN(s.toplam_tutar) AS en_kucuk_siparis,
MAX(s.toplam_tutar) AS en_buyuk_siparis,
SUM(s.toplam_tutar) AS toplam_harcama
FROM musteriler m
LEFT JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id, m.ad, m.soyad, m.il
ORDER BY MAX(s.toplam_tutar) DESC;
LEFT JOIN kullandık çünkü hiç sipariş vermemiş müşterileri de listede görmek istiyoruz. Bu durumda MIN ve MAX değerleri NULL olacaktır.
Ürün bazlı satış analizi için daha kapsamlı bir sorgu yapalım:
-- Ürün bazlı sipariş analizi
SELECT
u.urun_adi,
u.kategori,
u.fiyat AS liste_fiyati,
COUNT(s.id) AS siparis_adedi,
MIN(s.siparis_tarihi) AS ilk_siparis,
MAX(s.siparis_tarihi) AS son_siparis,
MIN(s.miktar) AS min_siparis_miktari,
MAX(s.miktar) AS max_siparis_miktari
FROM urunler u
LEFT JOIN siparisler s ON u.id = s.urun_id
GROUP BY u.id, u.urun_adi, u.kategori, u.fiyat
ORDER BY siparis_adedi DESC;
Bu sorgu her ürün için ne zaman ilk siparişin verildiğini, en son siparişin ne zaman olduğunu ve sipariş miktarlarının aralığını gösterir. Envanter yönetimi ve talep analizi için oldukça kullanışlıdır.
NULL Değerler ve MIN/MAX
NULL değerlerin davranışı önemli bir konudur. MariaDB ve MySQL’de MIN() ve MAX() fonksiyonları NULL değerleri tamamen görmezden gelir. Bu bazen beklenen sonucu vermeyebilir:
-- NULL davranışını test edelim
CREATE TABLE test_null (
id INT,
deger INT
);
INSERT INTO test_null VALUES (1, 10), (2, NULL), (3, 5), (4, NULL), (5, 20);
-- NULL değerler görmezden gelinir
SELECT
MIN(deger) AS min_deger, -- Sonuç: 5
MAX(deger) AS max_deger, -- Sonuç: 20
COUNT(*) AS toplam_kayit, -- Sonuç: 5
COUNT(deger) AS degerli_kayit -- Sonuç: 3
FROM test_null;
Eğer NULL değerleri de hesaba katmak isterseniz, COALESCE veya IFNULL fonksiyonlarıyla dönüşüm yapabilirsiniz:
-- NULL değerleri 0 kabul ederek hesapla
SELECT
MIN(COALESCE(deger, 0)) AS min_null_sifir, -- Sonuç: 0
MAX(COALESCE(deger, 0)) AS max_null_sifir -- Sonuç: 20
FROM test_null;
-- NULL değerlerin var olup olmadığını kontrol et
SELECT
MIN(deger) AS gercek_min,
CASE
WHEN MIN(deger) IS NULL THEN 'Tüm değerler NULL'
ELSE CAST(MIN(deger) AS CHAR)
END AS durum
FROM test_null
WHERE id > 10; -- Kayıt yoksa sonuç NULL döner
Tablo boşsa veya tüm değerler NULL‘sa, MIN() ve MAX() fonksiyonları NULL döndürür. Uygulama tarafında bu durumu mutlaka kontrol etmelisiniz.
Performans Optimizasyonu ve İndeks Kullanımı
MIN() ve MAX() fonksiyonlarının performansı doğrudan indeks yapısıyla ilgilidir. MariaDB ve MySQL, WHERE koşulu olmaksızın tek bir sütun üzerindeki MIN() veya MAX() sorgularını indeks kullanarak optimize edebilir.
-- İndeks durumunu kontrol et
EXPLAIN SELECT MIN(fiyat) FROM urunler;
EXPLAIN SELECT MAX(fiyat) FROM urunler;
-- fiyat sütununa indeks ekle
CREATE INDEX idx_fiyat ON urunler(fiyat);
-- Şimdi tekrar kontrol et - "Select tables optimized away" görmeli
EXPLAIN SELECT MIN(fiyat) FROM urunler;
EXPLAIN çıktısında Select tables optimized away ifadesini görürseniz, MariaDB/MySQL tabloya hiç bakmadan indeksin ilk veya son değerini okuyarak sonucu döndürmektedir. Bu son derece hızlı çalışır.
Bileşik indekslerle daha karmaşık optimizasyonlar da yapılabilir:
-- Kategori ve fiyat üzerine bileşik indeks
CREATE INDEX idx_kategori_fiyat ON urunler(kategori, fiyat);
-- Bu sorgu bileşik indeksten faydalanır
SELECT kategori, MIN(fiyat), MAX(fiyat)
FROM urunler
GROUP BY kategori;
-- Sorgu planını kontrol et
EXPLAIN SELECT kategori, MIN(fiyat), MAX(fiyat)
FROM urunler
GROUP BY kategori;
Bileşik indeks sayesinde GROUP BY kategori ve MIN/MAX(fiyat) işlemleri tek bir indeks taramasıyla tamamlanabilir.
Gerçek Dünya Senaryosu: E-Ticaret Performans Raporu
Bir e-ticaret sisteminde yaygın ihtiyaç duyulan performans raporunu oluşturalım. Bu sorgu birden fazla tabloyu birleştirerek kapsamlı bir analiz sunar:
-- Aylık satış performans raporu
SELECT
DATE_FORMAT(s.siparis_tarihi, '%Y-%m') AS ay,
COUNT(DISTINCT s.musteri_id) AS benzersiz_musteri,
COUNT(s.id) AS toplam_siparis,
MIN(s.toplam_tutar) AS en_dusuk_siparis,
MAX(s.toplam_tutar) AS en_yuksek_siparis,
AVG(s.toplam_tutar) AS ortalama_siparis,
SUM(s.toplam_tutar) AS aylik_ciro,
MIN(s.siparis_tarihi) AS ay_ilk_siparis,
MAX(s.siparis_tarihi) AS ay_son_siparis,
TIMESTAMPDIFF(
HOUR,
MIN(s.siparis_tarihi),
MAX(s.siparis_tarihi)
) AS aktif_saat_araligi
FROM siparisler s
WHERE s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(s.siparis_tarihi, '%Y-%m')
ORDER BY ay DESC;
Bu rapor aylık bazda en düşük ve en yüksek sipariş tutarlarını, toplam ciroy ve o ay kaç farklı müşterinin alışveriş yaptığını gösterir. TIMESTAMPDIFF ile ilk sipariş ve son sipariş arasındaki saat farkı hesaplanarak günün kaç saatinde aktif alışveriş yapıldığı analiz edilebilir.
Teslimat süresi analizi de MIN ve MAX ile yapılabilir:
-- Teslimat süresi analizi (gün olarak)
SELECT
u.kategori,
COUNT(s.id) AS siparis_sayisi,
MIN(DATEDIFF(s.teslimat_tarihi, s.siparis_tarihi)) AS en_hizli_teslimat,
MAX(DATEDIFF(s.teslimat_tarihi, s.siparis_tarihi)) AS en_yavas_teslimat,
AVG(DATEDIFF(s.teslimat_tarihi, s.siparis_tarihi)) AS ortalama_teslimat,
SUM(
CASE
WHEN DATEDIFF(s.teslimat_tarihi, s.siparis_tarihi) > 5
THEN 1 ELSE 0
END
) AS gec_teslimat_sayisi
FROM siparisler s
JOIN urunler u ON s.urun_id = u.id
WHERE s.teslimat_tarihi IS NOT NULL
GROUP BY u.kategori
ORDER BY ortalama_teslimat DESC;
WITH (CTE) ile Gelişmiş MIN/MAX Sorguları
MariaDB 10.2.1 ve MySQL 8.0 sürümlerinden itibaren WITH ile CTE (Common Table Expression) kullanılabilmektedir. Bu yapı karmaşık MIN/MAX sorgularını daha okunabilir hale getirir:
-- CTE ile kategorideki en pahalı ürünleri bul
WITH kategori_maxlari AS (
SELECT
kategori,
MAX(fiyat) AS max_fiyat
FROM urunler
GROUP BY kategori
),
kategori_minlari AS (
SELECT
kategori,
MIN(fiyat) AS min_fiyat
FROM urunler
GROUP BY kategori
)
SELECT
u.urun_adi,
u.kategori,
u.fiyat,
km.max_fiyat,
kmin.min_fiyat,
CASE
WHEN u.fiyat = km.max_fiyat THEN 'En Pahalı'
WHEN u.fiyat = kmin.min_fiyat THEN 'En Ucuz'
ELSE 'Orta'
END AS fiyat_konumu
FROM urunler u
JOIN kategori_maxlari km ON u.kategori = km.kategori
JOIN kategori_minlari kmin ON u.kategori = kmin.kategori
ORDER BY u.kategori, u.fiyat DESC;
Bu sorgu her ürünün kendi kategorisindeki konumunu gösterir. En pahalı mı, en ucuz mu yoksa ortada mı? Bu bilgi fiyatlandırma kararları için değerlidir.
Sık Yapılan Hatalar
MIN() ve MAX() kullanırken karşılaşılan bazı yaygın hatalar şunlardır:
- GROUP BY olmadan seçim hatası:
SELECT urun_adi, MIN(fiyat) FROM urunlergibi bir sorguONLY_FULL_GROUP_BYmoddayken hata verir.urun_adiya GROUP BY’a eklenmeli ya da aggregate fonksiyon içine alınmalıdır.
- WHERE ile aggregate filtreleme:
WHERE MIN(fiyat) > 1000yazamazsınız. Bunun yerineHAVING MIN(fiyat) > 1000kullanmalısınız.
- Boş sonuç kümesi: Hiç kayıt eşleşmediğinde MIN/MAX
NULLdöner. Uygulama kodunda bu durumuCOALESCE(MIN(fiyat), 0)gibi bir yapıyla ele alın.
- Tarih formatı karışıklığı: VARCHAR olarak saklanan tarih değerlerinde
MINveMAXalfabetik sıralama yapar, gerçek kronolojik sırayı vermez. Tarih sütunlarını her zamanDATEveyaDATETIMEtipinde tanımlayın.
Sonuç
MIN() ve MAX() fonksiyonları MariaDB ve MySQL’in temel araç setinin vazgeçilmez parçalarıdır. Basit görünseler de GROUP BY, alt sorgular, CTE ve JOIN kombinasyonlarıyla son derece güçlü analizler yapmanızı sağlarlar. Gerçek dünya senaryolarında bu fonksiyonları sıkça kullanacaksınız; satış raporları, stok yönetimi, teslimat analizi, performans izleme ve daha pek çok alanda kritik veriye ulaşmak için bunlara başvuracaksınız.
Performans tarafında en önemli noktayı bir kez daha vurgulayalım: sık sorgulanan sütunlarda indeks kullanmak, özellikle MIN() ve MAX() içeren sorgularda dramatik hız farkı yaratır. EXPLAIN komutunu alışkanlık haline getirin ve her kritik sorgunun nasıl çalıştığını anlayın. Veritabanı yönetiminde kör nokta bırakmamak, sorun yaşandığında da proaktif davranabilmek için bu alışkanlık son derece değerlidir.
