Bài 3: SQL JOINs
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
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
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.
1SELECT columns2FROM table13INNER JOIN table2 ON table1.key = table2.key;3.2 Examples với AdventureWorks
Basic INNER JOIN:
1-- Employees và Department hiện tại2SELECT 3 e.BusinessEntityID,4 e.JobTitle,5 d.Name AS Department6FROM HumanResources.Employee e7INNER JOIN HumanResources.EmployeeDepartmentHistory edh 8 ON e.BusinessEntityID = edh.BusinessEntityID9INNER JOIN HumanResources.Department d 10 ON edh.DepartmentID = d.DepartmentID11WHERE edh.EndDate IS NULL; -- Current department onlyProducts với Category:
1-- Products và danh mục của chúng2SELECT 3 p.Name AS ProductName,4 p.ListPrice,5 ps.Name AS Subcategory,6 pc.Name AS Category7FROM Production.Product p8INNER JOIN Production.ProductSubcategory ps 9 ON p.ProductSubcategoryID = ps.ProductSubcategoryID10INNER JOIN Production.ProductCategory pc 11 ON ps.ProductCategoryID = pc.ProductCategoryID12ORDER BY pc.Name, ps.Name, p.Name;Sales Orders với Customer Info:
1-- Đơn hàng với thông tin khách hàng2SELECT 3 soh.SalesOrderID,4 soh.OrderDate,5 soh.TotalDue,6 p.FirstName + ' ' + p.LastName AS CustomerName7FROM Sales.SalesOrderHeader soh8INNER JOIN Sales.Customer c ON soh.CustomerID = c.CustomerID9INNER JOIN Person.Person p ON c.PersonID = p.BusinessEntityID10ORDER BY soh.OrderDate DESC;3.3 INNER JOIN với Aggregation
1-- Doanh thu theo Category2SELECT 3 pc.Name AS Category,4 COUNT(DISTINCT sod.SalesOrderID) AS OrderCount,5 SUM(sod.LineTotal) AS TotalRevenue6FROM Sales.SalesOrderDetail sod7INNER JOIN Production.Product p ON sod.ProductID = p.ProductID8INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID9INNER JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID10GROUP BY pc.Name11ORDER 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.
1SELECT columns2FROM table13LEFT JOIN table2 ON table1.key = table2.key;4.2 Examples
Tất cả Products, kể cả không có Subcategory:
1SELECT 2 p.Name AS ProductName,3 p.ListPrice,4 ps.Name AS Subcategory5FROM Production.Product p6LEFT JOIN Production.ProductSubcategory ps 7 ON p.ProductSubcategoryID = ps.ProductSubcategoryID;Tìm Products không có Subcategory:
1-- Products chưa được phân loại2SELECT 3 p.ProductID,4 p.Name AS ProductName,5 p.ListPrice6FROM Production.Product p7LEFT JOIN Production.ProductSubcategory ps 8 ON p.ProductSubcategoryID = ps.ProductSubcategoryID9WHERE ps.ProductSubcategoryID IS NULL;Tất cả Customers, kể cả chưa đặt hàng:
1SELECT 2 c.CustomerID,3 p.FirstName + ' ' + p.LastName AS CustomerName,4 COUNT(soh.SalesOrderID) AS OrderCount,5 ISNULL(SUM(soh.TotalDue), 0) AS TotalSpent6FROM Sales.Customer c7LEFT JOIN Person.Person p ON c.PersonID = p.BusinessEntityID8LEFT JOIN Sales.SalesOrderHeader soh ON c.CustomerID = soh.CustomerID9GROUP BY c.CustomerID, p.FirstName, p.LastName10ORDER BY TotalSpent DESC;4.3 Tìm rows không match (Anti-Join)
Pattern phổ biến để tìm records "orphan" hoặc records không có trong bảng khác.
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.JobTitle6FROM HumanResources.Employee e7LEFT JOIN Person.Person p ON e.BusinessEntityID = p.BusinessEntityID8LEFT JOIN Sales.SalesPerson sp ON e.BusinessEntityID = sp.BusinessEntityID9WHERE 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.
1SELECT columns2FROM table13RIGHT JOIN table2 ON table1.key = table2.key;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
1-- Tương đương với LEFT JOIN, chỉ đổi thứ tự2-- Tất cả Departments, kể cả không có Employees3SELECT 4 d.Name AS Department,5 COUNT(edh.BusinessEntityID) AS EmployeeCount6FROM HumanResources.EmployeeDepartmentHistory edh7RIGHT JOIN HumanResources.Department d 8 ON edh.DepartmentID = d.DepartmentID9 AND edh.EndDate IS NULL10GROUP BY d.DepartmentID, d.Name11ORDER 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.
1SELECT columns2FROM table13FULL OUTER JOIN table2 ON table1.key = table2.key;6.2 Example
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 InventoryQty7FROM Production.Product p8FULL OUTER JOIN Production.ProductInventory pi 9 ON p.ProductID = pi.ProductID10ORDER 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
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 ManagerTitle9FROM HumanResources.Employee e10LEFT JOIN HumanResources.Employee m 11 ON e.OrganizationNode.GetAncestor(1) = m.OrganizationNode12LEFT JOIN Person.Person ep ON e.BusinessEntityID = ep.BusinessEntityID13LEFT JOIN Person.Person mp ON m.BusinessEntityID = mp.BusinessEntityID14ORDER BY e.OrganizationLevel, e.BusinessEntityID;8. Multiple JOINs
8.1 Joining nhiều tables
1-- Full Sales Order với tất cả thông tin2SELECT 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.LineTotal12FROM Sales.SalesOrderHeader soh13-- Customer info14JOIN Sales.Customer c ON soh.CustomerID = c.CustomerID15JOIN Person.Person p ON c.PersonID = p.BusinessEntityID16-- Address info17JOIN Person.BusinessEntityAddress bea ON c.PersonID = bea.BusinessEntityID18JOIN Person.Address a ON bea.AddressID = a.AddressID19JOIN Person.StateProvince sp ON a.StateProvinceID = sp.StateProvinceID20-- Territory21JOIN Sales.SalesTerritory st ON soh.TerritoryID = st.TerritoryID22-- Order details23JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID24JOIN Production.Product prod ON sod.ProductID = prod.ProductID25WHERE soh.OrderDate >= '2014-01-01'26ORDER BY soh.OrderDate DESC, soh.SalesOrderNumber;8.2 Mixed JOIN Types
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 TotalRevenue7FROM Production.Product p8LEFT JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID9LEFT JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID10LEFT JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID11GROUP BY p.ProductID, p.Name, pc.Name12ORDER BY TotalRevenue DESC;9. JOIN Performance Tips
JOINs có thể chậm với large tables. Một số tips:
9.1 Best Practices
1-- ✅ Tốt: JOIN trên indexed columns2SELECT *3FROM Sales.SalesOrderHeader soh4JOIN Sales.SalesOrderDetail sod 5 ON soh.SalesOrderID = sod.SalesOrderID; -- PK/FK67-- ❌ Tránh: JOIN trên non-indexed columns hoặc functions8SELECT *9FROM TableA a10JOIN TableB b ON YEAR(a.Date) = YEAR(b.Date); -- Không dùng được index9.2 Filter Early
1-- ✅ Tốt: Filter trước khi JOIN2SELECT *3FROM Sales.SalesOrderHeader soh4JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID5WHERE soh.OrderDate >= '2014-01-01';67-- ❌ Không tối ưu: JOIN tất cả rồi mới filter10. Thực hành
Hoàn thành các bài tập sau với AdventureWorks:
Exercise 1: Basic INNER JOIN
1-- Liệt kê Products với Category name2-- Columns: ProductName, ListPrice, CategoryName3-- Chỉ products có giá > 100045-- YOUR CODE HERE💡 Xem đáp án
1SELECT 2 p.Name AS ProductName,3 p.ListPrice,4 pc.Name AS CategoryName5FROM Production.Product p6INNER JOIN Production.ProductSubcategory ps 7 ON p.ProductSubcategoryID = ps.ProductSubcategoryID8INNER JOIN Production.ProductCategory pc 9 ON ps.ProductCategoryID = pc.ProductCategoryID10WHERE p.ListPrice > 100011ORDER BY pc.Name, p.ListPrice DESC;Exercise 2: LEFT JOIN - Find orphans
1-- Tìm tất cả Departments2-- Đếm số employees hiện tại trong mỗi department3-- Bao gồm cả departments không có employees45-- YOUR CODE HERE💡 Xem đáp án
1SELECT 2 d.Name AS Department,3 d.GroupName,4 COUNT(edh.BusinessEntityID) AS EmployeeCount5FROM HumanResources.Department d6LEFT JOIN HumanResources.EmployeeDepartmentHistory edh 7 ON d.DepartmentID = edh.DepartmentID8 AND edh.EndDate IS NULL9GROUP BY d.DepartmentID, d.Name, d.GroupName10ORDER BY EmployeeCount DESC;Exercise 3: Multiple JOINs with Aggregation
1-- Top 5 Salespersons theo doanh thu2-- Columns: SalespersonName, TerritoryName, TotalSales, OrderCount3-- Tính từ SalesOrderHeader45-- YOUR CODE HERE💡 Xem đáp án
1SELECT TOP 52 p.FirstName + ' ' + p.LastName AS SalespersonName,3 st.Name AS TerritoryName,4 SUM(soh.TotalDue) AS TotalSales,5 COUNT(*) AS OrderCount6FROM Sales.SalesOrderHeader soh7INNER JOIN Sales.SalesPerson sp ON soh.SalesPersonID = sp.BusinessEntityID8INNER JOIN Person.Person p ON sp.BusinessEntityID = p.BusinessEntityID9INNER JOIN Sales.SalesTerritory st ON soh.TerritoryID = st.TerritoryID10GROUP BY sp.BusinessEntityID, p.FirstName, p.LastName, st.Name11ORDER BY TotalSales DESC;Exercise 4: Anti-Join Pattern
1-- Tìm Products chưa bao giờ được bán2-- (không có trong SalesOrderDetail)34-- YOUR CODE HERE💡 Xem đáp án
1SELECT 2 p.ProductID,3 p.Name AS ProductName,4 p.ListPrice5FROM Production.Product p6LEFT JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID7WHERE sod.ProductID IS NULL8 AND p.ListPrice > 0 -- Exclude non-sellable products9ORDER BY p.ListPrice DESC;11. Tổng kết
| JOIN Type | Mô tả | Use Case |
|---|---|---|
| INNER JOIN | Chỉ rows match cả 2 bảng | Lấy related data |
| LEFT JOIN | Tất cả từ LEFT + match RIGHT | Tìm orphans, optional data |
| RIGHT JOIN | Tất cả từ RIGHT + match LEFT | Ít dùng, dùng LEFT thay |
| FULL JOIN | Tất cả từ cả 2 bảng | So sánh, reconciliation |
| SELF JOIN | JOIN với chính nó | Hierarchies, comparisons |
Key Points:
- JOIN sử dụng Primary Key và Foreign 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
