Lý thuyết
Bài 3/10

SQL JOINs

INNER, LEFT, RIGHT, FULL JOIN để kết nối dữ liệu từ nhiều bảng

Bài 3: SQL JOINs

Database Connections

1. Tại sao cần JOIN?

Trong Relational Database, data được chia thành nhiều tables để:

  • Tránh lặp dữ liệu (Normalization)
  • Đảm bảo data integrity
  • Tối ưu storage

JOIN kết nối các tables liên quan để trả lời business questions.

Ví dụ với AdventureWorks

Text
1Question: "Tên và email của khách hàng nào đã đặt hàng?"
2
3Data nằm ở nhiều tables:
4┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
5│ Person.Person │ │ Sales.Customer │ │ Sales.Sales │
6│ • FirstName │◄───│ • PersonID │◄───│ OrderHeader │
7│ • LastName │ │ • CustomerID │ │ • CustomerID │
8│ • EmailPromo │ │ │ │ • OrderDate │
9└─────────────────┘ └─────────────────┘ └─────────────────┘
10
11→ Cần JOIN để kết nối chúng!

2. JOIN Types Overview

Text
1┌─────────────────────────────────────────────────────────────┐
2│ JOIN Types │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ INNER JOIN LEFT JOIN RIGHT JOIN │
6│ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ │
7│ │ A │ B │ │ A │ B │ │ A │ B │ │
8│ │░░░│░░░│ │███│░░░│ │░░░│███│ │
9│ └───┴───┘ └───┴───┘ └───┴───┘ │
10│ Chỉ matching Tất cả A + Tất cả B + │
11│ matching B matching A │
12│ │
13│ FULL OUTER JOIN │
14│ ┌───┬───┐ │
15│ │ A │ B │ │
16│ │███│███│ │
17│ └───┴───┘ │
18│ Tất cả từ cả 2 bảng │
19└─────────────────────────────────────────────────────────────┘

3. INNER JOIN

3.1 Concept

Trả về chỉ những rows có match ở cả 2 tables.

SQL
1SELECT columns
2FROM table1
3INNER JOIN table2 ON table1.key = table2.key;

3.2 Examples với AdventureWorks

Basic INNER JOIN:

SQL
1-- Employees và Department hiện tại
2SELECT
3 e.BusinessEntityID,
4 e.JobTitle,
5 d.Name AS Department
6FROM HumanResources.Employee e
7INNER JOIN HumanResources.EmployeeDepartmentHistory edh
8 ON e.BusinessEntityID = edh.BusinessEntityID
9INNER JOIN HumanResources.Department d
10 ON edh.DepartmentID = d.DepartmentID
11WHERE edh.EndDate IS NULL; -- Current department only

Products với Category:

SQL
1-- Products và danh mục của chúng
2SELECT
3 p.Name AS ProductName,
4 p.ListPrice,
5 ps.Name AS Subcategory,
6 pc.Name AS Category
7FROM Production.Product p
8INNER JOIN Production.ProductSubcategory ps
9 ON p.ProductSubcategoryID = ps.ProductSubcategoryID
10INNER JOIN Production.ProductCategory pc
11 ON ps.ProductCategoryID = pc.ProductCategoryID
12ORDER BY pc.Name, ps.Name, p.Name;

Sales Orders với Customer Info:

SQL
1-- Đơn hàng với thông tin khách hàng
2SELECT
3 soh.SalesOrderID,
4 soh.OrderDate,
5 soh.TotalDue,
6 p.FirstName + ' ' + p.LastName AS CustomerName
7FROM Sales.SalesOrderHeader soh
8INNER JOIN Sales.Customer c ON soh.CustomerID = c.CustomerID
9INNER JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
10ORDER BY soh.OrderDate DESC;

3.3 INNER JOIN với Aggregation

SQL
1-- Doanh thu theo Category
2SELECT
3 pc.Name AS Category,
4 COUNT(DISTINCT sod.SalesOrderID) AS OrderCount,
5 SUM(sod.LineTotal) AS TotalRevenue
6FROM Sales.SalesOrderDetail sod
7INNER JOIN Production.Product p ON sod.ProductID = p.ProductID
8INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
9INNER JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
10GROUP BY pc.Name
11ORDER BY TotalRevenue DESC;

4. LEFT JOIN (LEFT OUTER JOIN)

4.1 Concept

Trả về tất cả rows từ bảng LEFT + matching rows từ bảng RIGHT. Rows không match sẽ có NULL ở các cột của bảng RIGHT.

SQL
1SELECT columns
2FROM table1
3LEFT JOIN table2 ON table1.key = table2.key;

4.2 Examples

Tất cả Products, kể cả không có Subcategory:

SQL
1SELECT
2 p.Name AS ProductName,
3 p.ListPrice,
4 ps.Name AS Subcategory
5FROM Production.Product p
6LEFT JOIN Production.ProductSubcategory ps
7 ON p.ProductSubcategoryID = ps.ProductSubcategoryID;

Tìm Products không có Subcategory:

SQL
1-- Products chưa được phân loại
2SELECT
3 p.ProductID,
4 p.Name AS ProductName,
5 p.ListPrice
6FROM Production.Product p
7LEFT JOIN Production.ProductSubcategory ps
8 ON p.ProductSubcategoryID = ps.ProductSubcategoryID
9WHERE ps.ProductSubcategoryID IS NULL;

Tất cả Customers, kể cả chưa đặt hàng:

SQL
1SELECT
2 c.CustomerID,
3 p.FirstName + ' ' + p.LastName AS CustomerName,
4 COUNT(soh.SalesOrderID) AS OrderCount,
5 ISNULL(SUM(soh.TotalDue), 0) AS TotalSpent
6FROM Sales.Customer c
7LEFT JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
8LEFT JOIN Sales.SalesOrderHeader soh ON c.CustomerID = soh.CustomerID
9GROUP BY c.CustomerID, p.FirstName, p.LastName
10ORDER BY TotalSpent DESC;

4.3 Tìm rows không match (Anti-Join)

LEFT JOIN + IS NULL = Anti-Join

Pattern phổ biến để tìm records "orphan" hoặc records không có trong bảng khác.

SQL
1-- Employees KHÔNG có Sales Orders (không phải Salesperson)
2SELECT
3 e.BusinessEntityID,
4 p.FirstName + ' ' + p.LastName AS EmployeeName,
5 e.JobTitle
6FROM HumanResources.Employee e
7LEFT JOIN Person.Person p ON e.BusinessEntityID = p.BusinessEntityID
8LEFT JOIN Sales.SalesPerson sp ON e.BusinessEntityID = sp.BusinessEntityID
9WHERE sp.BusinessEntityID IS NULL;

5. RIGHT JOIN (RIGHT OUTER JOIN)

5.1 Concept

Trả về tất cả rows từ bảng RIGHT + matching rows từ bảng LEFT. Ngược với LEFT JOIN.

SQL
1SELECT columns
2FROM table1
3RIGHT JOIN table2 ON table1.key = table2.key;
RIGHT JOIN vs LEFT JOIN

RIGHT JOIN ít được sử dụng vì bạn có thể đổi thứ tự tables và dùng LEFT JOIN thay thế. Kết quả tương đương.

5.2 Example

SQL
1-- Tương đương với LEFT JOIN, chỉ đổi thứ tự
2-- Tất cả Departments, kể cả không có Employees
3SELECT
4 d.Name AS Department,
5 COUNT(edh.BusinessEntityID) AS EmployeeCount
6FROM HumanResources.EmployeeDepartmentHistory edh
7RIGHT JOIN HumanResources.Department d
8 ON edh.DepartmentID = d.DepartmentID
9 AND edh.EndDate IS NULL
10GROUP BY d.DepartmentID, d.Name
11ORDER BY EmployeeCount;

6. FULL OUTER JOIN

6.1 Concept

Trả về tất cả rows từ CẢ HAI bảng, match nếu có, NULL nếu không.

SQL
1SELECT columns
2FROM table1
3FULL OUTER JOIN table2 ON table1.key = table2.key;

6.2 Example

SQL
1-- So sánh Products có Inventory vs không có
2SELECT
3 p.ProductID,
4 p.Name AS ProductName,
5 pi.LocationID,
6 ISNULL(pi.Quantity, 0) AS InventoryQty
7FROM Production.Product p
8FULL OUTER JOIN Production.ProductInventory pi
9 ON p.ProductID = pi.ProductID
10ORDER BY pi.Quantity DESC;

7. Self JOIN

7.1 Concept

JOIN một table với chính nó. Hữu ích cho hierarchical data.

7.2 Example: Employee Hierarchy

SQL
1-- Employees và Managers của họ
2SELECT
3 e.BusinessEntityID AS EmployeeID,
4 ep.FirstName + ' ' + ep.LastName AS EmployeeName,
5 e.JobTitle AS EmployeeTitle,
6 m.BusinessEntityID AS ManagerID,
7 mp.FirstName + ' ' + mp.LastName AS ManagerName,
8 m.JobTitle AS ManagerTitle
9FROM HumanResources.Employee e
10LEFT JOIN HumanResources.Employee m
11 ON e.OrganizationNode.GetAncestor(1) = m.OrganizationNode
12LEFT JOIN Person.Person ep ON e.BusinessEntityID = ep.BusinessEntityID
13LEFT JOIN Person.Person mp ON m.BusinessEntityID = mp.BusinessEntityID
14ORDER BY e.OrganizationLevel, e.BusinessEntityID;

8. Multiple JOINs

8.1 Joining nhiều tables

SQL
1-- Full Sales Order với tất cả thông tin
2SELECT
3 soh.SalesOrderNumber,
4 soh.OrderDate,
5 p.FirstName + ' ' + p.LastName AS CustomerName,
6 a.City,
7 sp.Name AS StateProvince,
8 st.Name AS SalesTerritory,
9 prod.Name AS ProductName,
10 sod.OrderQty,
11 sod.LineTotal
12FROM Sales.SalesOrderHeader soh
13-- Customer info
14JOIN Sales.Customer c ON soh.CustomerID = c.CustomerID
15JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
16-- Address info
17JOIN Person.BusinessEntityAddress bea ON c.PersonID = bea.BusinessEntityID
18JOIN Person.Address a ON bea.AddressID = a.AddressID
19JOIN Person.StateProvince sp ON a.StateProvinceID = sp.StateProvinceID
20-- Territory
21JOIN Sales.SalesTerritory st ON soh.TerritoryID = st.TerritoryID
22-- Order details
23JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
24JOIN Production.Product prod ON sod.ProductID = prod.ProductID
25WHERE soh.OrderDate >= '2014-01-01'
26ORDER BY soh.OrderDate DESC, soh.SalesOrderNumber;

8.2 Mixed JOIN Types

SQL
1-- Tất cả Products với Sales info (nếu có)
2SELECT
3 p.Name AS ProductName,
4 pc.Name AS Category,
5 ISNULL(SUM(sod.OrderQty), 0) AS TotalSold,
6 ISNULL(SUM(sod.LineTotal), 0) AS TotalRevenue
7FROM Production.Product p
8LEFT JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
9LEFT JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
10LEFT JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID
11GROUP BY p.ProductID, p.Name, pc.Name
12ORDER BY TotalRevenue DESC;

9. JOIN Performance Tips

Performance Considerations

JOINs có thể chậm với large tables. Một số tips:

9.1 Best Practices

SQL
1-- ✅ Tốt: JOIN trên indexed columns
2SELECT *
3FROM Sales.SalesOrderHeader soh
4JOIN Sales.SalesOrderDetail sod
5 ON soh.SalesOrderID = sod.SalesOrderID; -- PK/FK
6
7-- ❌ Tránh: JOIN trên non-indexed columns hoặc functions
8SELECT *
9FROM TableA a
10JOIN TableB b ON YEAR(a.Date) = YEAR(b.Date); -- Không dùng được index

9.2 Filter Early

SQL
1-- ✅ Tốt: Filter trước khi JOIN
2SELECT *
3FROM Sales.SalesOrderHeader soh
4JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
5WHERE soh.OrderDate >= '2014-01-01';
6
7-- ❌ Không tối ưu: JOIN tất cả rồi mới filter

10. Thực hành

Exercises

Hoàn thành các bài tập sau với AdventureWorks:

Exercise 1: Basic INNER JOIN

SQL
1-- Liệt kê Products với Category name
2-- Columns: ProductName, ListPrice, CategoryName
3-- Chỉ products có giá > 1000
4
5-- YOUR CODE HERE
💡 Xem đáp án
SQL
1SELECT
2 p.Name AS ProductName,
3 p.ListPrice,
4 pc.Name AS CategoryName
5FROM Production.Product p
6INNER JOIN Production.ProductSubcategory ps
7 ON p.ProductSubcategoryID = ps.ProductSubcategoryID
8INNER JOIN Production.ProductCategory pc
9 ON ps.ProductCategoryID = pc.ProductCategoryID
10WHERE p.ListPrice > 1000
11ORDER BY pc.Name, p.ListPrice DESC;

Exercise 2: LEFT JOIN - Find orphans

SQL
1-- Tìm tất cả Departments
2-- Đếm số employees hiện tại trong mỗi department
3-- Bao gồm cả departments không có employees
4
5-- YOUR CODE HERE
💡 Xem đáp án
SQL
1SELECT
2 d.Name AS Department,
3 d.GroupName,
4 COUNT(edh.BusinessEntityID) AS EmployeeCount
5FROM HumanResources.Department d
6LEFT JOIN HumanResources.EmployeeDepartmentHistory edh
7 ON d.DepartmentID = edh.DepartmentID
8 AND edh.EndDate IS NULL
9GROUP BY d.DepartmentID, d.Name, d.GroupName
10ORDER BY EmployeeCount DESC;

Exercise 3: Multiple JOINs with Aggregation

SQL
1-- Top 5 Salespersons theo doanh thu
2-- Columns: SalespersonName, TerritoryName, TotalSales, OrderCount
3-- Tính từ SalesOrderHeader
4
5-- YOUR CODE HERE
💡 Xem đáp án
SQL
1SELECT TOP 5
2 p.FirstName + ' ' + p.LastName AS SalespersonName,
3 st.Name AS TerritoryName,
4 SUM(soh.TotalDue) AS TotalSales,
5 COUNT(*) AS OrderCount
6FROM Sales.SalesOrderHeader soh
7INNER JOIN Sales.SalesPerson sp ON soh.SalesPersonID = sp.BusinessEntityID
8INNER JOIN Person.Person p ON sp.BusinessEntityID = p.BusinessEntityID
9INNER JOIN Sales.SalesTerritory st ON soh.TerritoryID = st.TerritoryID
10GROUP BY sp.BusinessEntityID, p.FirstName, p.LastName, st.Name
11ORDER BY TotalSales DESC;

Exercise 4: Anti-Join Pattern

SQL
1-- Tìm Products chưa bao giờ được bán
2-- (không có trong SalesOrderDetail)
3
4-- YOUR CODE HERE
💡 Xem đáp án
SQL
1SELECT
2 p.ProductID,
3 p.Name AS ProductName,
4 p.ListPrice
5FROM Production.Product p
6LEFT JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID
7WHERE sod.ProductID IS NULL
8 AND p.ListPrice > 0 -- Exclude non-sellable products
9ORDER BY p.ListPrice DESC;

11. Tổng kết

JOIN TypeMô tảUse Case
INNER JOINChỉ rows match cả 2 bảngLấy related data
LEFT JOINTất cả từ LEFT + match RIGHTTìm orphans, optional data
RIGHT JOINTất cả từ RIGHT + match LEFTÍt dùng, dùng LEFT thay
FULL JOINTất cả từ cả 2 bảngSo sánh, reconciliation
SELF JOINJOIN với chính nóHierarchies, comparisons

Key Points:

  • JOIN sử dụng Primary KeyForeign Key relationships
  • LEFT JOIN + IS NULL = tìm records không có trong bảng khác
  • Luôn filter sớm để improve performance
  • Sử dụng table aliases để code clean và readable

Bài tiếp theo: Subqueries