Tek tablodan veri çekmek kolay. Asıl güç, birden fazla tabloyu ilişkilendirip anlamlı sorular sormakta. Bu gün, T-SQL’in en güçlü özelliği: JOIN’ler.

Örnek Şema

CREATE TABLE Departments (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(50),
    Budget DECIMAL(12, 2)
);

CREATE TABLE Employees (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(100),
    DepartmentId INT FOREIGN KEY REFERENCES Departments(Id),
    ManagerId INT FOREIGN KEY REFERENCES Employees(Id),  -- self-reference
    Salary DECIMAL(10, 2)
);

CREATE TABLE Orders (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    EmployeeId INT FOREIGN KEY REFERENCES Employees(Id),
    Amount DECIMAL(10, 2),
    OrderDate DATE
);

1. INNER JOIN — Kesişim

İki tabloda eşleşen kayıtları döner. NULL satırlar dışarda:

SELECT e.Name, d.Name AS DeptName, e.Salary
FROM Employees AS e
INNER JOIN Departments AS d
    ON e.DepartmentId = d.Id;

Sadece hem Employees’de hem Departments’da eşleşen satırlar gelir.

Employees                Departments
┌────┬──────┬──────┐    ┌────┬─────────┐
│ 1  │ Ali  │  10  │    │ 10 │ IT      │
│ 2  │ Ayşe │  20  │    │ 20 │ HR      │
│ 3  │ M.   │ NULL │    │ 30 │ Sales   │  (no match)
└────┴──────┴──────┘    └────┴─────────┘

INNER JOIN sonucu:
┌──────┬────────┐
│ Ali  │ IT     │
│ Ayşe │ HR     │
└──────┴────────┘
(Mehmet ve Sales dışarda)

2. LEFT JOIN — Sol Tabloyu Korur

Sol tablonun tüm satırları gelir, sağdan eşleşmeyen NULL olur:

SELECT e.Name, d.Name AS DeptName
FROM Employees AS e
LEFT JOIN Departments AS d
    ON e.DepartmentId = d.Id;
Sonuç:
┌──────┬────────┐
│ Ali  │ IT     │
│ Ayşe │ HR     │
│ M.   │ NULL   │  ← departmansız çalışan da gelir
└──────┴────────┘

Klasik kullanım: “Sipariş VERMEMİŞ müşterileri bul”:

SELECT c.Name
FROM Customers c
LEFT JOIN Orders o ON c.Id = o.CustomerId
WHERE o.Id IS NULL;

3. RIGHT JOIN — Sağ Tabloyu Korur

LEFT’in tersi. Aynı sonucu LEFT JOIN ile de yazabilirsin (tabloları değiştirerek), bu yüzden pratikte çok az kullanılır.

SELECT e.Name, d.Name
FROM Employees AS e
RIGHT JOIN Departments AS d ON e.DepartmentId = d.Id;

4. FULL OUTER JOIN — İkisini de Koru

İki tablodaki tüm satırlar, eşleşmeyenler NULL:

SELECT e.Name, d.Name AS DeptName
FROM Employees AS e
FULL OUTER JOIN Departments AS d
    ON e.DepartmentId = d.Id;

5. Çoklu JOIN

Birden fazla tablo zincirleyebilirsin:

SELECT
    e.Name AS Employee,
    d.Name AS Department,
    m.Name AS Manager
FROM Employees AS e
INNER JOIN Departments AS d ON e.DepartmentId = d.Id
LEFT JOIN Employees AS m ON e.ManagerId = m.Id;

6. Self JOIN

Bir tabloyu kendisiyle birleştirme. Yöneticileri bulmak için:

SELECT
    e.Name AS Employee,
    m.Name AS Manager
FROM Employees AS e
LEFT JOIN Employees AS m ON e.ManagerId = m.Id;

Alias zorunlu — aynı tablo iki kez FROM’da olduğu için.

7. Subquery (Alt Sorgu)

Bir sorgunun içine başka sorgu:

Scalar Subquery (Tek Değer)

SELECT Name, Salary
FROM Employees
WHERE Salary > (SELECT AVG(Salary) FROM Employees);
-- Ortalama maaşın üstündekiler

Liste Subquery

SELECT Name
FROM Employees
WHERE DepartmentId IN (
    SELECT Id FROM Departments WHERE Budget > 100000
);

Korelasyonlu Subquery

Dış sorguyla ilişkili alt sorgu — her dış satır için ayrı çalışır:

SELECT Name, Salary
FROM Employees AS e
WHERE Salary > (
    SELECT AVG(Salary)
    FROM Employees
    WHERE DepartmentId = e.DepartmentId  -- dış sorguya referans
);
-- Departman ortalamasının üstündekiler

8. EXISTS — En Verimli Filtre

EXISTS, alt sorgunun herhangi bir satır döndürüp döndürmediğine bakar. IN’den çoğu zaman daha hızlıdır:

SELECT Name FROM Customers c
WHERE EXISTS (
    SELECT 1 FROM Orders o
    WHERE o.CustomerId = c.Id
);
-- Hiç sipariş veren müşteriler

NOT EXISTS

SELECT Name FROM Customers c
WHERE NOT EXISTS (
    SELECT 1 FROM Orders o
    WHERE o.CustomerId = c.Id
);
-- Hiç sipariş vermemiş müşteriler

9. CTE — Common Table Expression

CTE, sorgu içinde isimlendirilmiş geçici tablo. Karmaşık sorguları okunabilir hale getirir:

WITH DeptStats AS (
    SELECT
        DepartmentId,
        AVG(Salary) AS AvgSal,
        COUNT(*) AS HeadCount
    FROM Employees
    GROUP BY DepartmentId
)
SELECT d.Name, s.AvgSal, s.HeadCount
FROM DeptStats AS s
INNER JOIN Departments AS d ON s.DepartmentId = d.Id
WHERE s.HeadCount > 5;

Birden Fazla CTE

WITH
    HighEarners AS (
        SELECT * FROM Employees WHERE Salary > 10000
    ),
    BigDepts AS (
        SELECT Id FROM Departments WHERE Budget > 100000
    )
SELECT e.Name
FROM HighEarners AS e
INNER JOIN BigDepts AS d ON e.DepartmentId = d.Id;

Recursive CTE

Hiyerarşi sorguları için (organizasyon ağacı, kategori ağacı):

WITH OrgChart AS (
    -- Anchor: CEO (manager yok)
    SELECT Id, Name, ManagerId, 0 AS Level
    FROM Employees
    WHERE ManagerId IS NULL

    UNION ALL

    -- Recursive: her seviye sonraki seviyeyi getirir
    SELECT e.Id, e.Name, e.ManagerId, oc.Level + 1
    FROM Employees AS e
    INNER JOIN OrgChart AS oc ON e.ManagerId = oc.Id
)
SELECT * FROM OrgChart
ORDER BY Level, Name;

Pratik Sorular

Soru 1: Her departmandaki çalışan sayısı (departman ismiyle).

SELECT d.Name, COUNT(e.Id) AS EmployeeCount
FROM Departments AS d
LEFT JOIN Employees AS e ON e.DepartmentId = d.Id
GROUP BY d.Name;

Soru 2: En yüksek maaşlı 3 çalışan ve departmanları.

SELECT TOP 3
    e.Name,
    d.Name AS Department,
    e.Salary
FROM Employees AS e
INNER JOIN Departments AS d ON e.DepartmentId = d.Id
ORDER BY e.Salary DESC;

Soru 3: Hiç sipariş VERMEMİŞ çalışanları bul.

SELECT e.Name
FROM Employees AS e
LEFT JOIN Orders AS o ON o.EmployeeId = e.Id
WHERE o.Id IS NULL;

Soru 4: Her çalışanın kendi departman ortalamasından yüksek mi düşük mü maaş aldığı.

WITH DeptAvg AS (
    SELECT DepartmentId, AVG(Salary) AS AvgSal
    FROM Employees
    GROUP BY DepartmentId
)
SELECT
    e.Name,
    e.Salary,
    da.AvgSal,
    CASE
        WHEN e.Salary > da.AvgSal THEN 'Yüksek'
        WHEN e.Salary < da.AvgSal THEN 'Düşük'
        ELSE 'Ortalama'
    END AS Status
FROM Employees AS e
INNER JOIN DeptAvg AS da ON e.DepartmentId = da.DepartmentId;

Soru 5: Yöneticilerin altındaki toplam kişi sayısı (1 seviye).

SELECT
    m.Name AS Manager,
    COUNT(e.Id) AS Reports
FROM Employees AS m
LEFT JOIN Employees AS e ON e.ManagerId = m.Id
GROUP BY m.Name
HAVING COUNT(e.Id) > 0;

Günün Özeti

Bugün ilişkisel sorgu sanatını öğrendik:

Yarın Gün 4’te veri manipülasyonuna geçiyoruz — INSERT, UPDATE, DELETE, transaction’lar ve constraint’ler.