Son gün! Profesyonel T-SQL geliştirmenin vazgeçilmez araçlarına bakıyoruz: stored procedure’ler, window functions ve performans.

1. Stored Procedures

Stored procedure, veritabanında saklanan, parametre alabilen, yeniden kullanılabilir SQL koddur.

Temel Yapı

CREATE PROCEDURE sp_GetEmployeesByDepartment
    @Department NVARCHAR(50)
AS
BEGIN
    SET NOCOUNT ON;

    SELECT Name, Salary, HireDate
    FROM Employees
    WHERE Department = @Department
    ORDER BY Salary DESC;
END;

Çağırma

EXEC sp_GetEmployeesByDepartment @Department = 'IT';
-- ya da kısa:
EXEC sp_GetEmployeesByDepartment 'IT';

Birden Fazla Parametre

CREATE PROCEDURE sp_GetEmployees
    @Department NVARCHAR(50) = NULL,        -- opsiyonel (default NULL)
    @MinSalary DECIMAL(10, 2) = 0,
    @MaxSalary DECIMAL(10, 2) = 999999
AS
BEGIN
    SET NOCOUNT ON;

    SELECT *
    FROM Employees
    WHERE (@Department IS NULL OR Department = @Department)
      AND Salary BETWEEN @MinSalary AND @MaxSalary;
END;
EXEC sp_GetEmployees @MinSalary = 5000, @MaxSalary = 10000;
EXEC sp_GetEmployees @Department = 'IT';

OUTPUT Parametre

CREATE PROCEDURE sp_AddEmployee
    @Name NVARCHAR(100),
    @Department NVARCHAR(50),
    @NewId INT OUTPUT
AS
BEGIN
    INSERT INTO Employees (Name, Department)
    VALUES (@Name, @Department);

    SET @NewId = SCOPE_IDENTITY();
END;
DECLARE @Id INT;
EXEC sp_AddEmployee @Name = 'Ejder', @Department = 'IT', @NewId = @Id OUTPUT;
PRINT @Id;

Stored Procedure Avantajları

2. User-Defined Functions

Scalar Function — Tek Değer Döner

CREATE FUNCTION fn_GetEmployeeFullName (@FirstName NVARCHAR(50), @LastName NVARCHAR(50))
RETURNS NVARCHAR(101)
AS
BEGIN
    RETURN CONCAT(@FirstName, ' ', @LastName);
END;
SELECT dbo.fn_GetEmployeeFullName('Ali', 'Yılmaz');  -- 'Ali Yılmaz'

Table-Valued Function — Tablo Döner

Inline TVF (genelde tercih edilen):

CREATE FUNCTION fn_GetDepartmentEmployees (@Dept NVARCHAR(50))
RETURNS TABLE
AS
RETURN (
    SELECT Id, Name, Salary
    FROM Employees
    WHERE Department = @Dept
);
SELECT * FROM dbo.fn_GetDepartmentEmployees('IT');
-- Hatta JOIN'le bile kullanabilirsin

Function vs Stored Procedure: Function SELECT içinde kullanılabilir, SP kullanılamaz. SP veri değiştirebilir, function genelde sadece okur.

3. Trigger’lar

Trigger’lar, bir tablo üzerinde INSERT / UPDATE / DELETE olduğunda otomatik tetiklenen SP’lerdir.

AFTER Trigger (post-event)

CREATE TRIGGER trg_Employees_AuditUpdate
ON Employees
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO EmployeeAudit (EmployeeId, OldSalary, NewSalary, ChangedAt)
    SELECT
        i.Id,
        d.Salary,         -- DELETED: eski hali
        i.Salary,         -- INSERTED: yeni hali
        GETDATE()
    FROM inserted AS i
    INNER JOIN deleted AS d ON i.Id = d.Id
    WHERE i.Salary <> d.Salary;
END;

inserted ve deleted sanal tabloları:

INSTEAD OF Trigger

Asıl işlemi yerine geçer:

CREATE TRIGGER trg_Employees_PreventDelete
ON Employees
INSTEAD OF DELETE
AS
BEGIN
    -- Silmek yerine soft-delete
    UPDATE Employees
    SET Active = 0, DeletedAt = GETDATE()
    WHERE Id IN (SELECT Id FROM deleted);
END;

Trigger’lar güçlü ama kötüye kullanılırsa debugging cehenneme döner. Görünmez yan etkiler oluşturur. Şüphede kalırsan SP kullan.

4. Window Functions — Modern T-SQL’in En İyi Özelliği

Aggregate gibi davranır ama satırları gruplamaz — her satır için “penceredeki” hesabı verir.

ROW_NUMBER()

Sıralı numara verir:

SELECT
    Name,
    Department,
    Salary,
    ROW_NUMBER() OVER (ORDER BY Salary DESC) AS Rank
FROM Employees;

Partition ile (Grup İçinde Sıralama)

SELECT
    Name,
    Department,
    Salary,
    ROW_NUMBER() OVER (
        PARTITION BY Department
        ORDER BY Salary DESC
    ) AS DeptRank
FROM Employees;

Sonuç: Her departmandaki çalışanlara kendi departmanı içinde sıra numarası verir. Bu klasik “her gruptaki en yüksek N kayıt” problemini çözer:

WITH Ranked AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS rn
    FROM Employees
)
SELECT * FROM Ranked WHERE rn <= 3;
-- Her departmandaki en yüksek 3 maaşlı

RANK() ve DENSE_RANK()

Eşit değerlere aynı sıra:

SELECT
    Name,
    Salary,
    RANK() OVER (ORDER BY Salary DESC) AS R,          -- eşitlerde gap
    DENSE_RANK() OVER (ORDER BY Salary DESC) AS DR    -- gap yok
FROM Employees;

Örnek: Maaşlar [10000, 10000, 8000, 7000] ise:

LAG / LEAD — Önceki / Sonraki Satır

SELECT
    OrderDate,
    Amount,
    LAG(Amount, 1) OVER (ORDER BY OrderDate) AS PrevAmount,
    LEAD(Amount, 1) OVER (ORDER BY OrderDate) AS NextAmount,
    Amount - LAG(Amount, 1) OVER (ORDER BY OrderDate) AS DiffFromPrev
FROM Orders;

Trend analizi, dönem karşılaştırması için ideal.

Aggregate Window Functions

SELECT
    Name,
    Department,
    Salary,
    AVG(Salary) OVER (PARTITION BY Department) AS DeptAvg,
    Salary - AVG(Salary) OVER (PARTITION BY Department) AS DiffFromAvg,
    SUM(Salary) OVER (ORDER BY Id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal
FROM Employees;

Running total (kümülatif toplam) — finansal raporlarda klasik.

5. Query Plan ve Performans

Execution Plan Görmek

SET STATISTICS IO ON;
SET STATISTICS TIME ON;

SELECT * FROM Orders WHERE CustomerId = 42;

-- Ya da SSMS'de: Ctrl+M ile "Include Actual Execution Plan"

Common Anti-patterns

SELECT * — Sadece ihtiyacın olan sütunları çek:

-- Kötü
SELECT * FROM Orders;
-- İyi
SELECT Id, OrderDate, Amount FROM Orders;

WHERE’de fonksiyon — Index’i kullanılamaz hale getirir:

-- Kötü (index kullanamaz)
WHERE YEAR(OrderDate) = 2024

-- İyi (index kullanır)
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'

%text% — Index baştan kullanılamaz:

-- Yavaş
WHERE Name LIKE '%ahmet%'
-- Daha hızlı
WHERE Name LIKE 'ahmet%'

N+1 problem — Loop içinde sorgu. Tek JOIN ile çöz.

Stored Procedure Plan Cache

-- Plan cache'i temizle (sadece test ortamında!)
DBCC FREEPROCCACHE;

-- Bir SP'nin parametrelerle nasıl plan oluşturduğunu görmek için:
EXEC sp_GetEmployees @Department = 'IT';
-- Plan cache'lenir, sonraki çağrılar aynı planı kullanır

sp_executesql ile Dynamic SQL

DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM Employees WHERE Department = @dept';
EXEC sp_executesql @sql, N'@dept NVARCHAR(50)', @dept = 'IT';

EXEC(@sql) yerine sp_executesql kullan — parametre cache’lenir, SQL injection güvenliği var.

Mini Proje: Aylık Satış Raporu

-- 1. Aylık satış toplamı + bir önceki aydan değişim
SELECT
    FORMAT(OrderDate, 'yyyy-MM') AS Month,
    SUM(Amount) AS Total,
    LAG(SUM(Amount)) OVER (ORDER BY FORMAT(OrderDate, 'yyyy-MM')) AS PrevMonth,
    SUM(Amount) - LAG(SUM(Amount)) OVER (ORDER BY FORMAT(OrderDate, 'yyyy-MM')) AS Change
FROM Orders
WHERE OrderDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY FORMAT(OrderDate, 'yyyy-MM')
ORDER BY Month;

-- 2. Top 5 müşteri (geçen ayın)
WITH MonthlyTotals AS (
    SELECT
        CustomerId,
        SUM(Amount) AS Total,
        ROW_NUMBER() OVER (ORDER BY SUM(Amount) DESC) AS rn
    FROM Orders
    WHERE OrderDate >= DATEADD(MONTH, -1, GETDATE())
    GROUP BY CustomerId
)
SELECT c.Name, m.Total
FROM MonthlyTotals AS m
INNER JOIN Customers AS c ON c.Id = m.CustomerId
WHERE m.rn <= 5;

-- 3. Departman bazlı satış payı (her departmanın yüzdesi)
SELECT
    Department,
    SUM(o.Amount) AS Total,
    SUM(o.Amount) * 100.0 / SUM(SUM(o.Amount)) OVER () AS PercentOfTotal
FROM Employees AS e
INNER JOIN Orders AS o ON o.EmployeeId = e.Id
WHERE o.OrderDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY Department;

🎉 Tebrikler!

5 günlük T-SQL BootCamp’ini başarıyla tamamladın!

Artık:

Sonraki Adımlar

T-SQL öğrenme yolculuğun burada bitmiyor. SQL Server’ın derinliklerinde keşfedecek çok şey var. Mutlu sorgulamalar! ⛃