Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

T-SQL Cơ bản

Kiến trúc SQL Server

Các thành phần chính của SQL Server Engine

SQL Server Engine được chia thành hai phần lớn: Relational Engine (hay Query Processor) và Storage Engine.

SQL Server Instance
├── Relational Engine (Query Processor)
│   ├── Command Parser       -- Phân tích cú pháp T-SQL
│   ├── Query Optimizer      -- Lựa chọn execution plan tối ưu
│   └── Query Executor       -- Thực thi execution plan
└── Storage Engine
    ├── Buffer Manager       -- Quản lý bộ nhớ (Buffer Pool)
    ├── Log Manager          -- Quản lý Transaction Log
    ├── Access Methods       -- B-tree traversal, heap scans
    └── Lock Manager         -- Quản lý locking và concurrency

Relational Engine (Query Processor)

Thành phầnVai trò
Command ParserKiểm tra cú pháp T-SQL, tạo parse tree
Query OptimizerTạo execution plan dựa trên statistics và cost model
Query ExecutorThực thi từng bước trong execution plan

Storage Engine

Thành phầnVai trò
Buffer ManagerQuản lý Buffer Pool (cache data pages trong RAM)
Log ManagerGhi WAL (Write-Ahead Logging) vào transaction log
Access MethodsDuyệt B-tree index, heap tables
Lock ManagerQuản lý locking, deadlock detection

Database Files

SQL Server tổ chức dữ liệu trên disk theo 3 loại file:

FileExtensionVai trò
Primary Data File.mdfFile dữ liệu chính, chứa system tables và user data
Secondary Data File.ndfFile dữ liệu bổ sung (filegroups khác nhau)
Log File.ldfTransaction log, dùng cho recovery và replication
-- Xem thông tin files của database hiện tại
SELECT 
    name,
    physical_name,
    type_desc,
    size * 8 / 1024 AS size_mb,
    max_size,
    growth
FROM sys.database_files;

Lưu ý: Log file (.ldf) ghi tuần tự (sequential write) nên nên đặt trên disk riêng với data files để tối ưu I/O.


Pages và Extents

Pages (Trang)

  • Đơn vị lưu trữ nhỏ nhất trong SQL Server là page, kích thước cố định 8KB (8192 bytes).
  • Mỗi page có header 96 bytes, còn lại ~8096 bytes để lưu data.
  • Các loại page chính:
Page TypeMô tả
DataChứa dữ liệu của heap tables
IndexChứa B-tree index nodes
LOBLarge Object data (nvarchar(MAX), varbinary(MAX))
IAMIndex Allocation Map - theo dõi pages thuộc object nào
PFSPage Free Space - theo dõi không gian trống

Extents (Nhóm trang)

  • Extent = 8 pages liền kề = 64KB.
  • Hai loại extent:
LoạiMô tả
Uniform ExtentTất cả 8 pages thuộc cùng 1 object (dùng khi table lớn)
Mixed ExtentCác pages có thể thuộc nhiều objects khác nhau (dùng khi table nhỏ ≤ 8 pages)
-- Xem số pages của từng table
SELECT 
    t.name AS table_name,
    p.rows,
    SUM(a.total_pages) * 8 AS total_kb,
    SUM(a.used_pages) * 8 AS used_kb
FROM sys.tables t
JOIN sys.indexes i ON t.object_id = i.object_id
JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
JOIN sys.allocation_units a ON p.partition_id = a.container_id
GROUP BY t.name, p.rows
ORDER BY total_kb DESC;

Memory Architecture

Buffer Pool

Buffer Pool là vùng nhớ lớn nhất trong SQL Server, được dùng để cache data pages đọc từ disk.

Memory (RAM)
└── Buffer Pool
    ├── Data Page Cache      -- Cache các 8KB data pages
    ├── Plan Cache           -- Cache các execution plans
    ├── Log Cache            -- Cache log records trước khi flush
    └── Other caches         -- Connection info, metadata, etc.

Nguyên tắc hoạt động:

  1. SQL Server đọc page từ disk → lưu vào Buffer Pool
  2. Lần sau đọc page đó → lấy trực tiếp từ RAM (không đọc disk)
  3. Khi RAM đầy → dùng LRU (Least Recently Used) để evict pages cũ
-- Xem Buffer Pool usage
SELECT 
    database_id,
    COUNT(*) AS cached_pages,
    COUNT(*) * 8 / 1024 AS cached_mb
FROM sys.dm_os_buffer_descriptors
GROUP BY database_id
ORDER BY cached_pages DESC;

Plan Cache

Execution plans được cache lại để tái sử dụng:

-- Xem các plans đang được cache
SELECT 
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us,
    SUBSTRING(qt.text, (qs.statement_start_offset/2) + 1,
        ((qs.statement_end_offset - qs.statement_start_offset)/2) + 1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_elapsed_time DESC;

SQL Server Instance vs Database

InstanceDatabase
Định nghĩaMột cài đặt SQL Server EngineTập hợp dữ liệu logic trong instance
Cấu trúcQuản lý memory, logins, linked serversTables, views, stored procedures, schemas
MultipleNhiều databases trong 1 instanceNhiều schemas trong 1 database
IsolationShared engine resourcesCross-database queries được phép
-- Xem thông tin instance
SELECT 
    @@SERVERNAME AS server_name,
    @@VERSION AS version,
    SERVERPROPERTY('Edition') AS edition,
    SERVERPROPERTY('ProductVersion') AS product_version;

-- Xem các databases trong instance
SELECT name, state_desc, recovery_model_desc
FROM sys.databases
ORDER BY name;

T-SQL Cơ bản

SELECT, WHERE, ORDER BY

-- SELECT cơ bản
SELECT column1, column2, column3
FROM TableName;

-- Lấy tất cả cột (tránh dùng trong production)
SELECT * FROM Employees;

-- WHERE để lọc dữ liệu
SELECT EmployeeId, FullName, Salary
FROM Employees
WHERE DepartmentId = 5
  AND Salary > 50000
  AND HireDate >= '2020-01-01';

-- ORDER BY để sắp xếp
SELECT EmployeeId, FullName, Salary
FROM Employees
ORDER BY Salary DESC, FullName ASC;

GROUP BY và HAVING

-- GROUP BY - nhóm và tổng hợp
SELECT 
    DepartmentId,
    COUNT(*) AS employee_count,
    AVG(Salary) AS avg_salary,
    MAX(Salary) AS max_salary,
    MIN(Salary) AS min_salary
FROM Employees
GROUP BY DepartmentId;

-- HAVING - lọc trên kết quả aggregate (khác WHERE)
SELECT 
    DepartmentId,
    COUNT(*) AS employee_count,
    AVG(Salary) AS avg_salary
FROM Employees
WHERE IsActive = 1                    -- WHERE lọc trước GROUP BY
GROUP BY DepartmentId
HAVING COUNT(*) >= 5                  -- HAVING lọc sau GROUP BY
    AND AVG(Salary) > 60000
ORDER BY avg_salary DESC;

Thứ tự thực thi: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

Aliases và DISTINCT

-- Column alias
SELECT 
    e.EmployeeId AS emp_id,
    e.FirstName + ' ' + e.LastName AS full_name,
    d.DepartmentName AS dept
FROM Employees e                      -- Table alias
JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- DISTINCT - loại bỏ hàng trùng lặp
SELECT DISTINCT DepartmentId FROM Employees;

-- DISTINCT với nhiều cột
SELECT DISTINCT DepartmentId, JobTitle 
FROM Employees
ORDER BY DepartmentId;

TOP và OFFSET-FETCH

-- TOP N - lấy N hàng đầu
SELECT TOP 10 EmployeeId, FullName, Salary
FROM Employees
ORDER BY Salary DESC;

-- TOP với PERCENT
SELECT TOP 10 PERCENT EmployeeId, FullName
FROM Employees;

-- TOP WITH TIES - bao gồm các hàng có giá trị bằng nhau
SELECT TOP 5 WITH TIES EmployeeId, Salary
FROM Employees
ORDER BY Salary DESC;

-- OFFSET-FETCH (SQL Server 2012+) - phân trang chuẩn
DECLARE @PageSize INT = 10;
DECLARE @PageNumber INT = 2;  -- Trang thứ 2

SELECT EmployeeId, FullName, Salary
FROM Employees
ORDER BY EmployeeId
OFFSET (@PageNumber - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

Lưu ý: OFFSET-FETCH yêu cầu ORDER BY. Ưu tiên dùng OFFSET-FETCH thay vì TOP cho phân trang vì chuẩn hơn.

LIKE và Wildcard

-- % - khớp 0 hoặc nhiều ký tự
SELECT * FROM Employees WHERE LastName LIKE 'Smith%';    -- Bắt đầu bằng Smith
SELECT * FROM Employees WHERE Email LIKE '%@gmail.com';  -- Kết thúc bằng @gmail.com
SELECT * FROM Employees WHERE FullName LIKE '%Nguyen%';  -- Chứa Nguyen

-- _ - khớp đúng 1 ký tự
SELECT * FROM Products WHERE ProductCode LIKE 'A__';     -- A + 2 ký tự bất kỳ

-- [] - khớp 1 ký tự trong tập hợp
SELECT * FROM Employees WHERE LastName LIKE '[ABC]%';    -- Bắt đầu bằng A, B hoặc C

-- [^] - khớp 1 ký tự KHÔNG trong tập hợp
SELECT * FROM Employees WHERE LastName LIKE '[^ABC]%';   -- Không bắt đầu bằng A, B, C

-- Thoát ký tự đặc biệt với ESCAPE
SELECT * FROM Products WHERE Description LIKE '50\%' ESCAPE '\';  -- Chứa "50%"

Xử lý NULL

-- IS NULL / IS NOT NULL (không dùng = NULL)
SELECT * FROM Employees WHERE ManagerId IS NULL;
SELECT * FROM Employees WHERE ManagerId IS NOT NULL;

-- ISNULL(expression, replacement)  - SQL Server specific
SELECT 
    EmployeeId,
    ISNULL(MiddleName, '') AS middle_name,
    ISNULL(Phone, 'N/A') AS phone
FROM Employees;

-- COALESCE(val1, val2, ...) - trả về giá trị non-NULL đầu tiên (ANSI standard)
SELECT 
    EmployeeId,
    COALESCE(MobilePhone, OfficePhone, HomePhone, 'No phone') AS contact_phone
FROM Employees;

-- NULLIF(expr1, expr2) - trả về NULL nếu hai giá trị bằng nhau
-- Dùng để tránh chia cho 0
SELECT 
    Revenue,
    Cost,
    Revenue / NULLIF(Cost, 0) AS revenue_ratio
FROM FinancialData;

-- NULL trong aggregate functions (NULL bị bỏ qua)
SELECT 
    COUNT(*) AS total_rows,         -- Đếm tất cả hàng
    COUNT(Commission) AS has_commission, -- Đếm hàng có Commission != NULL
    AVG(Commission) AS avg_comm,    -- AVG bỏ qua NULL
    SUM(Commission) AS total_comm
FROM Employees;

CASE Expressions

-- Simple CASE (so sánh bằng)
SELECT 
    EmployeeId,
    FullName,
    Status,
    CASE Status
        WHEN 'A' THEN 'Active'
        WHEN 'I' THEN 'Inactive'
        WHEN 'T' THEN 'Terminated'
        ELSE 'Unknown'
    END AS status_description
FROM Employees;

-- Searched CASE (điều kiện logic)
SELECT 
    EmployeeId,
    FullName,
    Salary,
    CASE 
        WHEN Salary < 30000 THEN 'Entry Level'
        WHEN Salary BETWEEN 30000 AND 60000 THEN 'Mid Level'
        WHEN Salary BETWEEN 60001 AND 100000 THEN 'Senior Level'
        WHEN Salary > 100000 THEN 'Executive'
        ELSE 'Unknown'
    END AS salary_band
FROM Employees;

-- CASE trong ORDER BY
SELECT EmployeeId, FullName, Priority
FROM Tasks
ORDER BY 
    CASE Priority
        WHEN 'High' THEN 1
        WHEN 'Medium' THEN 2
        WHEN 'Low' THEN 3
        ELSE 4
    END;

-- CASE trong UPDATE
UPDATE Employees
SET Salary = Salary * 
    CASE 
        WHEN YearsOfService >= 10 THEN 1.10  -- Tăng 10%
        WHEN YearsOfService >= 5  THEN 1.07  -- Tăng 7%
        ELSE 1.05                            -- Tăng 5%
    END;

String Functions (Hàm xử lý chuỗi)

-- LEN - độ dài chuỗi (không tính trailing spaces)
SELECT LEN('Hello World');   -- 11
SELECT LEN('  Hello  ');     -- 9 (không tính 2 spaces cuối)

-- SUBSTRING(string, start, length) - cắt chuỗi (1-indexed)
SELECT SUBSTRING('Hello World', 7, 5);   -- 'World'
SELECT SUBSTRING(Email, 1, CHARINDEX('@', Email) - 1) AS username
FROM Employees;

-- CHARINDEX(pattern, string [, start]) - tìm vị trí chuỗi con
SELECT CHARINDEX('@', 'user@example.com');       -- 5
SELECT CHARINDEX('a', 'banana', 3);              -- 4 (bắt đầu tìm từ vị trí 3)

-- REPLACE(string, old, new) - thay thế
SELECT REPLACE('Hello World', 'World', 'SQL');   -- 'Hello SQL'
SELECT REPLACE(Phone, '-', '') AS cleaned_phone FROM Employees;

-- TRIM, LTRIM, RTRIM
SELECT TRIM('  Hello  ');    -- 'Hello'  (SQL Server 2017+)
SELECT LTRIM('  Hello  ');   -- 'Hello  '
SELECT RTRIM('  Hello  ');   -- '  Hello'
SELECT TRIM('x' FROM 'xxxHelloxxx');  -- 'Hello' (SQL Server 2017+)

-- UPPER, LOWER
SELECT UPPER('hello world');  -- 'HELLO WORLD'
SELECT LOWER('HELLO WORLD');  -- 'hello world'

-- CONCAT - nối chuỗi (NULL-safe)
SELECT CONCAT('Hello', ' ', 'World', '!');       -- 'Hello World!'
SELECT CONCAT(FirstName, ' ', LastName) AS full_name FROM Employees;

-- CONCAT_WS - nối với separator (SQL Server 2017+)
SELECT CONCAT_WS(', ', City, State, Country);    -- 'Hanoi, HN, Vietnam'

-- FORMAT - định dạng (costly, tránh dùng trong queries lớn)
SELECT FORMAT(12345.678, 'N2');          -- '12,345.68'
SELECT FORMAT(GETDATE(), 'dd/MM/yyyy');  -- '01/04/2026'
SELECT FORMAT(Salary, 'C', 'vi-VN');    -- Định dạng tiền VN

-- REPLICATE - lặp chuỗi
SELECT REPLICATE('*', 10);              -- '**********'

-- REVERSE
SELECT REVERSE('Hello');                -- 'olleH'

-- STRING_SPLIT (SQL Server 2016+)
SELECT value FROM STRING_SPLIT('a,b,c,d', ',');

-- LEFT, RIGHT
SELECT LEFT('Hello World', 5);          -- 'Hello'
SELECT RIGHT('Hello World', 5);         -- 'World'

-- PATINDEX - tìm pattern (hỗ trợ wildcard)
SELECT PATINDEX('%[0-9]%', 'abc123def');  -- 4 (vị trí số đầu tiên)

Date Functions (Hàm ngày tháng)

-- Hàm lấy ngày/giờ hiện tại
SELECT GETDATE();           -- datetime, local time
SELECT GETUTCDATE();        -- datetime, UTC time
SELECT SYSDATETIME();       -- datetime2(7), local time, cao hơn độ chính xác
SELECT SYSUTCDATETIME();    -- datetime2(7), UTC time
SELECT SYSDATETIMEOFFSET(); -- datetimeoffset, kèm timezone offset

-- DATEADD(part, number, date) - thêm/bớt thời gian
SELECT DATEADD(DAY, 30, GETDATE());           -- 30 ngày sau
SELECT DATEADD(MONTH, -3, GETDATE());         -- 3 tháng trước
SELECT DATEADD(YEAR, 1, '2025-01-01');        -- '2026-01-01'
SELECT DATEADD(HOUR, 2, GETDATE());           -- 2 giờ sau
SELECT DATEADD(MINUTE, -30, GETDATE());       -- 30 phút trước

-- DATEDIFF(part, start, end) - khoảng cách thời gian
SELECT DATEDIFF(DAY, '2025-01-01', '2026-01-01');   -- 365
SELECT DATEDIFF(MONTH, HireDate, GETDATE()) AS months_employed FROM Employees;
SELECT DATEDIFF(YEAR, BirthDate, GETDATE()) AS age FROM Employees;

-- DATEPART(part, date) - lấy phần của ngày
SELECT DATEPART(YEAR, GETDATE());    -- 2026
SELECT DATEPART(MONTH, GETDATE());   -- 4
SELECT DATEPART(DAY, GETDATE());     -- 1
SELECT DATEPART(WEEKDAY, GETDATE()); -- Ngày trong tuần (1=Sunday)
SELECT DATEPART(HOUR, GETDATE());    -- Giờ

-- DATENAME - tương tự DATEPART nhưng trả text
SELECT DATENAME(MONTH, GETDATE());   -- 'April'
SELECT DATENAME(WEEKDAY, GETDATE()); -- 'Wednesday'

-- DAY(), MONTH(), YEAR() - shorthand
SELECT DAY(GETDATE()), MONTH(GETDATE()), YEAR(GETDATE());

-- FORMAT với ngày (chậm hơn CONVERT, dùng cho display)
SELECT FORMAT(GETDATE(), 'dd/MM/yyyy');          -- '01/04/2026'
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss'); -- '2026-04-01 10:30:00'

-- CONVERT với ngày (nhanh hơn FORMAT)
SELECT CONVERT(VARCHAR(10), GETDATE(), 103);   -- '01/04/2026' (dd/mm/yyyy)
SELECT CONVERT(VARCHAR(10), GETDATE(), 120);   -- '2026-04-01' (yyyy-mm-dd)
SELECT CONVERT(DATE, GETDATE());               -- Cắt phần time

-- EOMONTH - ngày cuối tháng (SQL Server 2012+)
SELECT EOMONTH(GETDATE());           -- Ngày cuối tháng hiện tại
SELECT EOMONTH('2026-02-01');        -- '2026-02-28'

-- DATEFROMPARTS, DATETIMEFROMPARTS (SQL Server 2012+)
SELECT DATEFROMPARTS(2026, 4, 1);       -- '2026-04-01'
SELECT DATETIMEFROMPARTS(2026, 4, 1, 10, 30, 0, 0);  -- '2026-04-01 10:30:00'

-- Ví dụ thực tế: tìm nhân viên thuê trong 90 ngày qua
SELECT EmployeeId, FullName, HireDate
FROM Employees
WHERE HireDate >= DATEADD(DAY, -90, GETDATE())
  AND HireDate < GETDATE();

Math Functions (Hàm toán học)

-- ROUND(number, decimal_places [, truncate])
SELECT ROUND(3.14159, 2);     -- 3.14
SELECT ROUND(3.145, 2);       -- 3.15 (round half up)
SELECT ROUND(3.145, 2, 1);    -- 3.14 (truncate, không round)

-- ABS - giá trị tuyệt đối
SELECT ABS(-42);               -- 42
SELECT ABS(3.14);              -- 3.14

-- CEILING - làm tròn lên (trần)
SELECT CEILING(3.1);           -- 4
SELECT CEILING(-3.9);          -- -3

-- FLOOR - làm tròn xuống (sàn)
SELECT FLOOR(3.9);             -- 3
SELECT FLOOR(-3.1);            -- -4

-- POWER(base, exponent)
SELECT POWER(2, 10);           -- 1024
SELECT POWER(3.0, 3);          -- 27.0

-- SQRT - căn bậc hai
SELECT SQRT(144);              -- 12.0

-- SQUARE
SELECT SQUARE(5);              -- 25

-- PI
SELECT PI();                   -- 3.14159265358979

-- LOG, LOG10, EXP
SELECT LOG(100);               -- Logarithm tự nhiên
SELECT LOG(100, 10);           -- Log base 10 = 2 (SQL Server 2012+)
SELECT LOG10(1000);            -- 3

-- SIGN - dấu của số
SELECT SIGN(-5);   -- -1
SELECT SIGN(0);    -- 0
SELECT SIGN(5);    -- 1

-- Ví dụ thực tế: tính commission
SELECT 
    EmployeeId,
    Sales,
    ROUND(Sales * 0.05, 2) AS commission,
    CEILING(Sales / 1000.0) AS bonus_units
FROM SalesData;

Aggregate Functions (Hàm tổng hợp)

-- COUNT
SELECT COUNT(*) AS total FROM Employees;              -- Đếm tất cả hàng (kể cả NULL)
SELECT COUNT(Email) AS has_email FROM Employees;      -- Đếm hàng có Email NOT NULL
SELECT COUNT(DISTINCT DepartmentId) AS dept_count FROM Employees;  -- Đếm distinct values

-- SUM
SELECT SUM(Salary) AS total_salary FROM Employees;
SELECT SUM(Salary) AS total_salary FROM Employees WHERE IsActive = 1;

-- AVG (bỏ qua NULL)
SELECT AVG(Salary) AS avg_salary FROM Employees;
-- Chú ý: AVG(int) trả về int! Dùng AVG(CAST(col AS decimal))
SELECT AVG(CAST(Score AS DECIMAL(10,2))) AS avg_score FROM Students;

-- MIN, MAX
SELECT MIN(Salary) AS min_salary, MAX(Salary) AS max_salary FROM Employees;
SELECT MIN(HireDate) AS first_hire, MAX(HireDate) AS last_hire FROM Employees;

-- Kết hợp với GROUP BY
SELECT 
    d.DepartmentName,
    COUNT(e.EmployeeId) AS headcount,
    MIN(e.Salary) AS min_salary,
    MAX(e.Salary) AS max_salary,
    AVG(e.Salary) AS avg_salary,
    SUM(e.Salary) AS total_payroll
FROM Departments d
LEFT JOIN Employees e ON d.DepartmentId = e.DepartmentId
GROUP BY d.DepartmentName
ORDER BY total_payroll DESC;

-- STRING_AGG - gộp chuỗi (SQL Server 2017+)
SELECT 
    DepartmentId,
    STRING_AGG(FullName, ', ') WITHIN GROUP (ORDER BY FullName) AS employees
FROM Employees
GROUP BY DepartmentId;

Joins (Phép kết bảng)

-- INNER JOIN - chỉ trả về hàng có khớp ở cả 2 bảng
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- LEFT JOIN - tất cả từ bảng trái, NULL với bảng phải nếu không khớp
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
LEFT JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- RIGHT JOIN - tất cả từ bảng phải
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
RIGHT JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- FULL OUTER JOIN - tất cả từ cả 2 bảng
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
FULL OUTER JOIN Departments d ON e.DepartmentId = d.DepartmentId;

-- CROSS JOIN - tích Descartes (n × m rows)
SELECT p.ProductName, c.ColorName
FROM Products p
CROSS JOIN Colors c;

-- SELF JOIN - bảng join với chính nó
SELECT 
    e.EmployeeId,
    e.FullName AS employee,
    m.FullName AS manager
FROM Employees e
LEFT JOIN Employees m ON e.ManagerId = m.EmployeeId;

-- Multi-table join
SELECT 
    o.OrderId,
    c.CustomerName,
    e.FullName AS salesperson,
    SUM(od.Quantity * od.UnitPrice) AS order_total
FROM Orders o
JOIN Customers c ON o.CustomerId = c.CustomerId
JOIN Employees e ON o.EmployeeId = e.EmployeeId
JOIN OrderDetails od ON o.OrderId = od.OrderId
GROUP BY o.OrderId, c.CustomerName, e.FullName;