İçeriğe geç

SQL İndeksleme ve EXPLAIN

Orta 10 dk Çok sık karşılaşılır

30 saniyede özet

İndeks kitabın arkasındaki dizin gibidir: sorguyu sihirle hızlandırmaz, daha az sayfa okutur. Veritabanı indeksi kullanmıyorsa üç ayrı sebep olabilir ve bunların ikisi indeksin kendisiyle ilgili değildir.

Sorgun yavaş ve veritabanı tabloyu baştan sona okuyor. İlk akla gelen “indeks yok” olur, ama iki ihtimal daha var ve çözümleri farklı.

  1. Bayt: Yavaş sorguya indeks ekledim. Artık uçacak!

  2. Sen: Uçtu mu?

  3. Bayt: Hayır. Veritabanı indeksi hiç kullanmıyor, yine bütün tabloyu okuyor.

  4. Bayt: Dizin orada duruyor ama okuyucu ona bakmıyor. Neden?

Dizin işe yarar, ama yalnızca doğru soruda.
Adım adım oku
  1. İndeks yoksa veritabanı istenen satırı bulmak için bütün sayfaları okur.
  2. B-Tree dizini alfabetik bir kitap dizini gibidir: birkaç adımda doğru sayfaya iner.
  3. Birden çok sütunlu bir indekste sorgu ilk sütunu kullanmıyorsa dizinde başlanacak bir yer yoktur.
  4. Koşul satırların büyük kısmını getiriyorsa her sayfa zaten okunacaktır; tabloyu taramak daha ucuzdur.

İndeks ne yapar, ne yapmaz?

Bir indeks sorguyu hızlandırmaz. Okunması gereken sayfa sayısını azaltır.

Milyon satırlık bir tablo 10.000 sayfaya yayılmışsa, indekssiz her sorgu 10.000 sayfa okur. B-Tree ise sıralı bir yapıdır: her seviye aralığı yüzlerce kat daraltır, bu yüzden hedefe 3-4 sayfa okuyarak ulaşılır.

kök [A .......................... Z] 1 sayfa
iç düğüm [H ........ M] 1 sayfa
yaprak [Izmir] → satırın yeri 1 sayfa
Tablo bir milyon satırdan on milyona çıktı. İndeksle tek bir satırı bulmak şimdi kaç kat daha fazla sayfa okutur? Cevabı göster

Neredeyse hiç fazla okutmaz. Ağacın yüksekliği satır sayısıyla logaritmik büyür; her seviye aralığı yüzlerce kat daralttığı için on kat büyüme çoğu zaman derinliğe bir seviye bile eklemez.

Kafam karıştı, daha basit anlat

İndeks bir kitabın arkasındaki dizin gibidir. Aradığın kelimeyi bütün kitabı okumadan bulursun, çünkü dizin sana hangi sayfaya bakacağını söyler.

Kendin gör

B-Tree indeks — planlayıcı neden indeksi kullanmıyor?

Tohum 7
SELECT id, total, created_at
  FROM orders
 WHERE customer_id = 4711;
CREATE INDEX ON orders (customer_id);
100.000 satır · 1.000 heap sayfası
Index Scanindeks tam taramadan ucuz
  • kök — tüm aralık
  • iç düğüm — aralık daraldı
  • yaprak — customer_id = 4711
Okunan sayfa0 / 23

İncelenen satır

0

Dönen satır

0

Planlayıcı maliyeti

91/ seq 1.000

Hız
Adım 0

Şu an ne oldu?

Ağaçta iniliyor — seviye 0/3

Her seviye aralığı 200 kat daraltıyor. 1000 sayfalık bir tabloda hedefe ulaşmak yalnızca 3 sayfa okumaya bakıyor.

Aklında kalsın: B-Tree yüksekliği satır sayısıyla logaritmik büyür. Tabloyu on kat büyütmek derinliği genelde hiç değiştirmez.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

Sırayla dene — her adım farklı bir dersi gösteriyor:

  1. (customer_id) + customer_id = 4711. İndeks seçiliyor. 1000 sayfa yerine ~23 sayfa.
  2. Tablo boyutunu 500 bine çıkar. Sayfa sayısı neredeyse hiç değişmiyor — ağacın derinliği aynı kaldı. İndeksin asıl gücü bu.
  3. İndeksi (status, customer_id) yap, koşulu customer_id bırak. Seq Scan. İndeks duruyor ama kullanılamıyor.
  4. Koşulu status = 'PENDING' yap. Yine Seq Scan — ama bu sefer sebep farklı: indeks kullanılabilir, sadece çok satır eşliyor.
  5. “Yalnızca indeksteki sütunlar” anahtarını aç. Aynı sorgu artık Index Only Scan.
Hızlı kontrolBaşlangıç

Bir B-tree indeks hangi sorguyu hızlandırmaz?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

İndeks neden kullanılmaz? Üç sebep

SebepEXPLAIN ne derÇözüm
İndeks yokSeq Scanİndeks ekle
Leftmost prefix kırıkSeq ScanSütun sırasını değiştir veya ikinci indeks
Koşul çok satır eşliyorSeq ScanÇözüm indeks değil — sorguyu veya modeli değiştir
Kafam karıştı, daha basit anlat

Dizin olsa bile veritabanı onu kullanmayabilir. Çok satır gelecekse kitabı baştan okumak, dizinle tek tek sayfa atlamaktan daha hızlıdır.

Hızlı kontrolOrta

`WHERE UPPER(email) = 'A@B.COM'` sorgusu `email` üzerindeki indeksi kullanır mı?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Dizin alfabetiktir: ilk sütun kuralı

Bileşik bir indeks, sütunları soldan sağa sıralar. Önce status’a göre sıralı, her status içinde customer_id’ye göre.

CREATE INDEX ON orders (status, customer_id);
WHERE status = 'PENDING' AND customer_id = 4711 -- ✅ seek edilir
WHERE status = 'PENDING' -- ✅ ilk sütun var
WHERE customer_id = 4711 -- ❌ ağaçta inilecek yol yok

Soyada göre sıralı bir telefon rehberinde “adı Ahmet olanları” bulmak için rehberi baştan sona okumak gerekir.

Sütun sırasını nasıl seçersin.

  • Eşitlikle sorgulananlar önce, aralıkla sorgulananlar sonra. Bir aralık koşulundan (>, BETWEEN, LIKE 'x%') sonraki sütunlar artık seek için kullanılamaz.
  • En seçici sütun öne. customer_id binlerce farklı değer alır, status beş. Öne customer_id gelir.
  • Sorgularını say, sütunlarını değil. İndeks tasarımı tablo şemasından değil, gerçekten çalışan sorgulardan çıkar.

seçicilikBir koşulun satırların ne kadarını elediği. Sadece birkaç farklı değer alan bir sütunda indeks çoğu zaman hiç kullanılmaz.Tablonun beşte birini okumak, indeksten gidip her satır için tabloya dönmekten ucuzdur — planlayıcı bu hesabı yapar ve indeksi reddeder.Sözlükte gör → — indeksin işe yaramadığı yer. Simülatörde 4. adım tam olarak bunu gösteriyor: status = 'PENDING' koşulu satırların beşte birini eşliyor.

Her sayfada zaten birkaç eşleşme var; indeksten gitmek tüm sayfaları okumak + bir de indeksi okumak demek. Planlayıcı haklı olarak tam taramayı seçiyor.

Az sayıda farklı değer alan bir sütunu (durum, aktif/pasif) tek başına indekslemek işe yaramaz. Seçici bir sütunun arkasına eklenirse işe yarar: (customer_id, status).

Kafam karıştı, daha basit anlat

Telefon rehberi önce soyada, sonra ada göre sıralıdır. Yalnızca adı biliyorsan rehber sana yardım edemez. Bileşik indeks de ilk sütundan başlanarak kullanılır.

Hızlı kontrolOrta

`CREATE INDEX ON orders (customer_id, status, created_at)` varken her sorguyu indeksin kullanılıp kullanılmadığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

İndeks kullanılır

Leftmost prefix karşılanıyor

    Kullanılmaz

    Baştaki sütun sorguda yok

      covering indexSorgunun ihtiyaç duyduğu tüm sütunları içeren indeks. Veritabanı tabloya hiç gitmeden cevabı indeksten okur — index-only scan.Sözlükte gör →. Simülatörde 5. adımda ne değişti: sorgunun istediği tüm sütunlar indekste olduğu için heap’e hiç gidilmedi.

      -- indeks customer_id ve status tutuyor, sorgu başka sütun istemiyor
      SELECT customer_id, status FROM orders WHERE status = 'PENDING';
      --> Index Only Scan

      Normal bir Index Scan iki adımdır: indeks satırın yerini söyler, sonra satır heap’ten okunur. Covering index ikinci adımı kaldırır; bedeli daha büyük bir indeks ve daha yavaş yazmalardır.

      Aşağıdaki indeksler bir bankanın hesap hareketleri ekranından ve bekleyen EFT kuyruğundan çıkarıldı.

      Derinleş · Hesap hareketleri: sorgudan çıkan indeksler 4 dosya · ~61 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/resources/db/migration/ V21__account_movement_indexes.sql İndeksler: eşitlik sütunu önde, aralık sütunu arkada, ekranın gösterdiği sütunlar INCLUDE ile indekste. Bekleyen EFT'ler için yalnızca o satırları tutan küçük bir partial index.

      src/main/resources/db/migration/V21__account_movement_indexes.sql
      -- Statement screen: one account, a date range, newest first.
      -- Equality column first, range column second; the columns the screen shows are
      -- carried in the index so the heap is never read (Index Only Scan).
      CREATE INDEX idx_movement_account_booked
      ON account_movement (account_id, booked_at DESC, id DESC)
      INCLUDE (amount, currency, description);
      -- Outgoing EFT queue: only a tiny share of rows is ever PENDING, so index just those.
      -- An index on status alone would match too many rows to be used for the other values.
      CREATE INDEX idx_eft_pending
      ON outgoing_eft (created_at)
      WHERE status = 'PENDING';
      -- On a large live table these run as CREATE INDEX CONCURRENTLY, outside a
      -- transaction, so writes are not blocked while the index builds.

      src/main/java/com/bank/account/ MovementRepository.java Repository: indeksi tasarlatan iki sorgu. Sayfalama OFFSET ile değil, son görülen satırdan devam ederek.

      src/main/java/com/bank/account/MovementRepository.java
      interface MovementRepository extends Repository<AccountMovement, Long> {
      // Keyset pagination: "older than the last row I showed", not OFFSET, so page 40
      // costs the same as page 1. booked_at ties are broken by id.
      @Query(value = """
      SELECT id, booked_at, amount, currency, description
      FROM account_movement
      WHERE account_id = :accountId
      AND booked_at >= :from
      AND (booked_at, id) < (:beforeBookedAt, :beforeId)
      ORDER BY booked_at DESC, id DESC
      LIMIT :pageSize
      """, nativeQuery = true)
      List<MovementRow> page(long accountId, Instant from, Instant beforeBookedAt, long beforeId, int pageSize);
      // Matches the partial index: the WHERE clause repeats its predicate exactly.
      @Query(value = """
      SELECT id FROM outgoing_eft
      WHERE status = 'PENDING'
      ORDER BY created_at
      LIMIT :batchSize
      """, nativeQuery = true)
      List<Long> nextPendingEfts(int batchSize);
      }

      explain-statement-page.txt Beklenen plan: ekran sorgusu heap'e hiç gitmiyor. Plan adına değil, tahmini ve gerçek satır sayısına bak.

      explain-statement-page.txt
      EXPLAIN (ANALYZE, BUFFERS) on the statement page query, expected shape:
      Limit
      -> Index Only Scan using idx_movement_account_booked on account_movement
      Index Cond: ((account_id = $1) AND (booked_at >= $2))
      Filter: (ROW(booked_at, id) < ROW($3, $4))
      Heap Fetches: 0 <- every column came from the index
      What to check, not what to expect:
      - estimated rows vs actual rows: a large gap means stale statistics, run ANALYZE
      - Heap Fetches above 0 on a busy table: the visibility map is behind, VACUUM
      - a Sort node above the scan: the ORDER BY no longer matches the index order

      counter-example/ wrong-order.sql Karşı örnek: aynı iki sütun ters sırada. Aralık önde olduğu için hesap numarası seek için kullanılamaz; düşük seçicilikli durum sütunu da tek başına işe yaramaz.

      counter-example/wrong-order.sql
      -- Counter-example: the same columns, the other way round.
      CREATE INDEX idx_movement_booked_account ON account_movement (booked_at, account_id);
      -- The range column leads, so the tree can only be entered by date: every account's
      -- movements in that range are walked to find one account's rows.
      SELECT * FROM account_movement
      WHERE account_id = 42 AND booked_at >= now() - interval '30 days';
      -- And an index on a column with a handful of values rarely helps on its own:
      CREATE INDEX idx_eft_status ON outgoing_eft (status); -- 'SENT' is almost every row
      Hızlı kontrolBaşlangıç

      İndeks eklemenin bedeli nedir?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar ve EXPLAIN okumak

      Index Scan using idx_orders_customer on orders
      (cost=0.42..8.45 rows=20 width=48)
      (actual time=0.03..0.09 rows=20 loops=1)
      • rows=20 (tahmin) ile rows=20 (gerçek) arasındaki fark. Büyük sapma, istatistiklerin bayatladığını söyler — ANALYZE çalıştırmak gerekir. Kötü plan seçimlerinin en yaygın sebebi budur.
      • loops=1. Büyük bir sayı, iç içe döngüde tekrar tekrar çalışan bir plan demektir.
      • Plan adı değil, satır sayısı. Seq Scan görmek kötü değildir; 20 satır dönerken Seq Scan görmek kötüdür.

      EXPLAIN sorguyu çalıştırmaz, yalnızca planı gösterir. Gerçek süreleri görmek için EXPLAIN ANALYZE gerekir — ve bu sorguyu gerçekten çalıştırır.

      Kendini sına

      Şimşek turu1/5

      İndeks, okunması gereken sayfa sayısını azaltarak işe yarar.

      Soru 1/3Orta

      Bu indeks varken, aşağıdaki sorgunun planı ne olur?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
      plan.sql
      1CREATE INDEX idx_orders ON orders (status, customer_id);
      2
      3EXPLAIN
      4SELECT id, total
      5 FROM orders
      6 WHERE customer_id = 4711;
      SQLUTF-8LF

      Aklında kalacak üç şey

      1. 1 İndeks sorguyu hızlandırmaz, okunacak sayfa sayısını azaltır. Az sayfa okunmayacaksa hiçbir şey kazandırmaz.
      2. 2 Birden çok sütunlu indekste sütun sırası bir tercih değil, şarttır: sorgu ilk sütunu daraltmıyorsa dizinde başlanacak bir yer yoktur.
      3. 3 Veritabanı indeksi üç sebeple kullanmayabilir: indeks yok, ilk sütun kullanılmıyor ya da koşul çok fazla satır getiriyor. Üçünün çözümü farklıdır.
      Sonraki kapı Sınırı 10 saniyede 10 istek koydun. Biri iki saniyede 20 istek geçirebilir mi? Rate Limiting — Kapıdan Saniyede Kaç Kişi Geçer? · 10 dk

      4 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.