Veri Analistleri için SQL: Pencere Fonksiyonları, CTE ve İleri Düzey Sorgular

SQL pencere fonksiyonları (ROW_NUMBER, RANK, LAG/LEAD), Common Table Expressions ve ileri düzey sorgu tekniklerinin kapsamlı rehberi. Veri analisti mülakatları ve günlük çalışmalar için temel bilgiler.

SQL pencere fonksiyonları ve CTE'ler ile veri analizi gösterge panelleri ve sorgu sonuçları

Veri analistleri için SQL bilgisi, özellikle pencere fonksiyonları ve CTE konuları, teknik mülakatlarda en sık karşılaşılan alanlardan birini oluşturmaktadır. Temel SELECT sorgularının ötesine geçen bu ileri SQL kalıpları, büyük veri kümelerinden anlamlı içerikler çıkarmayı ve karmaşık analitik soruları tek bir sorgu ile yanıtlamayı mümkün kılmaktadır. Bu rehber, pencere fonksiyonlarından özyinelemeli CTE'lere, boşluk-ada analizinden koşullu analitik kalıplara kadar her konuyu pratik kod örnekleriyle ele almaktadır.

Hızlı Başvuru

Pencere fonksiyonları OVER() yan tümcesi ile çalışır ve satırları gruplamadan hesaplama yapmayı sağlar. Temel yapı: fonksiyon() OVER (PARTITION BY sütun ORDER BY sütun). GROUP BY'dan farkı, orijinal satırları koruyarak her satıra hesaplanmış değerleri eklemesidir.

SQL Pencere Fonksiyonlarını ve OVER Yan Tümcesini Anlamak

Pencere fonksiyonları, standart toplama fonksiyonlarından farklı olarak satırları daraltmaz. OVER yan tümcesi içerisindeki PARTITION BY ifadesi verileri mantıksal gruplara ayırırken, ORDER BY her grup içerisindeki sıralama düzenini belirler. ROWS BETWEEN ifadesi ise pencere çerçevesini tanımlar ve hangi satırların hesaplamaya dahil edileceğini kontrol eder.

Aşağıdaki örnek, her departman içerisindeki çalışanları maaş sırasına göre sıralar ve kümülatif maaş toplamını hesaplar:

sql
-- window_function_syntax.sql
SELECT
  employee_id,
  department,
  salary,
  -- Ranks employees within each department by salary
  RANK() OVER (
    PARTITION BY department
    ORDER BY salary DESC
  ) AS dept_salary_rank,
  -- Running total of salaries within department
  SUM(salary) OVER (
    PARTITION BY department
    ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_salary_total
FROM employees;

PARTITION BY department her departmanı bağımsız bir pencere olarak tanımlarken, ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ifadesi pencere çerçevesinin başından mevcut satıra kadar olan tüm satırları hesaplamaya dahil eder. Bu yaklaşım, her satırın hem kendi bilgisini hem de departman bazında kümülatif değerlerini aynı anda gösterir.

ROW_NUMBER, RANK ve DENSE_RANK Karşılaştırması

Sıralama fonksiyonları teknik mülakatlarda en çok sorulan konular arasında yer alır. Üç fonksiyonun davranışı, özellikle eşit değerler (ties) söz konusu olduğunda birbirinden ayrılır:

sql
-- ranking_comparison.sql
SELECT
  product_name,
  category,
  revenue,
  ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num,
  RANK()       OVER (ORDER BY revenue DESC) AS rank_val,
  DENSE_RANK() OVER (ORDER BY revenue DESC) AS dense_rank_val
FROM products;

| product_name | revenue | row_num | rank_val | dense_rank_val | |-------------|---------|---------|----------|----------------| | Widget A | 50000 | 1 | 1 | 1 | | Widget B | 50000 | 2 | 1 | 1 | | Widget C | 40000 | 3 | 3 | 2 | | Widget D | 30000 | 4 | 4 | 3 |

ROW_NUMBER eşit değerler dahil her satıra benzersiz bir numara atar. RANK eşit değerlere aynı sırayı verir ancak sonraki sırayı atlar (1, 1, 3). DENSE_RANK ise eşit değerlere aynı sırayı verir ancak boşluk bırakmaz (1, 1, 2). Dönem bazlı sıralama, sayfalama ve en üst N analizi gibi senaryolarda hangi fonksiyonun kullanılacağını bilmek, doğru sonuçlara ulaşmak için kritik öneme sahiptir.

LAG ve LEAD ile Dönemler Arası Analiz

Zaman serisi verileri üzerinde dönemsel karşılaştırmalar yapmak, veri analizinin temel gereksinimlerinden biridir. LAG fonksiyonu önceki satırın değerine erişirken, LEAD fonksiyonu sonraki satırın değerini getirir. Bu iki fonksiyon, büyüme oranları, trend analizi ve anomali tespiti için sıklıkla kullanılır.

sql
-- period_over_period.sql
SELECT
  month,
  revenue,
  -- Previous month revenue
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
  -- Month-over-month growth rate
  ROUND(
    (revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0
    / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0),
    2
  ) AS mom_growth_pct,
  -- Next month revenue (forward-looking)
  LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_sales
ORDER BY month;

LAG(revenue, 1) ifadesi bir önceki ayın gelirini döndürür. NULLIF fonksiyonu sıfıra bölme hatasını önlemek için kullanılır. Hesaplanan mom_growth_pct sütunu, aylık büyüme yüzdesini doğrudan sorgu çıktısında sunar. Bu yaklaşım, ayrı JOIN işlemleri veya alt sorgular yerine tek bir pencere fonksiyonu ile dönemsel karşılaştırmaları mümkün kılar.

NTILE ve Yüzdelik Dilimlendirme

Müşterileri harcama segmentlerine ayırmak veya performans yüzdeliklerini hesaplamak, veri analisti rolünün temel gereksinimlerinden biridir. NTILE(n) fonksiyonu sıralanan satırları n eşit gruba böler ve her satıra 1 ile n arasında bir dilim numarası atar.

sql
-- customer_segmentation.sql
SELECT
  customer_id,
  total_spend,
  -- Split customers into 4 quartiles by spend
  NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile,
  -- Decile scoring for finer granularity
  NTILE(10) OVER (ORDER BY total_spend DESC) AS spend_decile
FROM (
  SELECT
    customer_id,
    SUM(order_total) AS total_spend
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY customer_id
) customer_totals;

NTILE(4) en yüksek harcama yapan müşterileri 1. çeyrekliğe, en düşük harcama yapanları 4. çeyrekliğe atar. NTILE(10) ise daha ince taneli segmentasyon için onluk dilimler oluşturur. Bu yaklaşım pazarlama kampanyalarında hedefleme, müşteri yaşam döngüsü analizi ve RFM skorlaması gibi senaryolarda kullanılır. Satır sayısı eşit bölünmediğinde, NTILE ilk gruplara bir fazla satır atar.

Data Analytics mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Common Table Expressions: İç İçe Alt Sorguların Yerini Alan Yaklaşım

CTE'ler (WITH ifadesi), karmaşık sorgulamayı adımlar halinde yapılandırarak okunabilirliği önemli ölçüde artırır. Her CTE bloğu geçici bir sonuç kümesi tanımlar ve sonraki bloklar bu sonuçlara isim üzerinden erişir. Bu yaklaşım, iç içe geçmiş alt sorguların bakımını zorlaştırdığı durumlarda tercih edilen yöntemdir.

sql
-- cte_sales_analysis.sql
WITH monthly_revenue AS (
  -- Step 1: Aggregate raw orders into monthly totals
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    product_category,
    SUM(amount) AS revenue,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY DATE_TRUNC('month', order_date), product_category
),
ranked_categories AS (
  -- Step 2: Rank categories within each month
  SELECT
    month,
    product_category,
    revenue,
    unique_customers,
    RANK() OVER (
      PARTITION BY month
      ORDER BY revenue DESC
    ) AS category_rank
  FROM monthly_revenue
)
-- Step 3: Final output — top 3 categories per month
SELECT
  month,
  product_category,
  revenue,
  unique_customers,
  category_rank
FROM ranked_categories
WHERE category_rank <= 3
ORDER BY month, category_rank;

İlk CTE (monthly_revenue) ham sipariş verilerini aylık toplamlara dönüştürür. İkinci CTE (ranked_categories) pencere fonksiyonu kullanarak kategorileri sıralar. Son SELECT ifadesi yalnızca ilk üç kategoriyi filtreler. Bu üç katmanlı yaklaşım, her adımın bağımsız olarak test edilebilmesini ve hatanın kolayca izlenebilmesini sağlar.

Özyinelemeli CTE'ler ile Hiyerarşik Veri Sorgulama

Organizasyon şemaları, ürün kategorileri veya dosya sistemi yapıları gibi ağaç şeklindeki veriler, özyinelemeli CTE'ler ile sorgulanır. WITH RECURSIVE ifadesi bir temel durum (base case) ve tekrar eden durum (recursive case) olmak üzere iki bölümden oluşur.

sql
-- recursive_org_chart.sql
WITH RECURSIVE org_hierarchy AS (
  -- Base case: top-level managers (no manager above them)
  SELECT
    employee_id,
    employee_name,
    manager_id,
    1 AS depth,
    employee_name AS management_chain
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive case: join employees to their managers
  SELECT
    e.employee_id,
    e.employee_name,
    e.manager_id,
    oh.depth + 1,
    oh.management_chain || ' > ' || e.employee_name
  FROM employees e
  INNER JOIN org_hierarchy oh ON e.manager_id = oh.employee_id
)
SELECT
  employee_id,
  employee_name,
  depth,
  management_chain
FROM org_hierarchy
ORDER BY management_chain;

Temel durum, manager_id IS NULL koşuluyla üst düzey yöneticileri seçer. Tekrar eden durum, her çalışanı yöneticisinin satırına birleştirerek hiyerarşiyi seviye seviye oluşturur. management_chain sütunu, kökten yaprak düğüme kadar tüm yönetim zincirini tek bir metin olarak gösterir.

Özyinelemeli CTE Performans Uyarısı

Özyinelemeli CTE'ler derin hiyerarşilerde performans sorunlarına yol açabilir. Sonsuz döngü riskini azaltmak için depth kontrolü eklemek (WHERE depth < 20) önemlidir. Büyük veri kümelerinde LIMIT kullanılmalı ve özyineleme derinliği izlenmelidir. PostgreSQL varsayılan olarak 100 iterasyon sınırı uygular.

İleri Analitik Kalıplar: Boşluklar, Adalar ve Kümülatif Toplamlar

Boşluk-ada (gaps and islands) problemi, ardışık kayıtlardaki sürekliliği tespit etmek için kullanılan klasik bir SQL kalıbıdır. Bu kalıp, ardışık giriş yapan kullanıcılar, kesintisiz çalışma serileri veya art arda satış yapılan günler gibi senaryolarda uygulanır.

sql
-- consecutive_login_streaks.sql
WITH login_days AS (
  SELECT DISTINCT
    user_id,
    DATE(login_timestamp) AS login_date
  FROM user_sessions
),
streaks AS (
  SELECT
    user_id,
    login_date,
    -- Subtracting row_number from date creates a constant for consecutive days
    login_date - (ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY login_date
    ))::int AS streak_group
  FROM login_days
)
SELECT
  user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*) AS streak_length
FROM streaks
GROUP BY user_id, streak_group
HAVING COUNT(*) >= 3
ORDER BY streak_length DESC;

Tekniğin özünde, ROW_NUMBER ile tarih arasındaki fark sabit bir değer üretir. Ardışık günlerde bu fark değişmezken, boşluk olduğunda değer değişir. Bu sabit değer streak_group olarak kullanılarak ardışık günler gruplanır. HAVING COUNT(*) >= 3 filtresi yalnızca en az 3 gün süren serileri döndürür.

Pencere Fonksiyonlarını CASE ile Birleştirerek Koşullu Analitik

Pencere fonksiyonları CASE ifadesiyle birleştiğinde, hareketli ortalamalar, çeyreklik kümülatif toplamlar ve aykırı değer tespiti gibi karmaşık analitik kalıplar oluşturulabilir. Bu yaklaşım, farklı zaman dilimlerinde ve koşullarda eşzamanlı hesaplamalar yapmayı sağlar.

sql
-- conditional_analytics.sql
SELECT
  order_date,
  product_category,
  amount,
  -- 7-day moving average
  AVG(amount) OVER (
    PARTITION BY product_category
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d,
  -- Cumulative sum resetting each quarter
  SUM(amount) OVER (
    PARTITION BY product_category, DATE_TRUNC('quarter', order_date)
    ORDER BY order_date
  ) AS qtd_cumulative,
  -- Flag if current row exceeds 2x the category average
  CASE
    WHEN amount > 2 * AVG(amount) OVER (PARTITION BY product_category)
    THEN 'OUTLIER'
    ELSE 'NORMAL'
  END AS outlier_flag
FROM daily_sales
ORDER BY product_category, order_date;

moving_avg_7d son 7 günün ortalamasını hesaplar ve kısa vadeli trend analizi için kullanılır. qtd_cumulative her çeyrekte sıfırlanarak çeyreklik kümülatif toplamı gösterir; bunun için PARTITION BY ifadesine DATE_TRUNC('quarter', order_date) eklenir. outlier_flag ise mevcut satırın değerini kategori genelindeki ortalama ile karşılaştırarak 2 kattan fazla olan değerleri OUTLIER olarak işaretler.

Pencere Fonksiyonlarının Çalışma Sırası

SQL sorgularında pencere fonksiyonları WHERE, GROUP BY ve HAVING işlemlerinden sonra, ORDER BY ve LIMIT işlemlerinden önce çalışır. Bu nedenle pencere fonksiyonları WHERE yan tümcesi içerisinde doğrudan kullanılamaz. Filtreleme gerektiğinde, pencere fonksiyonunu bir CTE veya alt sorgu içerisine yerleştirmek ve dış sorguda filtrelemek gerekir.

Analitik Sorgularda Performans Optimizasyonu

Aynı pencere tanımı birden fazla fonksiyonda tekrarlandığında, WINDOW yan tümcesi ile adlandırılmış pencereler tanımlanabilir. Bu yaklaşım hem okunabilirliği artırır hem de sorgu iyileştiricisinin (query optimizer) aynı pencereyi yeniden hesaplamamasını sağlar.

sql
-- named_window.sql
SELECT
  department,
  employee_id,
  salary,
  RANK() OVER dept_window AS dept_rank,
  SUM(salary) OVER dept_window AS dept_total,
  AVG(salary) OVER dept_window AS dept_avg
FROM employees
WINDOW dept_window AS (
  PARTITION BY department
  ORDER BY salary DESC
);

WINDOW dept_window AS (...) ifadesi pencere tanımını bir kez yapar ve üç farklı fonksiyon aynı tanımı paylaşır. Performans için ek olarak şu noktalar dikkate alınmalıdır: PARTITION BY ve ORDER BY sütunlarında uygun indeksler oluşturmak sorgu süresini önemli ölçüde kısaltır. Gereksiz yere geniş pencere çerçeveleri kullanmaktan kaçınmak bellek tüketimini azaltır. Büyük veri kümelerinde CTE ile ön filtreleme yaparak pencere fonksiyonuna aktarılan satır sayısını sınırlamak etkili bir optimizasyon stratejisidir.

Data Analytics mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Sonuç

SQL pencere fonksiyonları ve CTE'ler, veri analistlerinin karmaşık soruları verimli ve okunabilir sorgularla yanıtlamasını sağlayan temel araçlardır. Bu rehberde ele alınan konuların özeti:

  • RANK, ROW_NUMBER ve DENSE_RANK farklılıklarını bilmek, eşit değerlerin olduğu sıralama senaryolarında doğru sonuçlara ulaşmak için zorunludur
  • LAG ve LEAD fonksiyonları, JOIN işlemleri olmadan dönemsel karşılaştırmalar yapmanın en verimli yoludur
  • NTILE ile müşteri segmentasyonu ve yüzdelik dilimlendirme işlemleri tek bir sorgu ile gerçekleştirilir
  • CTE'ler karmaşık sorgulamaları adım adım yapılandırarak hata ayıklamayı ve bakımı kolaylaştırır
  • Özyinelemeli CTE'ler hiyerarşik verileri sorgulamada vazgeçilmez bir araçtır ancak derinlik kontrolü ile birlikte kullanılmalıdır
  • Boşluk-ada kalıpları ROW_NUMBER ve tarih aritmetiği kombinasyonu ile ardışık kayıtları tespit eder
  • Adlandırılmış pencereler (WINDOW) tekrar eden tanımları ortadan kaldırarak hem okunabilirliği hem de performansı iyileştirir

Tüm bu kalıplar, pencere fonksiyonları ve CTE bilgisini pratikte uygulayarak pekiştirmek için Data Analytics pratik alıştırmaları üzerinden çalışılabilir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#sql
#data-analytics
#window-functions
#cte
#interview

Paylaş

İlgili makaleler