← Kursa Dön
📄 Text · 35 min

Slow Query Tespiti ve Optimizasyon

Giriş — "Neden Bu Sayfa Bu Kadar Yavaş?"

Her geliştiricinin başına gelir: geliştirme ortamında gayet hızlı çalışan bir sayfa, production'a çıkınca ağır çekimde yüklenir. Kullanıcılar şikayet etmeye başlar, patron surat asar, herkes "sunucu mu yavaş?" diye sorar. Ama aslında sorun çoğu zaman sunucu değildir — sorun yavaş SQL sorgularıdır.

Bir e-ticaret sitesinde sipariş listesi sayfasını düşün. 100 siparişin olduğu geliştirme ortamında sayfa 0.1 saniyede açılır. Ama production'da 500.000 sipariş var ve aynı sorgu artık 8 saniye sürüyor. İşte bu noktada "slow query" (yavaş sorgu) kavramıyla tanışıyorsun.

Bu derste, yavaş sorguları nasıl tespit edeceğini, neden yavaş olduğunu anlayacağını ve nasıl optimize edeceğini adım adım öğreneceğiz. Sadece teorik bilgi değil — gerçek dünyada kullanabileceğin pratik teknikler.


Slow Query Nedir?

Bir sorgunun "yavaş" sayılması görecelidir — 10 milisaniye süren bir sorgu, saniyede 10.000 istek alan bir sistemde yavaş olabilir. Ama genel kural olarak:

  • < 100ms: Hızlı, sorun yok

  • 100ms - 1s: Dikkat edilmeli, optimizasyon düşünülebilir

  • 1s - 5s: Yavaş, kesinlikle optimize edilmeli

  • > 5s: Kritik yavaş, acil müdahale gerekir

🎯 Analoji: Bunu bir restoran mutfağı gibi düşün. Normalde bir sipariş 10 dakikada hazırlanıyorsa, 15 dakika "biraz uzamış" ama kabul edilebilir. 30 dakika "yavaş" — müşteri mutsuz. 1 saat? Müşteri çoktan gitmiş. Veritabanında da süre eşiklerini belirliyorsun ve bu eşiği aşan her sorguyu "slow query" olarak işaretliyorsun.


MySQL Slow Query Log — Yavaş Sorguları Otomatik Yakalama

MySQL, belirlediğin bir süre eşiğini aşan sorguları otomatik olarak loglayan bir mekanizmaya sahip: Slow Query Log. Bu, yavaş sorguları tespit etmenin en temel ve güvenilir yolu.

Slow Query Log'u Aktifleştirme

-- Mevcut ayarları kontrol et
SHOW VARIABLES LIKE 'slow_query_log%';
-- slow_query_log       | OFF
-- slow_query_log_file  | /var/lib/mysql/hostname-slow.log

SHOW VARIABLES LIKE 'long_query_time';
-- long_query_time | 10.000000  (varsayılan 10 saniye — çok yüksek!)

-- Slow query log'u aç
SET GLOBAL slow_query_log = 'ON';

-- Eşiği 1 saniyeye düşür (önerilen başlangıç değeri)
SET GLOBAL long_query_time = 1;

-- Index kullanmayan sorguları da logla
SET GLOBAL log_queries_not_using_indexes = 'ON';

Bu ayarları kalıcı yapmak için MySQL konfigürasyon dosyasına (my.cnf veya my.ini) eklemen gerekir:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 1
log_queries_not_using_indexes = 1

⚠️ Dikkat: log_queries_not_using_indexes seçeneği, trafik yoğun sistemlerde çok fazla log üretebilir. Önce geliştirme ortamında dene, production'da dikkatli kullan.

Slow Query Log'un İçeriği

Log dosyasında her yavaş sorgu şu bilgilerle kaydedilir:

# Time: 2024-12-15T14:23:45.123456Z
# User@Host: app_user[app_user] @ [192.168.1.50]  Id: 12345
# Query_time: 3.456789  Lock_time: 0.000123  Rows_sent: 150  Rows_examined: 850000
SET timestamp=1702651425;
SELECT o.order_id, c.first_name, c.last_name, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.total_amount DESC;

Buradaki kritik bilgiler:

  • Query_time: 3.456789 — Sorgu 3.45 saniye sürmüş

  • Rows_sent: 150 — Kullanıcıya 150 satır gönderilmiş

  • Rows_examined: 850000 — Ama veritabanı 850.000 satır taramış! İşte sorun burada.

Rows_examined / Rows_sent oranı, sorgunun verimliliğini gösteren en önemli metrik. 850.000 satır tarayıp 150 satır döndürmek, 5666:1 oranı demek — bu korkunç bir oran. İdeal olarak bu oranın 10:1'in altında olmasını istersin.


mysqldumpslow — Log Analiz Aracı

Slow query log dosyası büyüdüğünde elle okumak imkansızlaşır. MySQL'in yerleşik aracı mysqldumpslow, log dosyasını analiz edip özet çıkarır:

# En yavaş 10 sorguyu göster
mysqldumpslow -t 10 /var/log/mysql/slow-query.log

# En çok tekrarlanan yavaş sorguları göster
mysqldumpslow -s c -t 10 /var/log/mysql/slow-query.log

# Toplam süreye göre sırala (en çok toplam zaman harcayan)
mysqldumpslow -s t -t 10 /var/log/mysql/slow-query.log

# Ortalama süreye göre sırala
mysqldumpslow -s at -t 10 /var/log/mysql/slow-query.log

mysqldumpslow parametreleri:

  • -s c: Tekrar sayısına (count) göre sırala

  • -s t: Toplam süreye (total time) göre sırala

  • -s at: Ortalama süreye (average time) göre sırala

  • -s r: Gönderilen satır sayısına göre sırala

  • -t N: İlk N sonucu göster

Çıktı şuna benzer:

Count: 347  Time=2.45s (850s)  Lock=0.00s (0s)  Rows=150.0 (52050)
  SELECT o.order_id, c.first_name FROM orders o JOIN customers c 
  ON o.customer_id = c.customer_id WHERE o.order_date BETWEEN 'S' AND 'S'

Bu çıktı diyor ki: bu sorgu 347 kez çalışmış, ortalama 2.45 saniye sürmüş ve toplamda 850 saniye (14 dakika!) harcamış. Bu, optimize edilmesi gereken bir numaralı aday.

💡 İpucu: Production ortamında "en yavaş tek sorgu"yu aramak yerine "toplam en çok zaman harcayan sorgu"yu bulmak genellikle daha etkilidir. 0.5 saniye süren ama günde 50.000 kez çalışan bir sorgu, 5 saniye süren ama günde 10 kez çalışan sorgudan çok daha fazla kaynak tüketir.


EXPLAIN — Sorgunun Röntgenini Çekmek

Bir önceki derste EXPLAIN'i tanıtmıştık. Şimdi slow query optimizasyonunda nasıl kullanacağımıza bakalım. EXPLAIN, bir sorgunun nasıl çalıştırılacağının planını gösterir — sorguyu çalıştırmadan.

Gerçek Bir Slow Query Analizi

Diyelim ki şu sorgu yavaş çalışıyor:

SELECT c.first_name, c.last_name, COUNT(o.order_id) AS order_count,
       SUM(o.total_amount) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'İstanbul'
  AND o.order_date >= '2024-01-01'
GROUP BY c.customer_id
HAVING total_spent > 1000
ORDER BY total_spent DESC;

EXPLAIN ile bakalım:

EXPLAIN SELECT c.first_name, c.last_name, COUNT(o.order_id) AS order_count,
               SUM(o.total_amount) AS total_spent
        FROM customers c
        LEFT JOIN orders o ON c.customer_id = o.customer_id
        WHERE c.city = 'İstanbul'
          AND o.order_date >= '2024-01-01'
        GROUP BY c.customer_id
        HAVING total_spent > 1000
        ORDER BY total_spent DESC;
+----+-------+------+------+---------+------+--------+-----------------------------+
| id | table | type | key  | key_len | ref  | rows   | Extra                       |
+----+-------+------+------+---------+------+--------+-----------------------------+
|  1 | c     | ALL  | NULL | NULL    | NULL | 150000 | Using where; Using filesort |
|  1 | o     | ALL  | NULL | NULL    | NULL | 500000 | Using where                 |
+----+-------+------+------+---------+------+--------+-----------------------------+

Bu EXPLAIN çıktısı alarm zilleri çaldırmalı! Neden?

  1. type: ALL — Her iki tablo da full table scan yapıyor. Index kullanılmıyor!

  2. rows: 150000 × 500000 — En kötü durumda 75 milyar satır kombinasyonu deneniyor

  3. Using filesort — Sonuçlar bellekte sıralanıyor (ORDER BY için), bu da ekstra maliyet

Adım Adım Optimizasyon

Adım 1: WHERE koşulları için index ekle

-- customers tablosunda city sütununa index
CREATE INDEX idx_customers_city ON customers(city);

-- orders tablosunda customer_id ve order_date için composite index
CREATE INDEX idx_orders_cust_date ON orders(customer_id, order_date);

EXPLAIN tekrar çalıştır:

+----+-------+------+---------------------+---------+---------+--------+-------------+
| id | table | type | key                 | key_len | ref     | rows   | Extra       |
+----+-------+------+---------------------+---------+---------+--------+-------------+
|  1 | c     | ref  | idx_customers_city  | 102     | const   |  12000 | Using where |
|  1 | o     | ref  | idx_orders_cust_date| 8       | c.cust. |      5 | Using where |
+----+-------+------+---------------------+---------+---------+--------+-------------+

Muazzam fark! Şimdi:

  • customers tablosunda 150.000 yerine 12.000 satır taranıyor (sadece İstanbul'dakiler)

  • orders tablosunda her müşteri için 500.000 yerine ~5 satır taranıyor

  • Toplam: 12.000 × 5 = 60.000 satır (önceki 75 milyar!)

Adım 2: ORDER BY optimizasyonu

Using filesort hâlâ görünebilir çünkü total_spent hesaplanmış bir değer — bunu index ile çözmek zor. Ama HAVING filtresinden sonra kalan satır sayısı az olacağı için filesort maliyeti ihmal edilebilir.

Adım 3: Sonucu ölç

-- Optimizasyon öncesi
-- Query_time: 3.456s  Rows_examined: 850000

-- Optimizasyon sonrası
-- Query_time: 0.045s  Rows_examined: 12150

-- 76 kat hızlanma!

EXPLAIN ANALYZE — Gerçek Çalışma İstatistikleri

MySQL 8.0.18+ sürümünde EXPLAIN ANALYZE komutu, sorguyu gerçekten çalıştırır ve her adımın gerçek süresini, satır sayısını gösterir:

EXPLAIN ANALYZE
SELECT c.first_name, c.last_name, COUNT(o.order_id) AS order_count
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'İstanbul'
GROUP BY c.customer_id
ORDER BY order_count DESC
LIMIT 10\G
-> Limit: 10 row(s)  (actual time=45.2..45.2 rows=10 loops=1)
    -> Sort: order_count DESC  (actual time=45.1..45.2 rows=10 loops=1)
        -> Table scan on <temporary>  (actual time=44.8..44.9 rows=12000 loops=1)
            -> Aggregate using temporary table  (actual time=44.8..44.8 rows=12000 loops=1)
                -> Nested loop inner join  (cost=15234 rows=60000) 
                   (actual time=0.1..38.5 rows=58432 loops=1)
                    -> Index lookup on c using idx_customers_city (city='İstanbul')
                       (cost=2400 rows=12000) (actual time=0.05..2.3 rows=12000 loops=1)
                    -> Index lookup on o using idx_orders_cust_date (customer_id=c.customer_id)
                       (cost=1.07 rows=5) (actual time=0.002..0.003 rows=4.87 loops=12000)

EXPLAIN ANALYZE'ın avantajı: tahmini (estimated) ve gerçek (actual) değerleri yan yana göstermesi. rows=12000 tahmininde gerçekten rows=12000 geldiyse tahmin doğru. Eğer tahmin çok farklıysa, istatistikler güncel değildir — ANALYZE TABLE çalıştırman gerekir.

⚠️ Dikkat: EXPLAIN ANALYZE sorguyu gerçekten çalıştırır! Production'da dikkatli kullan. DELETE veya UPDATE sorgularında kullanma — gerçekten silme/güncelleme yapar.


Performance Schema — Sorgu İstatistikleri

MySQL'in performance_schema veritabanı, çalışan tüm sorguların istatistiklerini toplar. Slow query log'dan daha ayrıntılıdır:

-- En çok zaman harcayan sorguları bul
SELECT 
    DIGEST_TEXT AS query_pattern,
    COUNT_STAR AS exec_count,
    ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_time_sec,
    ROUND(AVG_TIMER_WAIT / 1000000000000, 4) AS avg_time_sec,
    SUM_ROWS_EXAMINED AS total_rows_examined,
    SUM_ROWS_SENT AS total_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

Bu sorgu, tüm sorgu kalıplarını toplam süreye göre sıralar. Çıktı:

+------------------------------------------+------------+----------------+--------------+
| query_pattern                            | exec_count | total_time_sec | avg_time_sec |
+------------------------------------------+------------+----------------+--------------+
| SELECT ... FROM orders JOIN customers... |      15234 |         342.56 |       0.0225 |
| SELECT ... FROM products WHERE categ...  |      89012 |         267.89 |       0.0030 |
| INSERT INTO order_items ...              |      45678 |         123.45 |       0.0027 |
+------------------------------------------+------------+----------------+--------------+

Bu tablo sana şunu söylüyor: ilk sorgu toplam 342 saniye harcamış — optimize edilecek bir numaralı aday.

-- Tam tablo taraması yapan sorguları bul
SELECT 
    DIGEST_TEXT,
    COUNT_STAR,
    SUM_NO_INDEX_USED AS no_index_count,
    ROUND(AVG_TIMER_WAIT / 1000000000000, 4) AS avg_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_NO_INDEX_USED > 0
ORDER BY SUM_NO_INDEX_USED DESC
LIMIT 10;

💡 İpucu: Performance Schema varsayılan olarak açıktır ama kaynak tüketir. Etkisini ölçmek istiyorsan, performance_schema = OFF ile kapatıp karşılaştırabilirsin — ama genellikle açık bırakmak en iyisi.


Profiling — Sorgu Adımlarını Detaylı İnceleme

Bir sorgunun hangi adımda ne kadar zaman harcadığını görmek istiyorsan profiling kullanabilirsin:

-- Profiling'i aç
SET profiling = 1;

-- Sorguyu çalıştır
SELECT c.first_name, c.last_name, COUNT(o.order_id) AS order_count
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'İstanbul'
GROUP BY c.customer_id;

-- Profiling sonuçlarını gör
SHOW PROFILE FOR QUERY 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000052 |
| checking permissions | 0.000008 |
| Opening tables       | 0.000018 |
| init                 | 0.000025 |
| System lock          | 0.000009 |
| optimizing           | 0.000015 |
| statistics           | 0.000089 |
| preparing            | 0.000018 |
| Creating tmp table   | 0.000035 |
| executing            | 0.000004 |
| Sending data         | 0.045123 |  ← Zamanın %99'u burada!
| Creating sort index  | 0.002345 |
| end                  | 0.000005 |
| removing tmp table   | 0.000012 |
| Cleanup              | 0.000008 |
+----------------------+----------+

"Sending data" aşaması aslında "veriyi diskten okuma + filtreleme + istemciye gönderme" işlemlerinin toplamıdır. Zamanın büyük çoğunluğu burada harcanıyorsa, sorun genellikle eksik index veya çok fazla satır taramasıdır.

⚠️ Dikkat: SHOW PROFILE MySQL 8.0'da deprecated (kullanımdan kaldırılma sürecinde). Yeni sürümlerde performance_schema kullanılması öneriliyor. Ama hâlâ çalışıyor ve hızlı tanılama için işe yarıyor.


Yaygın Slow Query Nedenleri ve Çözümleri

1. Eksik Index

En yaygın neden. WHERE, JOIN ve ORDER BY koşullarında kullanılan sütunlarda index yoksa, veritabanı full table scan yapar.

-- ❌ YAVAŞ: index yok
SELECT * FROM orders WHERE customer_id = 12345;
-- Full table scan: 500.000 satır taranır

-- ✅ HIZLI: index ekle
CREATE INDEX idx_orders_customer ON orders(customer_id);
SELECT * FROM orders WHERE customer_id = 12345;
-- Index lookup: 15 satır okunur

2. Fonksiyon Kullanımı ile Index'i Devre Dışı Bırakma

WHERE koşulunda sütuna fonksiyon uygulamak, index kullanımını engeller:

-- ❌ YAVAŞ: YEAR() fonksiyonu index'i devre dışı bırakır
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- Full table scan — order_date üzerindeki index kullanılmaz!

-- ✅ HIZLI: Aralık sorgusu olarak yaz
SELECT * FROM orders 
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';
-- Index kullanılır!

-- ✅ ALTERNATİF: Functional index (MySQL 8.0.13+)
CREATE INDEX idx_order_year ON orders ((YEAR(order_date)));
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- Artık index kullanır

3. Implicit Type Conversion (Örtük Tür Dönüşümü)

Sütun tipiyle uyumsuz bir değerle karşılaştırma yapmak, index'i devre dışı bırakabilir:

-- ❌ YAVAŞ: phone_number VARCHAR ama sayısal değer veriyorsun
SELECT * FROM customers WHERE phone_number = 5551234567;
-- MySQL her satırda VARCHAR → INT dönüşümü yapar → index kullanılmaz

-- ✅ HIZLI: Doğru tiple karşılaştır
SELECT * FROM customers WHERE phone_number = '5551234567';
-- Index kullanılır

4. LIKE '%xyz' — Baştan Joker Karakter

-- ❌ YAVAŞ: Baştan % → index kullanılmaz
SELECT * FROM products WHERE product_name LIKE '%Laptop%';
-- Full table scan

-- ✅ Alternatif 1: Baştan eşleşme
SELECT * FROM products WHERE product_name LIKE 'Laptop%';
-- Index kullanılır (prefix match)

-- ✅ Alternatif 2: FULLTEXT index (tam metin araması gerekiyorsa)
ALTER TABLE products ADD FULLTEXT INDEX ft_product_name (product_name);
SELECT * FROM products WHERE MATCH(product_name) AGAINST('Laptop');

5. Çok Fazla JOIN

Her JOIN işlemi, sorgunun karmaşıklığını artırır. 5-6'dan fazla JOIN genellikle sorun işareti:

-- ❌ Potansiyel problem: 6 tablo JOIN
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories cat ON p.category_id = cat.category_id
JOIN departments d ON c.department_id = d.department_id;

-- ✅ Çözümler:
-- 1. Gerçekten tüm tabloları JOIN etmen gerekiyor mu? İhtiyacın olmayanları çıkar
-- 2. Denormalizasyon düşün (sık kullanılan verileri tek tabloda tut)
-- 3. Her JOIN koşulunda index olduğundan emin ol

Gerçek Dünya Optimizasyon Örneği

E-ticaret sistemimizde şu rapor sorgusu 12 saniye sürüyor:

-- ❌ Orijinal sorgu — 12 saniye
SELECT 
    cat.category_name,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * oi.unit_price) AS total_revenue,
    AVG(oi.quantity * oi.unit_price) AS avg_order_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories cat ON p.category_id = cat.category_id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
  AND o.status = 'completed'
GROUP BY cat.category_id
ORDER BY total_revenue DESC;

Adım 1: EXPLAIN ile analiz et

orders: type=ALL, rows=500000 → Full table scan!
order_items: type=ref, rows=3 → İyi
products: type=eq_ref, rows=1 → İyi
categories: type=eq_ref, rows=1 → İyi

Sorun orders tablosunda. order_date ve status için index yok.

Adım 2: Index ekle

-- Composite index: status + order_date (sıra önemli!)
CREATE INDEX idx_orders_status_date ON orders(status, order_date);

Neden status önce? Çünkü status = 'completed' eşitlik koşulu, order_date BETWEEN aralık koşulu. Composite index'te eşitlik koşulu her zaman önce gelmeli.

Adım 3: Tekrar EXPLAIN

orders: type=range, key=idx_orders_status_date, rows=120000 → 4x iyileşme

Adım 4: Sorguyu yeniden yaz

-- ✅ Optimize edilmiş sorgu — 0.8 saniye
SELECT 
    cat.category_name,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * oi.unit_price) AS total_revenue,
    AVG(oi.quantity * oi.unit_price) AS avg_order_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories cat ON p.category_id = cat.category_id
WHERE o.status = 'completed'
  AND o.order_date >= '2024-01-01' 
  AND o.order_date < '2025-01-01'
GROUP BY cat.category_id
ORDER BY total_revenue DESC;

Değişiklikler:

  1. BETWEEN yerine >= ve < kullandık (daha kesin ve index-friendly)

  2. orders tablosunu FROM'da ilk sıraya aldık (optimizer genellikle bunu kendisi yapar ama okunabilirlik için iyi)

  3. Index eklendi

Sonuç: 12 saniyeden 0.8 saniyeye — 15x hızlanma!


Optimizasyon Kontrol Listesi

Bir sorgu yavaşsa, şu adımları sırasıyla uygula:

  1. EXPLAIN çalıştır — type: ALL var mı? rows çok mu? Using filesort var mı?

  2. Index kontrol et — WHERE, JOIN, ORDER BY sütunlarında index var mı?

  3. Fonksiyon kontrolü — WHERE'de sütuna fonksiyon uygulanmış mı?

  4. Tür uyumu — Karşılaştırılan değerler doğru tipte mi?

  5. SELECT * — Gerçekten tüm sütunlar gerekli mi?

  6. Satır sayısı — LIMIT kullanılabilir mi? Gereksiz veri çekiliyor mu?

  7. JOIN sayısı — Tüm JOIN'ler gerekli mi?

  8. İstatistikler güncel mi?ANALYZE TABLE çalıştır

  9. Sorguyu yeniden yaz — Subquery yerine JOIN? IN yerine EXISTS?

-- İstatistikleri güncelle (optimizer'ın doğru karar vermesi için)
ANALYZE TABLE orders;
ANALYZE TABLE customers;
ANALYZE TABLE order_items;

-- Tablo durumunu kontrol et
SHOW TABLE STATUS LIKE 'orders'\G
-- Rows: 500000
-- Data_length: 52428800 (50MB)
-- Index_length: 15728640 (15MB)

PostgreSQL Farklılıkları

PostgreSQL'de slow query tespiti biraz farklıdır:

-- PostgreSQL: Slow query log ayarları (postgresql.conf)
-- log_min_duration_statement = 1000  (1 saniye, milisaniye cinsinden)
-- log_statement = 'none'  (sadece yavaş sorguları logla)

-- PostgreSQL'de EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_id = 12345;
-- Buffers: shared hit=3 read=1  ← disk I/O bilgisi de verir

-- pg_stat_statements eklentisi (Performance Schema muadili)
SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

PostgreSQL'in pg_stat_statements eklentisi, MySQL'in Performance Schema'sına benzer ama daha temiz ve kullanımı kolaydır.


Özet

  • Slow Query Log ile yavaş sorguları otomatik yakala — eşiği 1 saniyeye ayarla

  • mysqldumpslow ile log dosyasını analiz et — toplam süreye göre sırala

  • EXPLAIN ile sorgunun execution planını incele — type: ALL kırmızı bayrak

  • EXPLAIN ANALYZE gerçek istatistikleri gösterir ama sorguyu çalıştırır, dikkatli ol

  • Performance Schema tüm sorgu kalıplarının istatistiklerini toplar

  • Rows_examined / Rows_sent oranı sorgu verimliliğinin en iyi göstergesi

  • Optimizasyon sırası: index ekle → fonksiyon kullanımını düzelt → sorguyu yeniden yaz

  • Tek seferde bir değişiklik yap ve etkisini ölç — birden fazla değişikliği aynı anda yapma

Sıkça Yapılan Hatalar

  1. Slow query log eşiğini çok yüksek bırakmak — Varsayılan 10 saniye çok yüksek. 1 saniye veya daha düşük kullan.

  2. Sadece en yavaş tek sorguya odaklanmak — Toplam süreye bak. Sık çalışan "biraz yavaş" sorgular, nadir çalışan "çok yavaş" sorgulardan daha fazla kaynak tüketir.

  3. EXPLAIN ANALYZE'ı production'da DELETE/UPDATE ile kullanmak — Gerçekten çalıştırır! Sadece SELECT sorgularında kullan.

  4. Index ekleyip test etmemek — Index ekledikten sonra EXPLAIN'i tekrar çalıştır ve gerçekten kullanılıp kullanılmadığını doğrula.

  5. Geliştirme ortamında test edip "hızlı" demek — Geliştirme ortamında 100 satır var, production'da 10 milyon. Performans testini production benzeri veri miktarıyla yap.

  6. ANALYZE TABLE'ı unutmak — İstatistikler eski kalırsa, optimizer yanlış kararlar verir. Büyük veri değişikliklerinden sonra çalıştır.