MariaDB ve MySQL’de N’inci En Büyük Değeri Bulma Yöntemi
Veritabanı yönetiminde en sık karşılaşılan ihtiyaçlardan biri, bir tablodaki en büyük, ikinci en büyük veya n’inci en büyük değeri bulmaktır. Kulağa basit gibi gelse de bu sorunu çözmek için birden fazla yöntem bulunuyor ve her yöntemin farklı performans karakteristikleri var. Özellikle büyük veri setlerinde hangi yaklaşımı kullandığınız, sorgunun saniyeler mi yoksa milisaniyeler mi süreceğini belirleyebilir. Bu yazıda MariaDB ve MySQL üzerinde n’inci en büyük değeri bulmanın tüm yöntemlerini, gerçek dünya senaryolarıyla birlikte ele alacağız.
Neden Bu Kadar Önemli?
Bir e-ticaret sisteminde “ikinci en yüksek satış tutarını getir” isteği geldiğinde, ya da bir monitoring sisteminde “en yüksek CPU kullanımına sahip 3. sunucuyu bul” denildiğinde ne yaparsınız? Bu tür sorgular günlük operasyonlarda düşündüğünüzden çok daha sık karşınıza çıkar.
Sorunun özü şu: MAX() fonksiyonu her zaman en büyük değeri verir, ancak n’inci en büyüğü bulmak için farklı teknikler kullanmanız gerekiyor. MariaDB ve MySQL’de bu sorunu çözmek için subquery, LIMIT/OFFSET, pencere fonksiyonları ve değişken tabanlı yaklaşımlar gibi birçok yöntem mevcut.
Test Ortamı Kurulumu
Önce üzerinde çalışacağımız örnek tabloları oluşturalım. Gerçekçi senaryolar için birkaç farklı tablo kullanacağız.
-- Çalışan maaş tablosu
CREATE TABLE employees (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
department VARCHAR(50),
salary DECIMAL(10, 2),
hire_date DATE
);
-- Örnek veri ekleme
INSERT INTO employees (name, department, salary, hire_date) VALUES
('Ahmet Yilmaz', 'IT', 8500.00, '2019-03-15'),
('Fatma Kaya', 'IT', 12000.00, '2017-06-01'),
('Mehmet Demir', 'HR', 7200.00, '2020-01-10'),
('Ayse Celik', 'HR', 9800.00, '2018-09-22'),
('Can Ozturk', 'IT', 15000.00, '2016-04-05'),
('Zeynep Arslan', 'Finance', 11500.00, '2018-11-30'),
('Burak Sahin', 'Finance', 13200.00, '2017-02-14'),
('Selin Yildiz', 'IT', 9500.00, '2019-08-19'),
('Emre Koc', 'HR', 8100.00, '2021-05-03'),
('Deniz Acar', 'Finance', 10500.00, '2020-07-25');
Bu tabloyla birlikte bir de sunucu metrik tablosu oluşturalım, monitoring senaryoları için işimize yarayacak.
-- Sunucu metrik tablosu
CREATE TABLE server_metrics (
id INT AUTO_INCREMENT PRIMARY KEY,
server_name VARCHAR(100),
cpu_usage DECIMAL(5, 2),
memory_usage DECIMAL(5, 2),
recorded_at DATETIME DEFAULT NOW()
);
INSERT INTO server_metrics (server_name, cpu_usage, memory_usage) VALUES
('web-01', 45.5, 62.3),
('web-02', 78.2, 71.0),
('db-01', 92.1, 88.5),
('db-02', 55.8, 90.2),
('cache-01', 23.4, 45.1),
('app-01', 67.9, 58.7),
('app-02', 81.3, 77.4),
('monitoring', 12.6, 35.9),
('backup-01', 88.7, 65.2),
('gateway', 70.1, 52.8);
Yöntem 1: Subquery ile Klasik Yaklaşım
En yaygın bilinen ve MariaDB/MySQL’in tüm versiyonlarında çalışan yöntem, iç içe subquery kullanımıdır. Mantık şu: n’inci en büyük değeri bulmak için, en büyük değeri bulurken ilk (n-1) büyük değeri dışarıda bırakıyoruz.
-- İkinci en yüksek maaşı bulma (N=2)
SELECT MAX(salary) AS second_highest_salary
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
-- Genel formül: N'inci en büyük
-- N=3 için:
SELECT MAX(salary) AS third_highest_salary
FROM employees
WHERE salary < (
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees)
);
Bu yaklaşım küçük tablolar için gayet işe yarıyor ancak n değeri büyüdükçe iç içe subquery sayısı da artıyor, bu da hem yazması hem de bakımı zor bir yapı ortaya çıkarıyor. N=5 için beş kez iç içe yazmanız gerektiğini düşünün. Pratikte bu yöntemi yalnızca ikinci ya da üçüncü en büyük değer için kullanmanızı öneririm.
Yöntem 2: LIMIT ve OFFSET ile Temiz Çözüm
Bu yöntem hem okunması en kolay hem de oldukça esnek olan yaklaşımdır. ORDER BY DESC ile sıralayıp LIMIT ve OFFSET kombinasyonunu kullanıyoruz.
-- N'inci en büyük maaşı bulma (LIMIT/OFFSET yöntemi)
-- İkinci en büyük için OFFSET = N-1 = 1
SELECT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
-- Üçüncü en büyük CPU kullanımına sahip sunucu
SELECT server_name, cpu_usage
FROM server_metrics
ORDER BY cpu_usage DESC
LIMIT 1 OFFSET 2;
-- Beşinci en büyük maaş
SELECT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 4;
Burada dikkat etmeniz gereken kritik nokta: eğer tabloda tekrar eden (duplicate) değerler varsa, bu sorgu her zaman beklediğiniz sonucu vermeyebilir. Örneğin iki çalışanın aynı maaşa sahip olması durumunda sonuç yanıltıcı olabilir. Bu durumu şöyle yönetebilirsiniz:
-- Tekrar eden değerleri dışarıda bırakarak n'inci en büyük
-- (distinct değerler arasında ikinci en büyük)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
-- Aynı maaşa sahip tüm çalışanları da görmek istiyorsak
SELECT name, salary
FROM employees
WHERE salary = (
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1
);
Yöntem 3: Pencere Fonksiyonları (MariaDB 10.2+ ve MySQL 8.0+)
Modern veritabanı versiyonlarını kullanıyorsanız pencere fonksiyonları bu tür sorgular için en güçlü araçtır. RANK(), DENSE_RANK() ve ROW_NUMBER() fonksiyonları arasındaki farkı anlamak çok önemli.
- ROW_NUMBER(): Her satıra benzersiz bir sıra numarası atar, eşit değerler olsa bile
- RANK(): Eşit değerlere aynı sıra numarasını verir, sonraki sıra numarasını atlar (1, 1, 3 gibi)
- DENSE_RANK(): Eşit değerlere aynı sıra numarasını verir ama sonraki numarayı atlamaz (1, 1, 2 gibi)
-- DENSE_RANK ile n'inci en büyük maaş (MariaDB 10.2+ / MySQL 8.0+)
SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;
-- Sadece ikinci en büyüğü çekmek için:
SELECT name, salary
FROM (
SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 2;
-- Departmana göre kendi içinde n'inci en büyük maaş
-- Her departmanın en yüksek 2. maaşlı çalışanı
SELECT name, department, salary
FROM (
SELECT name, department, salary,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees
) ranked
WHERE dept_rank = 2;
Son sorgu özellikle çok değerli bir örnek. Partition By kullanarak her departman için ayrı ayrı sıralama yapabiliyoruz. Bu tür “her grup içinde n’inci en büyük” sorguları, pencere fonksiyonları olmadan yazmak oldukça karmaşık oluyor.
Yöntem 4: Korelasyonlu Subquery
Bu yöntem biraz daha az bilinen ama anlaşılması gereken bir yaklaşım. Özellikle eski MariaDB versiyonlarında pencere fonksiyonu yokken işe yarıyordu.
-- N'inci en büyük maaş (korelasyonlu subquery)
-- Mantık: "Benden büyük kaç farklı değer var?" sorusunu sormak
-- N-1 farklı değer varsa, o değer N'inci en büyüktür
-- İkinci en büyük maaş (N=2, dolayısıyla tam 1 farklı büyük değer olmalı)
SELECT DISTINCT salary
FROM employees e1
WHERE 1 = (
SELECT COUNT(DISTINCT salary)
FROM employees e2
WHERE e2.salary > e1.salary
);
-- Üçüncü en büyük maaş (N=3, tam 2 farklı büyük değer)
SELECT DISTINCT salary
FROM employees e1
WHERE 2 = (
SELECT COUNT(DISTINCT salary)
FROM employees e2
WHERE e2.salary > e1.salary
);
-- Dinamik olarak N değeri değiştirilebilir formül:
-- WHERE (N-1) = (SELECT COUNT(DISTINCT salary) ...)
Bu yöntemin performansı büyük tablolarda oldukça kötüdür çünkü her satır için bir subquery çalıştırılıyor. Milyonlarca satırlık tablolarda bu yaklaşımı kesinlikle kullanmayın.
Gerçek Dünya Senaryosu 1: E-Ticaret Sipariş Analizi
Bir e-ticaret platformunda çalışıyorsunuz ve “geçen ay en yüksek 3. sipariş tutarını veren müşteriyi bul” gibi bir istek geldi. Böyle bir sorguyu nasıl yazarsınız?
-- Önce örnek tablo ve veri
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT,
customer_name VARCHAR(100),
total_amount DECIMAL(10, 2),
order_date DATE
);
INSERT INTO orders (customer_id, customer_name, total_amount, order_date) VALUES
(1, 'Ahmet Yilmaz', 1250.00, '2024-11-05'),
(2, 'Fatma Kaya', 3800.00, '2024-11-12'),
(1, 'Ahmet Yilmaz', 950.00, '2024-11-18'),
(3, 'Mehmet Demir', 5200.00, '2024-11-03'),
(4, 'Ayse Celik', 2100.00, '2024-11-22'),
(5, 'Can Ozturk', 4700.00, '2024-11-07'),
(2, 'Fatma Kaya', 1800.00, '2024-11-14'),
(6, 'Zeynep Arslan', 3100.00, '2024-11-25'),
(3, 'Mehmet Demir', 890.00, '2024-11-09'),
(7, 'Burak Sahin', 6500.00, '2024-11-02');
-- Müşteri bazında toplam harcama hesapla,
-- en yüksek 3. harcamayı yapan müşteriyi bul
SELECT customer_name, total_spent
FROM (
SELECT customer_name,
SUM(total_amount) AS total_spent,
DENSE_RANK() OVER (ORDER BY SUM(total_amount) DESC) AS spending_rank
FROM orders
WHERE order_date >= '2024-11-01' AND order_date < '2024-12-01'
GROUP BY customer_id, customer_name
) ranked_customers
WHERE spending_rank = 3;
Gerçek Dünya Senaryosu 2: Monitoring Sisteminde Alarm
Bir monitoring sisteminde, CPU kullanımı açısından “en yüksek ikinci sunucuya” özel bir alarm kuralı tanımlamanız gerekiyor. Bu tür dinamik eşik değerleri veritabanı sorgusuyla belirlenebilir.
-- Son 5 dakika içindeki kayıtlardan en yüksek 2. CPU'ya sahip sunucu
SELECT server_name, cpu_usage,
CASE
WHEN cpu_usage >= 90 THEN 'CRITICAL'
WHEN cpu_usage >= 75 THEN 'WARNING'
ELSE 'OK'
END AS status
FROM (
SELECT server_name, cpu_usage,
DENSE_RANK() OVER (ORDER BY cpu_usage DESC) AS cpu_rank
FROM server_metrics
WHERE recorded_at >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
) ranked
WHERE cpu_rank = 2;
-- Alternatif: LIMIT/OFFSET ile aynı sonuç
SELECT server_name, cpu_usage
FROM server_metrics
WHERE recorded_at >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
ORDER BY cpu_usage DESC
LIMIT 1 OFFSET 1;
Performans Karşılaştırması ve Index Kullanımı
Hangi yöntemi kullandığınız kadar, doğru index tanımlamak da kritik önem taşıyor. N’inci en büyük sorguları için sıralama yaptığınız kolona mutlaka index ekleyin.
-- Salary kolonuna index ekle
CREATE INDEX idx_salary ON employees(salary);
-- Composite index senaryosu: departmana göre gruplama yapıyorsak
CREATE INDEX idx_dept_salary ON employees(department, salary);
-- Index kullanımını doğrulama
EXPLAIN SELECT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 2;
-- Pencere fonksiyonu sorgusunun execution planını kontrol et
EXPLAIN SELECT name, salary
FROM (
SELECT name, salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
) ranked
WHERE rnk = 3;
EXPLAIN çıktısında dikkat etmeniz gereken noktalar:
- type kolonunda
ALLgörüyorsanız tam tablo taraması yapılıyor demektir, index eklemek gerekiyor - key kolonunda index adı görünmelidir
- rows kolonundaki değer ne kadar düşükse sorgu o kadar verimli çalışıyor
Genel performans rehberi olarak şunu söyleyebilirim: LIMIT/OFFSET yaklaşımı indexle birlikte kullanıldığında genellikle en hızlı sonucu verir. Pencere fonksiyonları ise karmaşık gruplama senaryolarında hem daha okunabilir hem de optimize edilmiş bir sorgu planı sunar. Korelasyonlu subquery ise büyük tablolarda kaçınmanız gereken yöntemdir.
NULL Değerleri ile Başa Çıkma
Gerçek dünya tablolarında NULL değerleri kaçınılmaz. N’inci en büyük sorguları NULL içeren kolonlarda beklenmedik sonuçlar verebilir.
-- NULL değer içeren senaryo
INSERT INTO employees (name, department, salary) VALUES
('Test User', 'IT', NULL);
-- NULL'ları dışarıda bırakarak n'inci en büyük
SELECT salary
FROM employees
WHERE salary IS NOT NULL
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
-- COALESCE ile NULL değerleri ele alma
SELECT name, COALESCE(salary, 0) AS salary
FROM (
SELECT name, salary,
DENSE_RANK() OVER (ORDER BY salary DESC NULLS LAST) AS rnk
FROM employees
) ranked
WHERE rnk = 2;
-- MariaDB'de NULLS LAST desteğini kontrol et
-- Eğer destek yoksa alternatif:
SELECT DISTINCT salary
FROM employees
WHERE salary IS NOT NULL
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
Stored Procedure ile Dinamik N Değeri
Uygulamanızdan dinamik N değeri göndermeniz gerekiyorsa, stored procedure kullanmak kod tekrarını önlemenin güzel bir yolu.
-- N'inci en büyük maaşı döndüren stored procedure
DELIMITER //
CREATE PROCEDURE GetNthHighestSalary(IN n INT, IN dept VARCHAR(50))
BEGIN
DECLARE offset_val INT;
SET offset_val = n - 1;
IF dept IS NULL OR dept = '' THEN
-- Tüm departmanlar için
SELECT DISTINCT salary AS nth_highest
FROM employees
WHERE salary IS NOT NULL
ORDER BY salary DESC
LIMIT 1 OFFSET offset_val;
ELSE
-- Belirli bir departman için
SELECT DISTINCT salary AS nth_highest
FROM employees
WHERE salary IS NOT NULL AND department = dept
ORDER BY salary DESC
LIMIT 1 OFFSET offset_val;
END IF;
END //
DELIMITER ;
-- Kullanım örnekleri
CALL GetNthHighestSalary(1, ''); -- En yüksek maaş (tüm çalışanlar)
CALL GetNthHighestSalary(2, ''); -- İkinci en yüksek maaş
CALL GetNthHighestSalary(3, 'IT'); -- IT departmanında 3. en yüksek maaş
CALL GetNthHighestSalary(1, 'HR'); -- HR departmanında en yüksek maaş
Sık Yapılan Hatalar
Yıllar içinde gördüğüm ve kendinizin de düşebileceği yaygın hatalar şunlar:
- Duplicate değerleri göz ardı etmek:
LIMIT 1 OFFSET 1kullanırkenDISTINCTeklemeden doğrudan sıralama yapmak. İki çalışan aynı maaşa sahipse “ikinci en yüksek” aslında yine aynı değer olabilir. - NULL değerleri unutmak: Sıralama yapılacak kolonda NULL varsa
ORDER BY salary DESCsorgusunda NULL değerler beklenmedik konumlarda çıkabilir. - Index eksikliği: Büyük tablolarda index olmadan
ORDER BYile sıralama yapmak. Yüz binlerce satırda farkı çok net hissedeceksiniz. - Yanlış pencere fonksiyonu seçmek:
ROW_NUMBERileDENSE_RANKfarkını karıştırmak. Eşit değerler olduğundaROW_NUMBERsizi yanıltabilir. - Eski MariaDB versiyonunda pencere fonksiyonu kullanmaya çalışmak: MariaDB 10.2 öncesinde
OVER()sözdizimi desteklenmez.SELECT @@version;ile versiyon kontrolü yapın.
MariaDB vs MySQL Farklılıkları
İki sistem arasında bu konuda küçük ama önemli farklar var:
- MariaDB 10.2+ ve MySQL 8.0+ pencere fonksiyonlarını destekler. Eğer daha eski bir versiyon kullanıyorsanız LIMIT/OFFSET veya subquery yaklaşımlarına geri dönmeniz gerekir.
- MariaDB bazı durumlarda
NULLS FIRSTveNULLS LASTsözdizimini desteklerken MySQL 8.0.26 öncesinde bu sözdizimi desteklenmez. - Her iki sistemde de
EXPLAINkomutu execution planını gösterir ama çıktı formatı biraz farklılık gösterebilir.EXPLAIN FORMAT=JSONkullanarak daha detaylı bilgi alabilirsiniz.
Sonuç
N’inci en büyük değeri bulmak için “tek doğru yöntem” yoktur. Duruma göre doğru aracı seçmek sysadmin’liğin özü.
Pratik önerim şu: Eğer MariaDB 10.2+ veya MySQL 8.0+ kullanıyorsanız ve özellikle gruplama (PARTITION BY) gerekiyorsa, doğrudan pencere fonksiyonlarına gidin. Yazması temiz, bakımı kolay ve optimizer genellikle iyi bir plan üretiyor.
Basit tek tablo sorgularında, özellikle iyi indexlenmiş kolonlarda, LIMIT/OFFSET yaklaşımı hız açısından genellikle kazanıyor. Stored procedure içinde dinamik N değeri ile kullanmak da oldukça pratik.
Korelasyonlu subquery yöntemini ise yalnızca legacy sistemlerde ya da çok küçük tablolarda tercih edin. Üretim ortamında milyonlarca satırlık tablolarda bu yöntemi kullanmak ciddi performans sorunlarına yol açar.
Son olarak, hangi yöntemi seçerseniz seçin EXPLAIN çıktısını kontrol etmeyi alışkanlık haline getirin. Sorgunun ne yaptığını anlamadan kodu production’a almak, üzerinizde gece mesaisi biriktirir.
